Code viewer for World: Football World
// ====================================================================================================================
//
// Football World
// 
// ====================================================================================================================

// ====================================================================================================================
//
// Scoring: Player is given a point each time they score a goal.
// The arrow keys are used to move the player.
// The PgUp key taps the ball and PgDn key kicks the ball.
// The player can hit the ball straight or at an angle and can bounce it off the walls.
// Each time the ball is kicked into the goal; the ball, the player and the maze are reset.
//
// ====================================================================================================================


const CLOCKTICK = 100;			// speed of run in milliseconds
const MAXSTEPS 	= 1000;			// length of each run before final score



// --- GLOBAL CONSTANTS: ----------------------------------------------------------------------------------------------


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

const NOBOXES    =  8;                              // density of maze - number of internal boxes
const squaresize = 100;					            // size of square in pixels
const MAXPOS     = gridsize * squaresize;	        // length of one side in pixels

const ballrad    = Math.trunc( squaresize / 2 );    // size of ball radius in pixels
const postrad    = Math.trunc( squaresize / 3 );    // size of goal post radius in pixels
const postheight = 800;                             // height of goal post in pixels

const TAP   = 2;        // Number of squares ball moves when tapped (PgUp key)
const KICK  = 8;        // Number of squares ball moves when kicked (PgDn key)

const SKYCOLOR   = 0x885D5D;		// a number, not a string
const BLANKCOLOR = SKYCOLOR;        // default colour until texture applied


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'S ACTIONS -------------------------------------------------------------------------------------------------


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

const ACTION_LEFT 		= 0;
const ACTION_RIGHT 		= 1;
const ACTION_UP 		= 2;
const ACTION_DOWN 		= 3;
const ACTION_STAYSTILL 	= 4;
const ACTION_TAP     	= 5;
const ACTION_KICK   	= 6;

// contents of a grid square

const GRID_BLANK 	= 0;
const GRID_WALL 	= 1;
const GRID_MAZE 	= 2;
const GOAL_POST     = 3;
const GOAL_LINE     = 4;




// --- FUNCTIONS ------------------------------------------------------------------------------------------------------


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

function randomintAtoB( A, B )
{
  return ( Math.round ( randomfloatAtoB( A, B ) ) );
}



// --- WORLD CLASS ----------------------------------------------------------------------------------------------------

