// =============================================================================================
// 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> x = (" + x.toString() + ") ";
$("#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 = " y = (" + y.toString() + ") <BR> ";
$("#user_span4").html( status );
var score = self.getScore();
var status = " Bad steps: " + badsteps +
" Good steps: " + goodsteps +
" 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( " <font color=red> <B> You have been caught by the spooky skeleton! </B> </font> " );
else
$("#user_span6").html( " <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 );
}