Code viewer for World: Spooky Skeleton Survival

See raw JS.




// =============================================================================================
// Spooky Skeleton Survival
// Cillian Rice, 2016.
// Student No. 14446238
// CASE 3
//
// Built using code from Mark Humphrys Complex World
// Enemy actively chases agent when it has a vertical or horizontal line of sight and searches
// randomly for it when it doesn't. See line 146 for further LOS explanation.
// Survive until the end of the game by hiding from the skeleton.
// =============================================================================================


// =============================================================================================
// 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.
// =============================================================================================






// World must define these:
 
const	 	CLOCKTICK 	= 100;					// speed of run - move things every n milliseconds
const		MAXSTEPS 	= 1000;					// length of a run before final score
 





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

const gridsize = 50;						// number of squares along side of world	   

const NOBOXES =  Math.trunc ( (gridsize * gridsize) / 10 );
		// density of maze - number of internal boxes
		// (bug) use trunc or can get a non-integer 

const squaresize = 100;					// size of square in pixels
const MAXPOS = gridsize * squaresize;		// length of one side in pixels 
	
const SKYCOLOR 	= 0xddffdd;				// a number, not a string 
const BLANKCOLOR 	= SKYCOLOR ;			// make objects this color until texture arrives (from asynchronous file read)




const show3d = true;						// Switch between 3d and 2d view (both using Three.js) 
 
const startRadiusConst	 	= MAXPOS * 0.8 ;		// distance from centre to start the camera at
const skyboxConst			= MAXPOS * 3 ;		// where to put skybox 
const maxRadiusConst 		= MAXPOS * 10  ;		// maximum distance from camera we will render things  





//--- 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;
 
 




 
// --- some useful random functions  -------------------------------------------


function randomfloatAtoB ( A, B )			 
{
 return ( A + ( Math.random() * (B-A) ) );
}

function randomintAtoB ( A, B )			 
{
 return  ( Math.round ( randomfloatAtoB ( A, B ) ) );
}
  
function randomBoolean()			 
{
 if ( Math.random() < 0.5 ) { return false; }
 else { return true; }
}







//---- start of World class -------------------------------------------------------
 
function World() { 


// most of World can be private 
// regular "var" syntax means private variables:


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 WALLS 	= new Array ( 4 * gridsize );		// need to keep handles to wall and maze objects so can find them later to paint them 
var MAZE 	= new Array ( NOBOXES );
var theagent, theenemy;
  
var agentRotation = 0;
var enemyRotation = 0;		  // with 3D models, current rotation away from default orientation

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

var badsteps;
var goodsteps;
var  step;

var self = this;						// needed for private fn to call public fn - see below  

var LOS = 0; //Unobstructed enemy Line of Sight to agent
/* LOS is reset every time the player is spotted and decremented while they
are not in view. When its value is zero the enemyAgent reverts to searching. When its
value is greater than zero it knows where the player is and follows it. This allows
the enemy to chase the player for a period of time after spotting it. If sight is lost
for long enough the enemy will go back to searching randomly.*/

// regular "function" syntax means private functions:


function initGrid()
{
 for (var i = 0; i < gridsize ; i++) 
 {
  GRID[i] = new Array(gridsize);		// each element is an array 

  for (var j = 0; j < gridsize ; j++) 
  {
   GRID[i][j] = GRID_BLANK ;
  }
 }
}

function occupied ( i, j )		// is this square occupied
{
 if ( ( ei == i ) && ( ej == j ) ) return true;		// variable objects 
 if ( ( ai == i ) && ( aj == j ) ) return true;

 if ( GRID[i][j] == GRID_WALL ) return true;		// fixed objects	 
 if ( GRID[i][j] == GRID_MAZE ) return true;		 
	 
 return false;
}

 
// 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 ( x ) 
{
 return ( x - ( MAXPOS/2 ) );
}





//--- skybox ----------------------------------------------------------------------------------------------


function initSkybox() 
{

// x,y,z positive and negative faces have to be in certain order in the array 
 
// spooky skybox, credit:
// https://marketplace.secondlife.com/p/WM-Spooky-Forest-Mesh-Skybox-Unfurnished/6958081?id=6958081&slug=WM-Spooky-Forest-Mesh-Skybox-Unfurnished

  var materialArray = [
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/cillianrice1/spookyforest2.jpg" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/cillianrice1/spookyforest2.jpg" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/cillianrice1/spookyforest4.jpg" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/cillianrice1/spookyforest5.jpg" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/cillianrice1/spookyforest1.jpg" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/cillianrice1/spookyforest1.jpg" ), side: THREE.BackSide } ) )
 	];

  var skyGeometry = new THREE.CubeGeometry ( skyboxConst, skyboxConst, skyboxConst );	
  var skyMaterial = new THREE.MeshFaceMaterial ( materialArray );
  var theskybox = new THREE.Mesh ( skyGeometry, skyMaterial );
  threeworld.scene.add( theskybox );						// We are inside a giant cube
}
  
