Code viewer for World: Football Model World
/*  
    This is my world and mind to be corrected: "Football Model World" & "Football Model Mind". 
    Instructions are on screen. But it is arrows to move, PgUp to kick 1 space, PgDn to kick 5 spaces. 
    The basic concept is a game that allows the user to control their agent, in this case a Bart Simpson model, and kick the ball in the direction they're facing,
    to attempt to score a goal past the skeleton goalkeeper. 
    The goalkeeper is moving at random, 1 space at a time between the length of the goal, and it is the player's objective to score past it.
    If the ball hits the goalkeeper it will either bounce off the goalie a short distance, or be cleared by him 10 spaces away from goal.
    If the goal is scored the score will increment on screen and both the ball and the user will be reset to their origin positions.
    The ball rotates upon movement and can also bounce off of walls in order to prevent it becoming stuck.
    I implemented floor geometry inside the skybox to use as the main floor. 
    The user also rotates as he turns direction. 
    I set the time limit to 2000steps(10 minutes).
    The gameplay is accompanied by the dulcet tones of the Match of the Day theme tune.
    Upon scoring a goal, the crowd will cheer. And at the final whistle the referee's whistle will indicate full time and the screen message will display the final score. 
    
    The skybox consists of a stadium picture on each side to represent the stands of a stadium.
    The walls of the floor are textured with "advertising boards" to represent an actual stadium.
    
    I will explain the methods where necessary below.
    
    Have fun trying to score past the world-class goalkeeper!
*/




 
const   CLOCKTICK   = 150;        // speed of run - move things every n milliseconds
const   MAXSTEPS  = 2000;       // length of a run before final score(2000 = 10 minutes)


const  SCREENSHOT_STEP = 50;    



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


const gridsize = 30;          // number of squares along side of world     
const squaresize = 100;         // size of square in pixels
const MAXPOS = gridsize * squaresize;   // length of one side in pixels 

const SKYCOLOR  = 0xffffcc;       // a number, not a string 
const BLANKCOLOR  = SKYCOLOR ;      // make objects this color until texture arrives (from asynchronous file read)
const  LIGHTCOLOR   = 0xffffff ;

const startRadiusConst    = MAXPOS * 0.8 ;    // distance from centre to start the camera at
const skyboxConst     = MAXPOS ;    // where to put skybox 
const maxRadiusConst      = MAXPOS * 5 ;    // maximum distance from camera we will render things  

var ballDirection = "";
var ballMoves = 0;


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

const KICK_BALL       = 5; //Action for kicking the ball 1 space.
const KICK_BALL_LONG  = 6; //Action for kicking the ball 5 spaces.

const ENEMY_MOVE_UP   = 7; //Moves the goalkeeper up 1 space.
const ENEMY_MOVE_DOWN = 8; //Moves the goalkeeper down 1 space.

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

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

function randomPick ( a, b )
{
 if ( randomBoolean() ) 
  return a;
 else
  return b;
}


//---- start of World class -------------------------------------------------------
 
function World() { 
    
// most of World can be private 
// regular "var" syntax means private variables:


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 handle to each wall block object so can find it later to paint it 
var theagent, theenemy, theball;      

var agentRotation = 0;
var enemyRotation = 0;      // with 3D models, current rotation away from default orientation
var ballRotation = 0;

// enemy, agent and ball position on squares
var ei, ej, ai, aj, bi, bj;
var  step;
var goals;
var self = this;            // needed for private fn to call public fn - see below  


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;  Commented out so that the ball can go through the agent in case it's stuck at the wall.
     if ( ( bi == i ) && ( bj == j ) ) return true;
     if ( GRID[i][j] == GRID_WALL ) return true;    // fixed objects   
     return false;
}


function translate ( x ) 
{
    return ( x - ( MAXPOS/2 ) );
}

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

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

    //This loads the "advertisement board" around the pitch
    var loader1 = new THREE.TextureLoader(); 
    loader1.load ( '/uploads/kieranturgoose1/advert.jpg',    function ( thetexture )
    {      
        thetexture.minFilter = THREE.LinearFilter;
        paintWalls ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
    } ); 
    
    //Texture for the football itself.
    var loader2 = new THREE.TextureLoader();
    loader2.load ( '/uploads/kieranturgoose1/footballtexture.png',    function ( thetexture )
    {      
        thetexture.minFilter = THREE.LinearFilter;
        theball.material = new THREE.MeshBasicMaterial( { map: thetexture } );
    } ); 
}