function World() {

var BOXHEIGHT;      // 3d or 2d box height

var GRID 	 = new Array( gridsize );		// initialises fixed objects
var WALLS 	 = new Array( 4 * gridsize );
var MAZE 	 = new Array( NOBOXES );
var GROUND   = new Array( 1 );
var AUDIENCE = new Array( 4 );

var theplayer, theball;         // initialise variable objects
var bi, bj, pi, pj;             // ball and player position on squares

var goals;
var step;

var sideway = 0;        // no of squares that ball moves sideways : + is right; - is left
var tofrom  = 0;        // no of squares that ball moves forward/back : + is nearer; - is away
var pos     = 0;        // Position of player relative to ball when adjacent to it
var rolling = false;    // State of the ball : FALSE = stationary; TRUE = rolling after being kicked
var dist    = 0;        // number of squares ball has to move

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


function initGrid()         // initialise the grid
{
  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 )		// checks if a square is occupied
{
  if ( ( bi == i ) && ( bj == j ) ) return true;	// variable objects
  if ( ( pi == i ) && ( pj == j ) ) return true;

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

  return false;
}

function translate ( x )        // centre objects on origin
{
  return ( x - ( MAXPOS / 2 ) );
}



// --- SKYBOX ---------------------------------------------------------------------------------------------------------


function initSkybox() 
{

  // skybox, credit:
  // https://www.dreamstime.com/stock-photo-green-grass-blue-clear-sky-spring-nature-panorama-theme-banner-super-high-resolution-premium-quality-image45126910

  var materialArray = [
    ( new THREE.MeshBasicMaterial( { map: THREE.ImageUtils.loadTexture( "/uploads/macdonc5/right.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial( { map: THREE.ImageUtils.loadTexture( "/uploads/macdonc5/left.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial( { map: THREE.ImageUtils.loadTexture( "/uploads/macdonc5/top.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial( { map: THREE.ImageUtils.loadTexture( "/uploads/macdonc5/bottom.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial( { map: THREE.ImageUtils.loadTexture( "/uploads/macdonc5/back.png" ), side: THREE.BackSide } ) ),
 	( new THREE.MeshBasicMaterial( { map: THREE.ImageUtils.loadTexture( "/uploads/macdonc5/front.png" ), 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 );
}


// ====================================================================================================================
// ====================================================================================================================
// ====================================================================================================================
// This does the file read the old way using loadTexture.
// (todo) Change to asynchronous TextureLoader. A bit complex:
// Make blank skybox. Start 6 asynch file loads to call 6 return functions.
// Each return function checks if all 6 loaded yet. Once all 6 loaded, paint the skybox.
// ====================================================================================================================
// ====================================================================================================================
// ====================================================================================================================



// --- LOAD TEXTURES --------------------------------------------------------------------------------------------------
  // credits:
  // football: http:http://hd-wallpapers.us/wp-content/uploads/2014/06/golden-football.jpg

function loadTextures()
{

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

  var loader2 = new THREE.TextureLoader();                                      // the maze texture
  loader2.load ( '/uploads/macdonc5/walls.jpg', function ( thetexture ) {
	thetexture.minFilter = THREE.LinearFilter;
	paintMaze ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
  } );

  var loader4 = new THREE.TextureLoader();                                      // the ball texture
  loader4.load ( '/uploads/macdonc5/football.jpg',	function ( thetexture ) {
	thetexture.minFilter = THREE.LinearFilter;
	theball.material =  new THREE.MeshBasicMaterial( { map: thetexture } );
  } );

  var loader5 = new THREE.TextureLoader();                                      // the audience texture
  loader5.load ( '/uploads/macdonc5/crowd.jpg',	function ( thetexture ) {
	thetexture.minFilter = THREE.LinearFilter;
	paintAudience ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
  } );

  var loader6 = new THREE.TextureLoader();                                      // the ground texture
  loader6.load ( '/uploads/macdonc5/pitch.jpg',	function ( thetexture ) {
	thetexture.minFilter = THREE.LinearFilter;
	paintGround ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
  } );

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

}



// --- FIXED OBJECTS --------------------------------------------------------------------------------------------------


// --- walls ----------------------------------------------------------------------------------------------------------

function initLogicalWalls()		    // set up walls in data structure around the edge of the grid
{
  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()           // build blank boxes
{
  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 );      // cube geometry
 	    var thecube  = new THREE.Mesh( shape );
	    thecube.material.color.setHex( BLANKCOLOR );

    	thecube.position.x = translate ( i * squaresize );      // translate (i,j) block-numbering 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 )            // paint the blank boxes
{
  for ( var i = 0; i < WALLS.length; i++ )
  { 
    if ( WALLS[i] ) WALLS[i].material = material;
  }
}


// --- audience -------------------------------------------------------------------------------------------------------

function addAudience()              // set up audience
{
  var i, j, t;

  // leftwall audience centre point
  t = 0;
  i = 0;
  j = Math.trunc( gridsize / 2 );
  initThreeAudience( i, j, t);

  // backwall audience centre point
  t = 1;
  i = Math.trunc( gridsize / 2 );
  j = 0;
  initThreeAudience( i, j, t);

  // rightwall audience centre point
  t = 2;
  i = Math.trunc( gridsize );
  j = Math.trunc( gridsize / 2 );
  initThreeAudience( i, j, t);
}

function initThreeAudience( i, j, t)		// build audience
{

  var shape    = new THREE.PlaneGeometry( squaresize*gridsize, squaresize * gridsize / 2 );     // plane geometry
  var thecrowd = new THREE.Mesh( shape );
  thecrowd.material.color.setHex( BLANKCOLOR );

  thecrowd.position.x = translate ( i * squaresize );   	// translate (i,j) block-numbering to three.js (x,y,z) coordinates
  thecrowd.position.z = translate ( j * squaresize );
  thecrowd.position.y = 450;

  // rotate the side crowds by 90 degrees
  if ( i == gridsize )
  {
    thecrowd.rotation.y = ( -Math.PI / 2);         // right hand audience
  }
  if ( i == 0 )
  {
    thecrowd.rotation.y = ( Math.PI / 2 );          // left hand audience
  }

  AUDIENCE[t] = thecrowd;				// save it for later
  threeworld.scene.add( thecrowd );
}


function paintAudience ( material )         // paint the audience
{
  for ( var i = 0; i < AUDIENCE.length; i++ )
  {
    if ( AUDIENCE[i] )  AUDIENCE[i].material = material;
  }
}


// --- ground ---------------------------------------------------------------------------------------------------------

function initThreeGround()		// set up the ground
{
  var i, j;

  i = gridsize / 2;
  j = gridsize / 2;

  var shape     = new THREE.PlaneGeometry( squaresize * ( gridsize - 2 ), squaresize * ( gridsize - 2 ) );
  var theground = new THREE.Mesh( shape );
  theground.material.color.setHex( BLANKCOLOR );

  theground.position.x = translate ( i * squaresize );      // translate (i,j) block-numbering to three.js (x,y,z) coordinates
  theground.position.z = translate ( j * squaresize );
  theground.position.y = 0;

  theground.rotation.x = (-Math.PI / 2);

  threeworld.scene.add( theground );
  GROUND[0] = theground;                // save it for later
}

function paintGround ( material )       // paint the ground
{
   if ( GROUND[0] )  GROUND[0].material = material;
}


// --- maze -----------------------------------------------------------------------------------------------------------

function initLogicalMaze()      // set up the maze
{
  for ( var c = 1; c <= NOBOXES; c++ )
  {
    var i = randomintAtoB( 2, gridsize - 3 );	// inner squares are 1 to gridsize-2 so leave at least 1 clear row on left and right
  	var j = randomintAtoB( 5, gridsize - 6 );    // leave at least 4 clear rows at goal end and and 5 clear rows at near end
    GRID[i][j] = GRID_MAZE;
  }
}

function initThreeMaze()        // build blank boxes
{
  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 );      // cube geometry
  	    var thecube  = new THREE.Mesh( shape );
	    thecube.material.color.setHex( BLANKCOLOR );

  	    thecube.position.x = translate ( i * squaresize );      // translate (i,j) block-numbering to three.js (x,y,z) coordinates
  	    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 )          // paint the maze
{
 for ( var i = 0; i < MAZE.length; i++ )
 {
   if ( MAZE[i] )  MAZE[i].material = material;
 }
}

function redrawMaze()           // generate a new maze layout
{
  var i, j, t;

  for ( i = 0; i < gridsize ; i++ )      // clear out existing logical maze
  {
    for ( j = 0; j < gridsize ; j++ )
    {
      if ( GRID[i][j] == GRID_MAZE ) GRID[i][j] = GRID_BLANK;
    }
  }

  initLogicalMaze();        // generate a new random logical maze

  // update the positions of the existing graphical maze elements
  t = 0;
  for ( i = 0; i < gridsize; i++ )
    for ( j = 0; j < gridsize; j++ )
      if ( GRID[i][j] == GRID_MAZE )
      {
        MAZE[t].position.x = translate ( i * squaresize );      // translate (i,j) block-numbering to three.js (x,y,z) coordinates
  	    MAZE[t].position.z = translate ( j * squaresize );
  	    MAZE[t].position.y = 0;

        threeworld.scene.add( MAZE[t] );
        t++;
      }
}


// --- goal -----------------------------------------------------------------------------------------------------------

function initLogicalGoal()          // set up the goal
{
  var l = Math.trunc( gridsize / 2 ) - 3;	// Left goal post position
  var j = 1 ;                            // Goal is just in front of back wall
  var r = Math.trunc( gridsize / 2 ) + 3;	// Right goal post position

  GRID[l][j] = GOAL_POST;
  GRID[r][j] = GOAL_POST;

  for ( var i = l + 1; i < r; i++ )
  {
    GRID[i][j] = GOAL_LINE;
  }
}

function initThreeGoal()
{
  for ( var i = 0; i < gridsize; i++ )
    for ( var j = 0; j < gridsize; j++ )
      if ( GRID[i][j] == GOAL_POST )
      {
   	    var shape    = new THREE.CylinderGeometry( postrad, postrad, postheight );
  	    var thepost  = new THREE.Mesh( shape );
	    thepost.material.color.setHex( {color: 0xffff00}  );

  	    thepost.position.x = translate ( i * squaresize );
  	    thepost.position.z = translate ( j * squaresize );
  	    thepost.position.y = 400;

 	    threeworld.scene.add( thepost );
      }
}



// --- BALL FUNCTIONS -------------------------------------------------------------------------------------------------


function initLogicalBall()      // set up the ball
{
  // start in fixed location:
  var i, j;

  i = Math.trunc( gridsize / 2 );
  j = gridsize - 4;

  bi = i;
  bj = j;
}

function initThreeBall()        // build the ball sphere
{
  var shape = new THREE.SphereGeometry( ballrad, 32, 32 );        // sphere geometry
  theball   = new THREE.Mesh( shape );
  theball.material.color.setHex( BLANKCOLOR  );
  drawBall();
}

function drawBall()         // draw the ball given bi, bj
{
  var x = translate ( bi * squaresize );
  var z = translate ( bj * squaresize );
  var y = 50;

  theball.position.x = x;
  theball.position.y = y;
  theball.position.z = z;
 
  // roll the ball
  if ( sideway < 0 ) theball.rotation.x += squaresize / ballrad;
  if ( sideway > 0 ) theball.rotation.x -= squaresize / ballrad;
  if ( tofrom < 0 ) theball.rotation.z -= squaresize / ballrad;
  if ( tofrom > 0 ) theball.rotation.z += squaresize / ballrad;
 
  threeworld.scene.add( theball );

  threeworld.lookat.copy( theball.position );		// if camera moving, look back at where the ball is  
}

function moveLogicalBall(leftright,forwardback,position)        // the ball movement in the grid and off the walls
{

  var i, j;
  i = bi + leftright;
  j = bj + forwardback;

  if ( !occupied( i, j ) )  	// if no obstacle then move the ball
  {
    sideway = bi - i;
    tofrom  = bj - j;
    bi = i;
    bj = j;
  }
  else
  {
    if ( GRID[bi][bj] == GOAL_LINE )    // at wall between goal posts - increment score and reset ball, player and maze positions
    {
      goals++;
      initLogicalPlayer();
      initLogicalBall();
      redrawMaze();

      // reset all of the initial variables
      rolling = false;
      dist    = 0;
      pos     = 0;
      sideway = 0;
      tofrom  = 0;
      return;
    }

    if ( GRID[i][j] == GRID_WALL )            // handle reflection off walls by reversing pos position
    {
      // ball hits wall straight on
      if ( position == 2 ) pos = 6;
      else if ( position == 4 ) pos = 8;
      else if ( position == 6 ) pos = 2;
      else if ( position == 8 ) pos = 4;
      
      // ball hits wall diagonally
      if ( i == 0 ) //left wall
      {
        if ( position == 1 )        pos = 3;
        else if ( position == 7 )   pos = 5;

      }
      else if ( i == gridsize - 1 ) // right wall
      {
        if ( position == 3 )        pos = 1;
        else if ( position == 5 )   pos = 7;
      }
      if ( j == 0 ) //back wall
      {
        if ( position == 5 )        pos = 3;
        else if ( position == 7 )   pos = 1;
      }
      else if ( j == gridsize - 1 ) // front wall
      {
        if ( position == 3 )        pos = 5;
        else if ( position == 1 )   pos = 7;
      }
      return;
    }
  }
}

function getPos ()          // calculate relative position of player if adjacent to ball
{
  if (rolling) return;      // don't recalculate position if ball is rolling

  pos = 0;

  if(( bi == pi - 1 )  && ( bj == pj + 1 ))         pos = 1;
  else if (( bi == pi ) && ( bj == pj + 1 ))        pos = 2;
  else if (( bi == pi + 1 ) && ( bj == pj + 1 ))    pos = 3;
  else if (( bi == pi + 1 ) && ( bj == pj ))        pos = 4;
  else if (( bi == pi + 1 ) && ( bj == pj - 1 ))    pos = 5;
  else if (( bi == pi ) && ( bj == pj - 1 ))        pos = 6;
  else if (( bi == pi - 1 ) && ( bj == pj - 1 ))    pos = 7;
  else if (( bi == pi - 1 ) && ( bj == pj ))        pos = 8;
}

function kickBall (a)   // move ball one square at a time based on position of player relative to ball when ball is kicked
{

  if ( pos == 0 ) return;       // don't move ball if player is not beside it
  
  if ( !rolling )               // only calculate new distance to move ball if it is stationary
  {
    if ( a == ACTION_TAP )
    {
      dist = TAP;
      rolling = true;
    }
    else if ( a == ACTION_KICK )
    {
      dist = KICK;
      rolling = true;
    }
  }

  if ( dist > 0 )     // move the ball one square at a time through dist squares and decrement dist each time
  {
    if ( pos == 1 ) moveLogicalBall( -1, 1, pos );
    else if ( pos == 2 ) moveLogicalBall( 0, 1, pos );
    else if ( pos == 3 ) moveLogicalBall( 1, 1, pos );
    else if ( pos == 4 ) moveLogicalBall( 1, 0, pos );
    else if ( pos == 5 ) moveLogicalBall( 1, -1, pos );
    else if ( pos == 6 ) moveLogicalBall( 0, -1, pos );
    else if ( pos == 7 ) moveLogicalBall( -1, -1, pos );
    else if ( pos == 8 ) moveLogicalBall( -1, 0, pos );
      
    dist--;

    if (dist == 0) rolling = false;     // set rolling to false when ball has moved the required distance
  }
}



// --- PLAYER FUNCTIONS -----------------------------------------------------------------------------------------------


function initLogicalPlayer()        // set up player in fixed location behind ball
{
  var i, j;

  i = Math.round( gridsize / 2 );
  j = gridsize - 2;

  pi = i;
  pj = j;
}

function initThreePlayer ( object )         // build player
{
	object.scale.multiplyScalar ( 50 );
	theplayer = object;
	rotatePlayerTowards( Math.PI );
	drawPlayer();
}

function drawPlayer()	// draw the player given pi, pj
{
  var x = translate ( pi * squaresize );
  var z = translate ( pj * squaresize );
  var y = 0;

  theplayer.position.x = x;
  theplayer.position.y = y;
  theplayer.position.z = z;
  threeworld.scene.add( theplayer );

 threeworld.follow.copy( theplayer.position );		// follow vector = player position (for camera following player)
}

function moveLogicalPlayer( a )		// this is called by the infrastructure that gets action a from the Mind
{                                   // change players position and rotate them
  var i = pi;
  var j = pj;

  // keyboard controls change the player's position
  if ( a == ACTION_LEFT )       i--;
  else if ( a == ACTION_RIGHT ) i++;
  else if ( a == ACTION_UP ) 	j--;
  else if ( a == ACTION_DOWN ) 	j++;

  if ( true )
  {
    if (( Math.abs( bi - i ) > 1 ) || ( Math.abs( bj - j ) > 1 ))   // rotation while player isn't beside the ball
    {
      if ( a == ACTION_LEFT )         rotatePlayerTowards ( 3 * ( Math.PI / 2 ) );
      else if ( a == ACTION_RIGHT )   rotatePlayerTowards ( 1 * ( Math.PI / 2 ) );
      else if ( a == ACTION_UP )      rotatePlayerTowards ( 2 * ( Math.PI / 2 ) );
      else if ( a == ACTION_DOWN )    rotatePlayerTowards ( 0 * ( Math.PI / 2 ) );
    }
    else            // rotation of player while beside the ball
    {
        if (( bi == pi-1 ) && ( bj == pj + 1 ))         rotatePlayerTowards ( 0 * ( Math.PI / 2 ) );
        else if (( bi == pi ) && ( bj == pj + 1 ))      rotatePlayerTowards ( 0 * ( Math.PI / 2 ) );
        else if (( bi == pi + 1 ) && ( bj == pj + 1 ))  rotatePlayerTowards ( 0 * ( Math.PI / 2 ) );
        else if (( bi == pi + 1 ) && ( bj == pj ))      rotatePlayerTowards ( 1 * ( Math.PI / 2 ) );
        else if (( bi == pi + 1 ) && ( bj == pj - 1 ))  rotatePlayerTowards ( 2 * ( Math.PI / 2 ) );
        else if (( bi == pi ) && ( bj == pj - 1 ))      rotatePlayerTowards ( 2 * ( Math.PI / 2 ) );
        else if (( bi == pi - 1 ) && ( bj == pj - 1 ))  rotatePlayerTowards ( 2 * ( Math.PI / 2 ) );
        else if (( bi == pi - 1 ) && (bj == pj))        rotatePlayerTowards ( 3 * ( Math.PI / 2 ) );
    }
  }

  if ( ! occupied(i,j) )
  {
    pi = i;
    pj = j;
  }
}

function rotatePlayerTowards ( newRotation )	// rotate the player
{
  var x = ( newRotation );
  theplayer.rotation.set ( 0, x, 0);
}



// --- KEYBOARD CONTROLS ----------------------------------------------------------------------------------------------


function keyHandler(e)		// user control
{                           // Note that this.takeAction(a) is constantly running at same time, redrawing the screen.

  if ( e.keyCode == 37 )  moveLogicalPlayer ( ACTION_LEFT );
  if ( e.keyCode == 38 )  moveLogicalPlayer ( ACTION_UP );
  if ( e.keyCode == 39 )  moveLogicalPlayer ( ACTION_RIGHT );
  if ( e.keyCode == 40 )  moveLogicalPlayer ( ACTION_DOWN );
        
  if ( e.keyCode == 33 )
  {
    getPos();
    kickBall ( ACTION_TAP );        // PgUp key to move ball TAP squares
  }
  if ( e.keyCode == 34 )
  {
    getPos();
    kickBall ( ACTION_KICK );       // PgDn key to move ball KICK squares
  }
}



// --- SCORE: ---------------------------------------------------------------------------------------------------------


function updateStatusBefore( a )      // this is called before anyone has moved on this step, user 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()		// player and ball have moved, can calculate score
{
  // new state after both have moved
  var instr = " &nbsp;&nbsp;&nbsp;&nbsp;  Move Player with Arrow Keys ( Up, Dn, L & R); PgUp to Tap ball; PgDn to Kick it. Enjoy!<BR> "
  var y = self.getState();
  var status = " &nbsp; y = (" + y.toString() + ") ";

  status = status + instr;

  $("#user_span4").html( status );

  var status = "   Goals: " + goals;

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



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

  step    = 0;
  goals   = 0;
  pos     = 0;
  sideway = 0;
  tofrom  = 0;
  rolling = false;

 // for all runs:

  initGrid();
  initLogicalWalls();
  initLogicalMaze();
  initLogicalGoal();
  initLogicalPlayer();
  initLogicalBall();

  // 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();
	initThreeGoal();
//	initThreePlayer();
	initThreeBall();
	addAudience();
	initThreeGround();

	// Then paint them with textures

    loadTextures();

	document.onkeydown = keyHandler;
  }
};

this.getState = function()
{
  var x = [ pi, pj, bi, bj ];
  return ( x );
};

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

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

  moveLogicalPlayer( a );

  // move the ball if it has been kicked

  getPos();      // get the position of the player relative to ball
    
  kickBall( a );   // move the ball

  if ( true  )
  {
    drawPlayer();
    drawBall();
   
    // stop ball spinning
    sideway = 0;
    tofrom  = 0;
   
    updateStatusAfter();			// show status line after moves
  }
};

this.endRun = function()
{
  if ( true  )
  {
    musicPause();
    $("#user_span6").html( " &nbsp; <font color=red> <B> Run over. </B> </font>   "  );
  }
};



}



// --- 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=/uploads/starter/Defense.Line.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 );
}