Code viewer for World: Complex World (clone by Na...

// Cloned by Snavy Pitah on 30 Oct 2023 from World "Complex World (clone by Nakyung Kim)" by Nakyung Kim 
// Please leave this clone trail here.
 


// Practical 1
// A* practical
// The practical is to use A* to make a better enemy in "Complex World".
// ====================================================================================================================

// =============================================================================================
// More complex starter World 
// 3d-effect Maze World (really a 2-D problem)
// Movement is on a semi-visible grid of squares 
//
// This more complex World shows:
// - Skybox
// - Internal maze (randomly drawn each time)
// - Enemy actively chases agent
// - Music/audio
// - 2D world (clone this and set show3d = false)
// - User keyboard control (clone this and comment out Mind actions to see)
// =============================================================================================

// =============================================================================================
// Scoring:
// Bad steps = steps where enemy is within one step of agent.
// Good steps = steps where enemy is further away. 
// Score = good steps as percentage of all steps.
//
// There are situations where agent is trapped and cannot move.
// If this happens, you score zero.
// =============================================================================================

// ===================================================================================================================
// === Start of tweaker's box ======================================================================================== 
// ===================================================================================================================

// The easiest things to modify are in this box.
// You should be able to change things in this box without being a JavaScript programmer.
// Go ahead and change some of these. What's the worst that could happen?

AB.clockTick       = 100;    

	// Speed of run: Step every n milliseconds. Default 100.
	
AB.maxSteps        = 1000;    

	// Length of run: Maximum length of run in steps. Default 1000.

AB.screenshotStep  = 50;   
  
	// Take screenshot on this step. (All resources should have finished loading.) Default 50.

//---- global constants: -------------------------------------------------------

	const show3d = true;		// Switch between 3d and 2d view (both using Three.js) 


    const TEXTURE_WALL 	= '/uploads/jackie2002/gold.png' ; //changed done
    const TEXTURE_MAZE 	= '/uploads/jackie2002/mercury.jpeg' ; 
    const TEXTURE_AGENT = '/uploads/jackie2002/moon1.jpeg' ;
    const TEXTURE_ENEMY = '/uploads/jackie2002/sun.jpeg' ;
 
    const MUSIC_BACK  = '/uploads/jackie2002/spcae.mp3' ; // changed the BGM
	const SOUND_ALARM = '/uploads/jackie2002/mario.mp3' ; // changed the alarm sound

    const gridsize = 25;						// number of squares along side of world 20 -> 25   
    
    const NOBOXES =  Math.trunc ( (gridsize * gridsize) / 6 ); 
    		// density of maze - number of internal boxes   (gridsize * gridsize) / 10 -> (gridsize * gridsize) / 4
    		// (bug) use trunc or can get a non-integer 
    
    const squaresize = 150;					// size of square in pixels -> change 100 -> 150
    
    const MAXPOS = gridsize * squaresize;		// length of one side in pixels 
    	
    const SKYCOLOR 	= 0xddffdd;				// a number, not a string 
    
     
    const startRadiusConst	 	= MAXPOS * 0.8 ;		// distance from centre to start the camera at
    const maxRadiusConst 		= MAXPOS * 10  ;		// maximum distance from camera we will render things  

//--- change ABWorld defaults: -------------------------------

ABHandler.MAXCAMERAPOS 	= maxRadiusConst ;
ABHandler.GROUNDZERO		= true;						// "ground" exists at altitude zero


//--- skybox: -------------------------------
// skybox is a collection of 6 files 
// x,y,z positive and negative faces have to be in certain order in the array 

 const SKYBOX_ARRAY = [										 
                '/uploads/jackie2002/1px.png',
                '/uploads/jackie2002/1nx.png',
                '/uploads/jackie2002/1py.png',
                '/uploads/jackie2002/1ny.png',
                '/uploads/jackie2002/1pz.png',
                '/uploads/jackie2002/1nz.png'
                ];
                
//background credit: https://pixexid.com/image/a-cyberpunk-city-skyline-at-night-gxxewnru

// https://jaxry.github.io/panorama-to-cubemap/
// I made my cubemap here. I used 3D AI generate image to make cube map.

// ===================================================================================================================
// === End of tweaker's box ==========================================================================================
// ===================================================================================================================

//--- Mind can pick one of these actions -----------------
 
 
const ACTION_LEFT = 0;
const ACTION_RIGHT = 1;
const ACTION_UP = 2;
const ACTION_DOWN = 3;
const ACTION_STAYSTILL = 4;

// in initial view, (smaller-larger) on i axis is aligned with (left-right)
// in initial view, (smaller-larger) on j axis is aligned with (away from you - towards you)

// contents of a grid square
const GRID_BLANK = 0;
const GRID_WALL = 1;
const GRID_MAZE = 2;

const GRID_ENEMY = 10; // added constant in case of the grid contains enemy
const GRID_AGENT = 20; // // added constant in case of the grid contains agent

var BOXHEIGHT; // 3d or 2d box height
var GRID = new Array(gridsize); // can query GRID about whether squares are occupied, will in fact be initialised as a 2D array
var theagent, theenemy;

var wall_texture, agent_texture, enemy_texture, maze_texture;

// enemy and agent position on squares
var ei, ej, ai, aj;

var badsteps;
var goodsteps;




// I don't want to make enemy to move diagonal direction, I deleted the code.

// if you want to see A* in action
const PROOF = true;
var proofArray = [];


function loadResources()		// asynchronous file loads - call initScene() when all finished 
{
	var loader1 = new THREE.TextureLoader();
	var loader2 = new THREE.TextureLoader();
	var loader3 = new THREE.TextureLoader();
	var loader4 = new THREE.TextureLoader();
	
	loader1.load ( TEXTURE_WALL, function ( thetexture )  		
	{
		thetexture.minFilter  = THREE.LinearFilter;
		wall_texture = thetexture;
		if ( asynchFinished() )	initScene();		// if all file loads have returned 
	});
		
	loader2.load ( TEXTURE_AGENT, function ( thetexture )  	 
	{
		thetexture.minFilter  = THREE.LinearFilter;
		agent_texture = thetexture;
		if ( asynchFinished() )	initScene();		 
	});	
	
	loader3.load ( TEXTURE_ENEMY, function ( thetexture )  
	{
		thetexture.minFilter  = THREE.LinearFilter;
		enemy_texture = thetexture;
		if ( asynchFinished() )	initScene();		 
	});
	
	loader4.load ( TEXTURE_MAZE, function ( thetexture )  
	{
		thetexture.minFilter  = THREE.LinearFilter;
		maze_texture = thetexture;
		if ( asynchFinished() )	initScene();		 
	});
	
}


function asynchFinished()		 // all file loads returned 
{
	if ( wall_texture && agent_texture && enemy_texture && maze_texture )   return true; 
	else return false;
}	



//--- grid system -------------------------------------------------------------------------------
// my numbering is 0 to gridsize-1

function occupied(i, j) {
    // Check if indices are outside the grid boundaries
    if (i < 0 || j < 0 || i >= gridsize || j >= gridsize) {
        return true; // considered "occupied" if off the grid
    }

    // List of occupied grid values
    const occupiedValues = [GRID_WALL, GRID_MAZE, GRID_ENEMY, GRID_AGENT];

    // Check if the grid square at (i, j) is in the list of occupied values
    return occupiedValues.includes(GRID[i][j]);
}




function iswall(i, j) {
    // List of wall values
    const wallValues = [GRID_WALL, GRID_MAZE];

    // Check if the grid square at (i, j) is in the list of wall values
    return wallValues.includes(GRID[i][j]);
}



// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
// logically, coordinates are: y=0, x and z all positive (no negative)
// logically my dimensions are all positive 0 to MAXPOS
// to centre everything on origin, subtract (MAXPOS/2) from all dimensions

function translate(i, j) {
    var v = new THREE.Vector3();

    v.y = -squaresize;
    v.x = (i * squaresize) - (MAXPOS / 2);
    v.z = (j * squaresize) - (MAXPOS / 2);

    return v;
}

var animatedMeshes = []; // I made the wall to move.

function randomRotationSpeed(min, max) { // rotation speed of the wall
    return Math.random() * (max - min) + min;
}
	
function initScene()		// all file loads have returned 
{
	 var i,j, shape, thecube;
	 
	// set up GRID as 2D array
	 
	 for ( i = 0; i < gridsize ; i++ ) 
		GRID[i] = new Array(gridsize);		 


	// set up walls
   for (i = 0; i < gridsize; i++) 
        for (j = 0; j < gridsize; j++) 
            if ((i == 0) || (i == gridsize - 1) || (j == 0) || (j == gridsize - 1)) {
                GRID[i][j] = GRID_WALL;
                shape = new THREE.TorusGeometry ( squaresize/2.7, BOXHEIGHT/8, squaresize/2, 100 )	// I made the wall as animated ring		 
                thering = new THREE.Mesh(shape);
                thering.material = new THREE.MeshBasicMaterial({ map: wall_texture });
                thering.position.copy(translate(i, j));
                ABWorld.scene.add(thering);
                  animatedMeshes.push({ //It will be rotated.
                mesh: thering,
                rotationSpeedX: randomRotationSpeed(0.005, 0.02),
                rotationSpeedY: randomRotationSpeed(0.005, 0.02)
            });
            } else {
                GRID[i][j] = GRID_BLANK;
            }

		
   // set up maze 
   
for (var c = 1; c <= NOBOXES; c++) {
    i = AB.randomIntAtoB(1, gridsize - 2);
    j = AB.randomIntAtoB(1, gridsize - 2);
    GRID[i][j] = GRID_MAZE;
    
    shape = new THREE.BoxGeometry ( squaresize, BOXHEIGHT, squaresize );	
    thecube = new THREE.Mesh(shape);
    thecube.material = new THREE.MeshBasicMaterial({ map: maze_texture });
    
    thecube.position.copy(translate(i, j));
    ABWorld.scene.add(thecube);
}
	 	 
   
	// set up enemy 
	// start in random location
	
	 do
	 {
	  i = AB.randomIntAtoB(1,gridsize-2);
	  j = AB.randomIntAtoB(1,gridsize-2);
	 }
	 while ( occupied(i,j) );  	  // search for empty square 

	 ei = i;
	 ej = j;
	 
	 shape    = new THREE.SphereGeometry ( squaresize/1.7, BOXHEIGHT/1.7, squaresize );	// I made the wall as sphere like the sun.		 
	 theenemy = new THREE.Mesh( shape );
 	 theenemy.material =  new THREE.MeshBasicMaterial( { map: enemy_texture } );
	 ABWorld.scene.add(theenemy);
	 drawEnemy();		  

	 
	
	// set up agent 
	// start in random location
	
	 do
	 {
	  i = AB.randomIntAtoB(1,gridsize-2);
	  j = AB.randomIntAtoB(1,gridsize-2);
	 }
	 while ( occupied(i,j) );  	  // search for empty square 

	 ai = i;
	 aj = j;
 
	 shape    = new THREE.SphereGeometry ( squaresize/1.7, BOXHEIGHT/1.7, squaresize );	// I made the wall as sphere like the moon.		 
	 theagent = new THREE.Mesh( shape );
	 theagent.material =  new THREE.MeshBasicMaterial( { map: agent_texture } );
	 ABWorld.scene.add(theagent);
	 drawAgent();
	 
	 //Indicates the location of the agent and enemy 
	 
	 GRID[ai][aj] = GRID_AGENT;
     GRID[ei][ej] = GRID_ENEMY;


  // finally skybox 
  // setting up skybox is simple 
  // just pass it array of 6 URLs and it does the asych load 
  
  	 ABWorld.scene.background = new THREE.CubeTextureLoader().load ( SKYBOX_ARRAY, 	function() 
	 { 
		ABWorld.render(); 
	 
		AB.removeLoading();
	
		AB.runReady = true; 		// start the run loop
	 });
	 
	 animate();
 		
}

//This code makes the wall to rotate.

function animate() {
    animatedMeshes.forEach(obj => {
        obj.mesh.rotation.x += obj.rotationSpeedX;
        obj.mesh.rotation.y += obj.rotationSpeedY;
    });
    
    ABWorld.render();
    requestAnimationFrame(animate);
}


// --- draw moving objects -----------------------------------

function drawEnemy() // given ei, ej, draw it
{
    theenemy.position.copy(translate(ei, ej)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates

    ABWorld.lookat.copy(theenemy.position); // if camera moving, look back at where the enemy is
}


function drawAgent() // given ai, aj, draw it
{
    theagent.position.copy(translate(ai, aj)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates

    ABWorld.follow.copy(theagent.position); // follow vector = agent position (for camera following agent)
}

// Add the A* Algorithm

//To encapsulize start and target point.
class encapsulizations {
    constructor(i, j) {
        this.i = i;
        this.j = j;
    }
}

var start = new encapsulizations();
var target = new encapsulizations();

const MAX_OPENSET = gridsize * gridsize;

function PathNode(i, j) {
    // Location
    this.i = i;
    this.j = j;

    // Initialize A* values to zero
    this.f = 0;
    this.g = 0;
    this.h = 0;

    // Array to store neighboring nodes
    this.neighbors = [];

    // Reference to the previous node in the path
    this.parent = undefined;
    this.closed = false; // Flag to determine if the node has been processed

    // next movable locations
    this.addNeighbors = function () {
        var i = this.i;
        var j = this.j;


        if (i < gridsize - 1) this.neighbors.push(aStarGrid[i + 1][j]);
        if (i > 0) this.neighbors.push(aStarGrid[i - 1][j]);
        if (j < gridsize - 1) this.neighbors.push(aStarGrid[i][j + 1]);
        if (j > 0) this.neighbors.push(aStarGrid[i][j - 1]);

    };
}

// Create Global A* Array
var aStarGrid = [];

// initialisation of A* Grid array.
function aStarInitialisation() {
    var i, j;
    // aStarGrid array initialisation
    for (i = 0; i < gridsize; i++) {
        aStarGrid[i] = [];
        for (j = 0; j < gridsize; j++) {
            aStarGrid[i][j] = new PathNode(i, j);
            console.log("A* Initialisation: " + i + ", " + j);
        }
    }

    for (i = 0; i < gridsize; i++) {
        for (j = 0; j < gridsize; j++) {
            aStarGrid[i][j].addNeighbors();
        }
    }
    console.info("A* Initialisation is completed!");
}

// A* function that returns the next position in the as a coordinate (i,j)
function aStar(grid, start, target) {

    // Initialise of both list of nodes
    var openSet = [];
    var child;
    var completed = false;
    var targetPoint = aStarGrid[target.i][target.j]; // To initialize the target location
    var startPoint = aStarGrid[start.i][start.j]; // Enemy's startpoint
    var currentPoint;
    var nextMove = new encapsulizations(start.i, start.j);
    var stateCount = 0; //adding

    // start point will be pushed into openset
    openSet.push(startPoint); // 

    // Initialise the previous results
    console.info("Initialise the previous results");
    for (i = 0; i < gridsize; i++) {
        for (j = 0; j < gridsize; j++) {
            currentPoint = aStarGrid[i][j];
            currentPoint.f = currentPoint.h =currentPoint.g = 0;
            currentPoint.closed = false;
            currentPoint.parent = null;
        }
    }

    // loop all of the a* openset.
    while ((openSet.length > 0) && !completed) {

        if (openSet.length > MAX_OPENSET) {
            // Prevent against memory overload during OpenSet debugging.
            console.error("Error occured!");
            completed = true;
            return nextMove;
        }

        // Sort the list by f value.
        openSet.sort((a, b) => (a.f > b.f) ? 1 : -1); 
        currentPoint = openSet.shift(); 
        stateCount++; // Increment the state count //adding
        currentPoint.closed = true; // To check the closed status

        console.info("Currently at position: (" + currentPoint.i + ", " + currentPoint.j + "). OpenSet count: " + openSet.length);

        if (currentPoint == targetPoint) {

            // Proof array
            if (PROOF){
                proofArray.push(targetPoint);
            }

            // If the target is founded, begin retracing steps.
            while (currentPoint.parent != startPoint) {

                if (PROOF){
                    proofArray.push(currentPoint);
                }
                
                // retracing 
                currentPoint = currentPoint.parent;
            }
            completed = true;

            // Proof array
            if (PROOF){
                proofArray.push(startPoint);
            }


        } else {
            // build openSet
            for (n = 0; n < currentPoint.neighbors.length; n++) {
                // for each of the neighbours, calculate the children
                child = currentPoint.neighbors[n];

                // To check closedSet : occupied or not?
                if (child.closed) {
                } else if (iswall(child.i, child.j)) {
                    child.closed = true;
                } else {
                    // calculate values to add to openSet
                    if ((child.i == currentPoint.i) || (child.j == currentPoint.j))
                        child.g = currentPoint.g + 10; 
                    else
                        child.g = currentPoint.g + 14; 

                    child.h = aStarHeuristics(child, targetPoint);
                    child.f = child.g + child.h;
                    child.parent = currentPoint;
                    if (openSet.indexOf(child) < 0) {
                        // if not in openSet, push the childe node.
                        openSet.push(child);
                    }

                }
            }
        }
    }

    nextMove.i = currentPoint.i;
    nextMove.j = currentPoint.j;
    
    console.log("Total states explored: " + stateCount); //adding
    return nextMove;
}


function aStarHeuristics(start, end) {
    return Math.sqrt(Math.pow(start.i - end.i, 2) + Math.pow(start.j - end.j, 2));
}


// --- take actions -----------------------------------

function moveLogicalEnemy() {
    // move towards agent
    // put some randomness in so it won't get stuck with barriers

    var i, j;

    start.i = ei;
    start.j = ej;
    target.i = ai;
    target.j = aj;

    // call the a* algorithm 
    bestPath = aStar(GRID, start, target);

    // Only Move if the space is empty
    if (!occupied(bestPath.i, bestPath.j)) {
        GRID[ei][ej] = GRID_BLANK; // Store Agents Position
        // move enemy to new position
        ei = bestPath.i;
        ej = bestPath.j;
        GRID[ei][ej] = GRID_ENEMY;
    }
    console.info("enemy moves to : " + ei + "," + ej)

}


function moveLogicalAgent(a) // this is called by the infrastructure that gets action a from the Mind
{
    var i = ai;
    var j = aj;

    // depending on the agent move, spin the sphere
    switch (a) {
        case ACTION_LEFT:
            i--;
            theagent.rotation.y = 0;
            break;
        case ACTION_RIGHT:
            i++;
            theagent.rotation.y = -1;
            break;
        case ACTION_UP:
            j++;
            theagent.rotation.y = -2;
            break;
        case ACTION_DOWN:
            j--;
            theagent.rotation.y = 2;
            break;
        default:
            break;
    }

    if (!occupied(i, j)) {
        GRID[ai][aj] = GRID_BLANK; // Clear Agents Previous Position
        ai = i;
        aj = j;
        GRID[ai][aj] = GRID_AGENT; // Store Agents Position

    }
}


var OURKEYS = [37, 38, 39, 40];

function ourKeys(event) {
    return (OURKEYS.includes(event.keyCode));
}


function keyHandler(event) {
    if (!AB.runReady) return true; // not ready yet

    // if not one of our special keys, send it to default key handling:

    if (!ourKeys(event)) return true;

    // else handle key and prevent default handling:

    if (event.keyCode == 37) moveLogicalAgent(ACTION_LEFT);
    if (event.keyCode == 38) moveLogicalAgent(ACTION_DOWN);
    if (event.keyCode == 39) moveLogicalAgent(ACTION_RIGHT);
    if (event.keyCode == 40) moveLogicalAgent(ACTION_UP);

    // when the World is embedded in an iframe in a page, we want arrow key events handled by World and not passed up to parent

    event.stopPropagation();
    event.preventDefault();
    return false;
}

// score
function badstep() 
{
    if ((Math.abs(ei - ai) < 2) && (Math.abs(ej - aj) < 2)) return true;
    else return false;
}



function agentBlocked() 
{
    return (occupied(ai - 1, aj) &&
        occupied(ai + 1, aj) &&
        occupied(ai, aj + 1) &&
        occupied(ai, aj - 1));
}


function updateStatusBefore(a)
{
    var x = AB.world.getState();
    AB.msg("Step: " + AB.step + "<br>x = (" + x.toString() + ")");

}


function updateStatusAfter() // agent and enemy have moved, can calculate score
{
    // new state after agent and enemy moved

    var y = AB.world.getState();
    var score = (goodsteps / AB.step) * 100;

    AB.msg(" Bad steps: " + badsteps +
        "   Good steps: " + goodsteps +
        "   Score: " + score.toFixed(2) + "% ", 2);
}

function playSound() // is the enemy within one square of the agent
{

    // load click sound & based on distance, speed up and change volume
    var sound = new Audio('/uploads/brendanb/click.ogg');
    var soundRate = 1 + (((Math.abs(ei - ai)) + Math.abs((ej - aj))) / (gridsize));
    console.log("PlaybackRate: " + soundRate);

    sound.playbackRate = soundRate;
    sound.volume = 1 / soundRate;
    if (Math.abs(ei - ai) + Math.abs(ej - aj) < 2) {
        sound.playbackRate = soundRate * 2;
        sound.play();
    }
    sound.play();

}

AB.world.newRun = function () {
    AB.loadingScreen();
    AB.runReady = false;

    badsteps = 0;
    goodsteps = 0;
    aStarInitialisation(); //initialization of A* Algorithm


    if (show3d) {
        BOXHEIGHT = squaresize;
        ABWorld.init3d(startRadiusConst, maxRadiusConst, SKYCOLOR);
    } else {
        BOXHEIGHT = 1;
        ABWorld.init2d(startRadiusConst, maxRadiusConst, SKYCOLOR);
    }


    loadResources(); // aynch file loads
    // calls initScene() when it returns

    // Initiailise A* Array
    document.onkeydown = keyHandler;

};



AB.world.getState = function () {
    var x = [ai, aj, ei, ej];
    return (x);
};

var enemyAngle = 0;
var cameraAngle;

AB.world.takeAction = function (a) {
    updateStatusBefore(a); // show status line before moves

    console.info("-: Agent: " + ai + "," + aj + " Enemy: " + ei + "," + ej);

    moveLogicalAgent(a);

    if ((AB.step % 2) === 0) { // slow the enemy down to every nth step
        playSound();
        moveLogicalEnemy();
    }

    console.info("+: Agent: " + ai + "," + aj + " Enemy: " + ei + "," + ej);

    if (badstep()) badsteps++;
    else goodsteps++;

    // Spin the enemy
    enemyAngle += 0.2;
    theenemy.rotation.y = enemyAngle;

    drawAgent();
    drawEnemy();
    updateStatusAfter(); // show status line after moves

    if (agentBlocked() ) // if agent blocked in, run over
    {
        AB.abortRun = true;
        goodsteps = 0;
        musicPause();
	    soundAlarm();
    }

    // // Proof array
    if (PROOF){
        drawProof();
    }

};

AB.world.endRun = function () {
    musicPause();
    if (AB.abortRun) AB.msg(" <br> <font color=red> <B> Agent trapped.</B> </font>   ", 3);
    else AB.msg(" <br> <font color=green> <B> Run over. </B> </font>   ", 3);
};

AB.world.getScore = function () {
    // only called at end - do not use AB.step because it may have just incremented past AB.maxSteps

    var s = (goodsteps / AB.maxSteps) * 100; // float like 93.4372778
    var x = Math.round(s * 100); // 9344
    return (x / 100); // 93.44
};



// Draw the path on the map based on a* algorithm
var finalLine = [];
var existingLine;
function drawProof() {

    var drawstartpoint; // Represents the start point of a line segment
    var drawendpoint; // Represents the end point of a line segment
    var line;   // The drawn line

    var oldMaterial = new THREE.LineBasicMaterial({color: 0x222222, linewidth: 1 });

     // newMaterial to be used for drawing the line
    var newMaterial = new THREE.LineBasicMaterial({color: 'yellow', linewidth: 2 });
    
    // newGeometry to hold the vertices for the line
    var newGeometry = new THREE.BufferGeometry();

    // Array to store the translated vertex positions for the line
    var vertices = [];

    // Loop through the proofArray to extract and draw lines between consecutive points
    for (var i = proofArray.length; i > 1; i--) {
        drawstartpoint = proofArray.shift();

        if (drawstartpoint.parent) {
            drawstartpoint = drawstartpoint.parent;
            var translatedPoint = translate(drawstartpoint.i, drawstartpoint.j);
            vertices.push(translatedPoint.x, translatedPoint.y, translatedPoint.z);
            // console.log("Line between " + drawstartpoint.i + "," + drawstartpoint.j + " and ");
        } else {
            var translatedPoint = translate(drawstartpoint.i, drawstartpoint.j);
            vertices.push(translatedPoint.x, translatedPoint.y, translatedPoint.z);
            // console.log("Line between " + drawstartpoint.i + "," + drawstartpoint.j + " and ");
        }
    }

    // Assign the vertices array to the geometry
    newGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    line = new THREE.Line(newGeometry, newMaterial);
    
    // Store the drawn line for future references (or operations)
    finalLine.push(line);
    ABWorld.scene.add(line);
}


// --- music and sound effects ----------------------------------------

var backmusic = AB.backgroundMusic ( MUSIC_BACK );

function musicPlay()   { backmusic.play();  }
function musicPause()  { backmusic.pause(); }

											 
function soundAlarm()
{
	var alarm = new Audio ( SOUND_ALARM );
	alarm.play();							// play once, no loop 
}