// Cloned by Kline on 15 Nov 2022 from World "Football Model World (clone by Mustafa Kamal)" by Mustafa Kamal
// Please leave this clone trail here.
// Cloned by Mustafa Kamal on 15 Nov 2022 from World "Football Model World" by Kieran Turgoose
// Please leave this clone trail here.
/*
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() {
AB.world.newRun = function()
{
AB.newSplash();
AB.splashHtml ( `
<h1> Password only Websocket boxes </h1>
A version of Websocket boxes that only certain users can join. <br>
You enter a password. You only join with users who enter the same password (not with all users of the World). <br>
The first user to run the World can enter any password! But other users must know what password they will use.
<p>
Enter password:
<input style='width:25vw;' maxlength='2000' NAME="p" id="p" VALUE='' >
<button onclick='start();' class=ab-normbutton >Start</button>
<p>
<div id=errordiv name=errordiv> </div> ` );
AB.runReady = false;
};
AB.world.nextStep = function()
{
moveArmy();
};
function alphanumeric ( str ) // return if string is just alphanumeric
{
var rc = str.match(/^[0-9a-zA-Z]+$/); // start of line "[0-9a-zA-Z]+" end of line
if ( rc ) return true;
else return false;
}
function start() // user has entered a password
{
var password = jQuery("input#p").val();
password = password.trim();
if ( ! alphanumeric ( password ) )
{
$("#errordiv").html( "<font color=red> <B> Error: Password must be alphanumeric. </b></font> " );
return;
}
// else we have a password, start the socket run with this password
AB.socketStart ( password );
AB.removeSplash();
}
// 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/kamalm3/sansiro1.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kamalm3/sansiro1.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/kamalm3/sansiro1.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/kamalm3/sansiro1.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 == 18) moveLogicalAgent(KICK_BALL); //pgUP
if (e.keyCode == 32) 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 -------------------------------------------------------