// loader return can call private function


function loadTextures()
{
    var manager = new THREE.LoadingManager();		
	var loader = new THREE.OBJLoader( manager );
			
    loader.load( "/uploads/starter/skelet.obj", buildenemy );



    //load simple OBJ
    //loader.load( "/uploads/starter/male02.obj", buildagent );


    // load OBJ plus MTL (plus TGA files) 
	THREE.Loader.Handlers.add( /.tga$/i, new THREE.TGALoader() );
	var m = new THREE.MTLLoader();
	m.setTexturePath ( "/uploads/starter/" );
	m.setPath        ( "/uploads/starter/" );
	m.load( "Peter_Parker.mtl", function( materials ) {
 
		materials.preload();
		var o = new THREE.OBJLoader();
		o.setMaterials ( materials );
		o.setPath ( "/uploads/starter/" );
		o.load( "Peter_Parker.obj", function ( object ) {
			addparker ( object );
		} );
	} );

 var loader1 = new THREE.TextureLoader();
 loader1.load ( '/uploads/cillianrice1/grey.jpg',		function ( thetexture ) {			 
		thetexture.minFilter = THREE.LinearFilter;
		paintWalls ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
	} ); 

 var loader2 = new THREE.TextureLoader();
 loader2.load ( '/uploads/cillianrice1/somename.jpg',		function ( thetexture ) {			 
		thetexture.minFilter = THREE.LinearFilter;
		paintMaze ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
 	} ); 
}

function buildenemy ( object ) 
{ 
	object.scale.multiplyScalar ( 3 );    	  // make 3d object n times bigger 
	object.traverse( paintEnemy );
	theenemy = object;
	threeworld.scene.add( theenemy ); 
}

function paintEnemy ( child ) 
{
	if ( child instanceof THREE.Mesh ) 
	{
      	child.material.map = THREE.ImageUtils.loadTexture( "/uploads/starter/ghost.3.png" );
	}
}
 
function addparker ( object )
{
	object.scale.multiplyScalar ( 70 );    	   
	theagent = object;
	threeworld.scene.add( theagent ); 
}

// --- add fixed objects ---------------------------------------- 
   
 
function initLogicalWalls()		// set up logical walls in data structure, whether doing graphical run or not	
{
 for (var i = 0; i < gridsize ; i++) 
  for (var j = 0; j < gridsize ; j++) 
   if ( ( i==0 ) || ( i==gridsize-1 ) || ( j==0 ) || ( j==gridsize-1 ) )
   {
    	GRID[i][j] = GRID_WALL ;		 
   }
}


function initThreeWalls()		// graphical run only, set up blank boxes, painted later 	
{
 var t = 0;
 for (var i = 0; i < gridsize ; i++) 
  for (var j = 0; j < gridsize ; j++) 
   if ( GRID[i][j] == GRID_WALL )
   {
 	 var shape    = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );			 
 	 var thecube  = new THREE.Mesh( shape );
	 thecube.material.color.setHex( BLANKCOLOR  );			  
 
    	 thecube.position.x = translate ( i * squaresize );   		// translate my simple (i,j) block-numbering coordinates to three.js (x,y,z) coordinates 
    	 thecube.position.z = translate ( j * squaresize );   	
    	 thecube.position.y =  0;	
 
 	 threeworld.scene.add(thecube);
	 WALLS[t] = thecube;				// save it for later
	 t++; 
   }
}


function paintWalls ( material )		 
{
 for ( var i = 0; i < WALLS.length; i++ )
 { 
   if ( WALLS[i] )  WALLS[i].material = material;
 }
}