//To scale and add the agent to the world.
function addparker ( object )
{
  object.scale.multiplyScalar ( 200 );        
  theagent = object;
  object.rotation.y = (Math.PI /2);
  threeworld.scene.add( theagent ); 
}

function initSkybox() 
{
// urban photographic skyboxes, credit:
// http://opengameart.org/content/urban-skyboxes

   var materialArray = [
  ( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kieranturgoose1/shittystadium.jpg" ), side: THREE.BackSide } ) ),
  ( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kieranturgoose1/shittystadium.jpg" ), side: THREE.BackSide } ) ),
  ( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kieranturgoose1/cloudy-sky.jpg" ), side: THREE.BackSide } ) ),
  ( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kieranturgoose1/modern-wall-and-floor-tile.jpg" ), side: THREE.BackSide } ) ),
  ( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kieranturgoose1/shittystadium.jpg" ), side: THREE.BackSide } ) ),
  ( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kieranturgoose1/shittystadium.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
  
    //Floor geometry to use as the main floor. Textured with a football pitch .jpg
    //Co-ordinates of floor adjusted accordingly. 
    var floorMaterial = new THREE.MeshBasicMaterial( {map: THREE.ImageUtils.loadTexture('/uploads/kieranturgoose1/footballpitch.jpg'), side:THREE.DoubleSide} );
    var floorGeometry = new THREE.PlaneGeometry(skyboxConst, skyboxConst ,0, 0);
    var floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.position.y = -50;
    floor.position.x = -50;
    floor.rotation.x = Math.PI / 2;
    threeworld.scene.add(floor);

}


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 && j!=13 && j!=14 && j!=15 && j!=16 && j!=17)
            {
                var shape    = new THREE.BoxGeometry( squaresize, squaresize, 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 )    // paint blank boxes  
{
    for ( var i = 0; i < WALLS.length; i++ )
    { 
        if ( WALLS[i] )  WALLS[i].material = material;
    }
}



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


function drawEnemy()    // given ei, ej, draw it 
{
    if ( theenemy )
    {
        var x = translate ( ei * squaresize );    
        var z = translate ( ej * squaresize );    
        var y = -50;    
    
        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 buildenemy ( object ) 
{ 
    object.scale.multiplyScalar ( 4 );        // make 3d object n times bigger 
    object.traverse( paintEnemy );
    object.rotation.y = Math.PI /2;
    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 initLogicalEnemy()
{
    var i, j;
    do
    {
        i = randomintAtoB(1,gridsize-2);
        j = randomintAtoB(1,gridsize-2);
    }
    while ( occupied(i,j) );     // search for empty square 
        ei = 2;
        ej = 13;  //Starting point for the goalkeeper.
}

//Return the goalkeepers movements randomly between the length of the goal.
function jgetAction()
{
    if ( ej == 13 ) return (ENEMY_MOVE_UP);
    if ( ej == 14 || ej == 15 || ej == 16 ) return enemyGetAction();
    if ( ej == 17)  return (ENEMY_MOVE_DOWN); 
    //return ( ACTION_STAYSTILL );
}

//Called above to return one of the actions randomly when on co-ordinate: 14,15 or 16.
function enemyGetAction()
{
    return randomPick (ENEMY_MOVE_UP, ENEMY_MOVE_DOWN);
}

//How the enemy actions are defined.
function moveLogicalEnemy()
{
    var a = jgetAction();
    if ( a == ENEMY_MOVE_UP )
    {
        ej = ej+1;
    }
    else if ( a == ENEMY_MOVE_DOWN )
    {
        ej = ej-1;
    }

}

const INTERIMROT = 10;    // number of interim rotations drawn when model turns round

// all rotations positive 0 to 2 PI 

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

// --- Ball functions -----------------------------------

//Initialising the ball.
function initThreeBall()
{
    var ball   = new THREE.SphereGeometry( squaresize/3, squaresize/3, squaresize/3);      
    theball = new THREE.Mesh(ball);
    theball.material.color.setHex( BLANKCOLOR  );  
    threeworld.scene.add( ball );
    drawBall();      
}

//Drawing the ball
function drawBall()   // given bi, bj, draw it 
{
    if(theball)
    {
        var x = translate (bi*squaresize);     
        var z = translate (bj*squaresize);    
        var y = -16;    
        
        theball.position.x = x;
        theball.position.y = y;
        theball.position.z = z;
        
        threeworld.scene.add( theball ); 
        
        threeworld.lookat.copy ( theball.position );    // if camera moving, look back at where the enemy is  
        threeworld.lookat.y = ( squaresize * 1.5 );     // point camera higher up 
    }
}

//Start position of the ball. Midspot of pitch.
function initLogicalBall()
{
    bi = 15;
    bj = 15;
}

//Movements of the ball defined.
function moveLogicalBall()
{
    var i = bi;
    var j = bj;

    //Moving the ball depending on it's direction. 
    if(ballDirection == "right")
    {
        i = bi+1; 
    }
    if(ballDirection == "left")
    {
        i = bi-1;
    }
    if(ballDirection == "up")
    {
        j = bj-1;
    }
    if(ballDirection == "down")
    {
        j = bj+1;
    }

    if(ballMoves > 0) //If there are still rolls left to be done, rotate accordinly. 
    {
        if( !occupied(i,j) )
        {
            //Rotate the ball to look like it is rolling. 
            if( bi < i) //move right
            {
                theball.rotation.z -= 0.75;
            }
            else if (bi > i) //move left
            {
                theball.rotation.z += 0.75;
            }
            else if( bj < j) //move down
            {
                theball.rotation.x += 0.75;
            }
            else if (bj > j) //move up
            {
                theball.rotation.x -= 0.75;
            }
            bi = i;
            bj = j;
            ballMoves--;
        }
        //If against the wall(occupied) but not at the enemy's space.
        else if(ei != bi || ej != bj)
        {
            if(ballDirection == "up")
            {
                ballDirection = "down";
            }
            else if(ballDirection == "down")
            { 
                ballDirection = "up";
            }
            else if(ballDirection == "right")
            {
                ballDirection = "left";
            }
            else if(ballDirection == "left")
            {
                ballDirection = "right";
            }
        }
    }
    //If shot is blocked the goalkeeper "clears" the ball 10 spaces.
    else if(bj == ej && bi == ei)
    {
        bi = bi+10;
        ballMoves = 0;
    }
}
// --- agent functions -----------------------------------


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

        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()
{
    var i, j;
    do
    {
        i = randomintAtoB(1,gridsize-2);
        j = randomintAtoB(1,gridsize-2);
    }
    while ( occupied(i,j) );     // search for empty square 
        ai = 27; //Starting point for Bart
        aj = 15;
}

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);
    if (e.keyCode == 33) moveLogicalAgent(KICK_BALL);      //pgUP
    if (e.keyCode == 34) moveLogicalAgent(KICK_BALL_LONG); //pgDWN
}

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

    //Defines the actions for each of the key presses.
    if ( a == ACTION_LEFT ) i--;
    else if ( a == ACTION_RIGHT )   i++;
    else if ( a == ACTION_UP )  j++;
    else if ( a == ACTION_DOWN )  j--;
    //To kick the ball 1 space
    else if ( a == KICK_BALL)
    {
        //check if the ball is beside us
        
        if ((bi == ai+1) && (bj == aj)) //ball is to our immediate right
        {
            ballMoves = 1;
            ballDirection = "right"; //set direction
        }
        if ((bi == ai-1) && (bj == aj)) //ball is to our immediate left
        {
            ballMoves = 1;
            ballDirection = "left";
        }
        if ((bj == aj+1) && (bi == ai)) //ball is to our immediate underneath
        {
            ballMoves = 1;
            ballDirection = "down";
        }
        if ((bj == aj-1) && (bi == ai)) //ball is to our immediate above
        {
            ballMoves = 1;
            ballDirection = "up";
        }
    }
    //To kick the ball 5 spaces.
    else if ( a == KICK_BALL_LONG)
    {
        //check if the ball is beside us
        
        if ((bi == ai+1) && (bj == aj)) //ball is to our immediate right
        {
            ballMoves = 5;
            ballDirection = "right";
        }
        if ((bi == ai-1) && (bj == aj)) //ball is to our immediate left
        {
            ballMoves = 5;
            ballDirection = "left";
        }
        if ((bj == aj+1) && (bi == ai)) //ball is to our immediate underneath
        {
            ballMoves = 5;
            ballDirection = "down";
        }
        if ((bj == aj-1) && (bi == ai)) //ball is to our immediate above
        {
            ballMoves = 5;
            ballDirection = "up";
        }
    }
    if (!occupied(i,j)) 
    {
        //Rotates the agent upon key presses.
        if ( true  )
        {
            if ( a == ACTION_LEFT )   {  rotateAgentTowards ((Math.PI / 2)); }
            else if ( a == ACTION_RIGHT )  {  rotateAgentTowards (3* (Math.PI / 2)); }
            else if ( a == ACTION_UP )     {  rotateAgentTowards (Math.PI); }
            else if ( a == ACTION_DOWN )   {  rotateAgentTowards (0); }
        }
        ai = i;
        aj = j;
    }
}

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


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

//Counts goals scored when it reaches the goal area. Resets the agent and ball to origin position.
function goalScored()
{
  if(bi == 1 && (bj == 13 || bj == 14 || bj == 15 || bj == 16 || bj == 17))
  {
    goals++;
    bi = 14;
    bj = 15;
    ballMoves = 0;  //Resets to 0 so the ball stops dead upon reset to origin position.
    ai = 27;
    aj = 15;
    //Plays crowd cheer when goal is scored
    var p = "<audio  id=theaudio  src=/uploads/kieranturgoose1/crowdcheersoundeffect.mp3 autoplay> </audio>" ;
    $("#user_span3").html( p );
  }
  return goals;
}

//Updates the onscreen status.
function updateStatus()    
{
    var goals = goalScored();
    var status =  "Score goals by kicking the ball past the skeleton goalie. Use arrow keys to move. PgUp kicks one space. PgDN kicks five spaces. Step: " + step + " out of " + MAXSTEPS + ". Goals: " + goals;
    $("#user_span1").html( status );
}

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


// must have this public variable:

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

this.newRun = function() 
{
    this.endCondition = false;
    step = 0;
    goals = 0;

    // define logical data structure for the World, even if no graphical representation:

    initGrid();
    initLogicalWalls(); 
    initLogicalAgent();
    initLogicalEnemy();
    initLogicalBall();

    if ( true  )
    {
            threeworld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR  ); 


        // light
        var ambient = new THREE.AmbientLight();
        threeworld.scene.add( ambient );

        document.addEventListener("keydown",keyHandler); //Allows key presses

        var thelight = new THREE.DirectionalLight ( LIGHTCOLOR, 3 );
        thelight.position.set ( startRadiusConst, startRadiusConst, startRadiusConst );
        threeworld.scene.add(thelight);
    


        //Plays Match of the Day theme song throughout playthrough.
        var x = "<audio  id=theaudio  src=/uploads/kieranturgoose1/matchoftheday-themetune.mp3   autoplay loop> </audio>" ;
        $("#user_span2").html( x );
        
        initSkybox();
        initThreeWalls();
        initThreeBall();
        loadTextures();     // will return sometime later, but can go ahead and render now  
    }   
};

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



this.nextStep = function()		 
{
 var a = 4;

    step++;

    moveLogicalAgent(a);
    moveLogicalBall();  
    if ( ( step % 2 ) == 0 )   // slow the enemy down to every nth step
    {
        moveLogicalEnemy();
    }

    if ( true  )
    {
        drawAgent();
        drawEnemy();
        drawBall();
        updateStatus();
    }
    if(step == (MAXSTEPS-2)) //Plays the final whistle audio just before the run ends.
    {
        var p = "<audio  id=theaudio  src=/uploads/kieranturgoose1/refereewhistlesoundeffects.mp3	autoplay> </audio>" ;
        $("#user_span3").html( p );
    }
    if(step == MAXSTEPS)  //Updates the screen at the end of the game. 
    {
        var endGame =  "FULL TIME!         Final Score: " + goals; 
        $("#user_span1").html( endGame );
    }
};



this.endRun = function()
{
};


}

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