function initLogicalMaze()		 
{
 for ( var c=1 ; c <= NOBOXES ; c++ )
 {
  	var i = randomintAtoB(1,gridsize-2);	// inner squares are 1 to gridsize-2
  	var j = randomintAtoB(1,gridsize-2);
    	GRID[i][j] = GRID_MAZE ;		 
 }
}


function initThreeMaze()		  	
{
 var t = 0;
 for (var i = 0; i < gridsize ; i++) 
  for (var j = 0; j < gridsize ; j++) 
   if ( GRID[i][j] == GRID_MAZE )
   {
   	var shape    = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );			 
  	var thecube  = new THREE.Mesh( shape );
	thecube.material.color.setHex( BLANKCOLOR  );			  

  	thecube.position.x = translate ( i * squaresize );   	
  	thecube.position.z = translate ( j * squaresize );   	
  	thecube.position.y =  0;	
 
 	threeworld.scene.add(thecube);
	MAZE[t] = thecube;		// save it for later
	t++; 
   }
}


function paintMaze ( material )		 
{
 for ( var i = 0; i < MAZE.length; i++ )
 { 
   if ( MAZE[i] )  MAZE[i].material = material;
 }
}







// --- enemy functions -----------------------------------


function drawEnemy()	// given ei, ej, draw it 
{
  var x = translate ( ei * squaresize );   	
  var z = translate ( ej * squaresize );   	
  var y =  0;	

 theenemy.position.x = x;
 theenemy.position.y = y;
 theenemy.position.z = z;

 threeworld.lookat.copy ( theenemy.position );		// if camera moving, look back at where the enemy is  
 threeworld.lookat.y = ( squaresize * 1.5 );     // point camera higher up
}


function initLogicalEnemy()
{
// start in random location:
 var i, j;
 do
 {
  i = randomintAtoB(1,gridsize-2);
  j = randomintAtoB(1,gridsize-2);
 }
 while ( occupied(i,j) );  	  // search for empty square 

 ei = i;
 ej = j;
}

function moveSearchingEnemy()
{ 
 // small random move
 var oldi = ei;
 var oldj = ej;
 var i = randomintAtoB ( ei-1, ei+1 );
 var j = randomintAtoB ( ej-1, ej+1 );
 
 if(i == (oldi+1))
 {
     rotateEnemyTowards ( 3 * (Math.PI / 2) );
 }
 
 if(i == (oldi-1))
 {
     rotateEnemyTowards ( 1 * (Math.PI / 2) );
 }
 
 if(j == (oldj+1))
 {
     rotateEnemyTowards ( 0 * (Math.PI / 2) );
 }
 
 if(j == (oldj-1))
 {
     rotateEnemyTowards ( 2 * (Math.PI / 2) );
 }
 
 if ( ! occupied(i,j) )  	// if no obstacle then move, else just miss a turn
 {
  ei = i;
  ej = j;
 }
}

function moveLogicalEnemy()
{ 
// move towards agent 
// put some randomness in so it won't get stuck with barriers 
    
 var i, j;
    if ( ei < ai )
    {
        i = randomintAtoB(ei, ei+1);
        rotateEnemyTowards ( 3 * (Math.PI / 2) );
    }
    if ( ei == ai ) i = ei; 
    if ( ei > ai )
    {
        i = randomintAtoB(ei-1, ei); 
        rotateEnemyTowards ( 1 * (Math.PI / 2) );
    }

    if ( ej < aj )
    {
        j = randomintAtoB(ej, ej+1); 
        rotateEnemyTowards ( 0 * (Math.PI / 2) );
    }
        
    if ( ej == aj ) j = ej; 
    if ( ej > aj )
    {
        j = randomintAtoB(ej-1, ej); 
        rotateEnemyTowards ( 2 * (Math.PI / 2) );
    }

    if ( ! occupied(i,j) )  	// if no obstacle then move, else just miss a turn
    {
        ei = i;
        ej = j;
    }
}

function rotateEnemyTowards ( newRotation )		
{
 if ( enemyRotation == newRotation ) return;
 // else 
 var x = ( enemyRotation + newRotation ) / 2; 
 theenemy.rotation.set ( 0, x, 0 );
 enemyRotation = x;	
}

// --- agent functions -----------------------------------


function drawAgent()	// given ai, aj, draw it 
{
  var x = translate ( ai * squaresize );   	
  var z = translate ( aj * squaresize );   	
  var y =  0;	

 theagent.position.x = x;
 theagent.position.y = y;
 theagent.position.z = z;

 threeworld.follow.copy ( theagent.position );		// follow vector = agent position (for camera following agent)
 threeworld.follow.y = ( squaresize * 1.5 );     // put camera higher up
}


function initLogicalAgent()
{
// start in random location:
 var i, j;
 do
 {
  i = randomintAtoB(1,gridsize-2);
  j = randomintAtoB(1,gridsize-2);
 }
 while ( occupied(i,j) );  	  // search for empty square 

 ai = i;
 aj = j;
}

function initThreeAgent()
{
 var shape    = new THREE.BoxGeometry( squaresize, BOXHEIGHT, squaresize );			 
 theagent = new THREE.Mesh( shape );
 theagent.material.color.setHex( BLANKCOLOR );	
 drawAgent(); 		  
}


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

      if ( a == ACTION_LEFT ) 	i--;
 else if ( a == ACTION_RIGHT ) 	i++;
 else if ( a == ACTION_UP ) 		j++;
 else if ( a == ACTION_DOWN ) 	j--;

 if ( ! occupied(i,j) ) 
 {
  ai = i;
  aj = j;
 }
}



function keyHandler(e)		
// user control 
// Note that this.takeAction(a) is constantly running at same time, redrawing the screen.
{
    if (e.keyCode == 37)  moveLogicalAgent ( ACTION_LEFT 	);
    if (e.keyCode == 38)  moveLogicalAgent ( ACTION_DOWN  	);
    if (e.keyCode == 39)  moveLogicalAgent ( ACTION_RIGHT 	);
    if (e.keyCode == 40)  moveLogicalAgent ( ACTION_UP	);
}

function rotateAgentTowards ( newRotation )		
{
 if ( agentRotation == newRotation ) return;
 // else 
 var x = ( agentRotation + newRotation ) / 2; 
 theagent.rotation.set ( 0, x, 0 );
 agentRotation = x;	
}



// --- score: -----------------------------------


function badstep()			// is the enemy within one square of the agent
{
 if ( ( Math.abs(ei - ai) < 2 ) && ( Math.abs(ej - aj) < 2 ) ) return true;
 else return false;
}


function agentBlocked()			// agent is blocked on all sides, run over
{
 return ( 	occupied (ai-1,aj) 		&& 
		occupied (ai+1,aj)		&&
		occupied (  ai,aj+1)		&&
		occupied (  ai,aj-1) 	);		
} 


function updateStatusBefore(a)
// this is called before anyone has moved on this step, agent has just proposed an action
// update status to show old state and proposed move 
{
 var x = self.getState();
 var status = " Step: <b> " + step + " </b> &nbsp; x = (" + x.toString() + ") &nbsp;"; 

 $("#user_span3").html( status );
}


function   updateStatusAfter()		// agent and enemy have moved, can calculate score
{
 // new state after both have moved
 var y = self.getState();
 var status = " &nbsp; y = (" + y.toString() + ") <BR> "; 
 $("#user_span4").html( status );

 var score = self.getScore();

 var status = "   Bad steps: " + badsteps + 
		" &nbsp; Good steps: " + goodsteps + 
		" &nbsp; Score: " + score.toFixed(2) + "% " + "<b> The skeleton will chase you if he sees you! You can lose him by staying out of sight for a while. Arrow keys to move!</b>"; 

 $("#user_span5").html( status );
}

function los()
{
    var foundAgent = false;
    
    //Check right for Line of Sight
    for(var i = (ei+1); i < gridsize-1 ; i++)
    {
        if(occupied(i,ej))
        {
            if((ai==i) && (aj==ej))
            {
                LOS = 100;
                foundAgent = true;
                break;
            }
            else
            {
                LOS--;
                foundAgent = false;
                break;
            }
        }
    }
    
    //Check left for LOS
    if(foundAgent == false)
    {
        for(var i = ei-1; i>0; i--)
        {
            if(occupied(i,ej))
            {
                if((ai==i) && (aj==ej))
                {
                    LOS = 100;
                    foundAgent = true;
                    break;
                }
                else
                {
                    LOS--;
                    foundAgent = false;
                    break;
                }
            }
        }
    }
    
    //Check up for LOS
    if(foundAgent == false)
    {
        for(var i = ej+1; i<gridsize; i++)
        {
            if(occupied(ei,i))
            {
                if((aj==i) && (ai==ei))
                {
                    LOS = 100;
                    foundAgent = true;
                    break;
                }
                else
                {
                    LOS--;
                    foundAgent = false;
                    break;
                }
            }
        }
    }
    
    //Check down for LOS
    if(foundAgent == false)
    {
        for(var i = ej-1; i>0; i--)
        {
            if(occupied(ei,i))
            {
                if((aj==i) && (ai==ei))
                {
                    LOS = 100;
                    foundAgent = true;
                    break;
                }
                else
                {
                    LOS--;
                    foundAgent = false;
                    break;
                }
            }
        }
    }
    
    return LOS;
}




//--- public functions / interface / API ----------------------------------------------------------


	this.endCondition;			// If set to true, run will end. 



this.newRun = function() 
{

// (subtle bug) must reset variables like these inside newRun (in case do multiple runs)

  this.endCondition = false;
  badsteps = 0;	
  goodsteps = 0;
	step = 0;


 // for all runs:

 	initGrid();
	initLogicalWalls(); 
	initLogicalMaze();
	initLogicalAgent();
	initLogicalEnemy();

 // for graphical runs only:

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

	initSkybox();
 	initMusic();

	// Set up objects first:

	initThreeWalls(); 
	initThreeMaze();
	initThreeAgent();

	// Then paint them with textures - asynchronous load of textures from files. 
	// The texture file loads return at some unknown future time in some unknown order.
	// Because of the unknown order, it is probably best to make objects first and later paint them, rather than have the objects made when the file reads return.
	// It is safe to paint objects in random order, but might not be safe to create objects in random order. 

	loadTextures();	

	document.onkeydown = keyHandler;	 
  }

};




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



this.takeAction = function ( a )
{
  step++;

  if ( true  )
   updateStatusBefore(a);			// show status line before moves 

  moveLogicalAgent(a);

  if ( los() > 0 )		// check if enemy has line of sight
    moveLogicalEnemy(); // chase enemy if it has line of sight
  else
    moveSearchingEnemy(); //Search for it if it doesnt

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

  if ( true  )
  {
   drawAgent();
   drawEnemy();
   updateStatusAfter();			// show status line after moves  
  }

  if( ((ei == ai+1) && (ej == aj)) || ((ei == ai-1) && (ej == aj)) || 
  ((ei == ai) && (ej == aj-1)) || ((ei == ai) && (ej == aj+1) || 
  ((ei == ai+1) && (ej == aj+1)) || ((ei == ai+1) && (ej == aj-1)) || 
  ((ei == ai-1) && (ej == aj+1)) || ((ei == ai-1) && (ej == aj-1))) )
  {
	  this.endCondition = true;
  }

  if ( agentBlocked() )			// if agent blocked in, the skeleton kills you. 
  {
	this.endCondition = true;
	goodsteps = 0;			// you score zero as far as database is concerned 			 
  	if ( true  )
  	{
	 musicPause();
	 soundAlarm();
	}
  }

};



this.endRun = function()
{
 if ( true  )
 {
  musicPause(); 
  if ( this.endCondition )
    $("#user_span6").html( " &nbsp; <font color=red> <B> You have been caught by the spooky skeleton! </B> </font>   "  );
  else
    $("#user_span6").html( " &nbsp; <font color=red> <B> You survived! </B> </font>   "  );
 }
};


this.getScore = function()
{
 return ( ( goodsteps / step ) * 100 );
};


}

//---- end of World class -------------------------------------------------------








// --- music and sound effects ----------------------------------------
// credits:
// http://www.dl-sounds.com/royalty-free/defense-line/
// http://soundbible.com/1542-Air-Horn.html 



function initMusic()
{
	// put music element in one of the spans
  	var x = "<audio  id=theaudio  src=http://student.computing.dcu.ie/~ricec8/Spooky%20Scary%20Skeletons%20(Trap%20Remix).mp3   autoplay loop> </audio>" ;
  	$("#user_span1").html( x );
} 
 

function musicPlay()  
{
	// jQuery does not seem to parse pause() etc. so find the element the old way:
 	document.getElementById('theaudio').play();
}


function musicPause() 
{
 	document.getElementById('theaudio').pause();
}


function soundAlarm()
{
 	var x = "<audio    src=/uploads/starter/air.horn.mp3   autoplay  > </audio>";
  	$("#user_span2").html( x );
}