Code viewer for World: Castle World


// ==== Starter World =================================================================================================
// This code is designed for use on the Ancient Brain site.
// This code may be freely copied and edited by anyone on the Ancient Brain site.
// To include a working run of this program on another site, see the "Embed code" links provided on Ancient Brain.
// ====================================================================================================================




// Demo of 3D model
// OBJ plus MTL

// A "Simple World" like chase inside an invisible arena inside the castle 
// Chase uses x,y,z rather than grid of squares 


  
  
  

// ===================================================================================================================
// === Start of tweaker's box ======================================================================================== 
// ===================================================================================================================

// The easiest things to modify are in this box.
// You should be able to change things in this box without being a JavaScript programmer.
// Go ahead and change some of these. What's the worst that could happen?


AB.clockTick       = 100;    

	// Speed of run: Step every n milliseconds.  
	
AB.maxSteps        = 500;    

	// Length of run: Maximum length of run in steps.  

AB.screenshotStep  = 50;   
  
	// Take screenshot on this step. (All resources should have finished loading.)  

	

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

	const OBJPATH = "/uploads/starter/";	// path of OBJ and MTL 
	const OBJNAME = "castle.obj";
	const MTLNAME = "castle.new.mtl";

// castle credit
// https://www.script-tutorials.com/webgl-with-three-js-lesson-6/
// http://www.script-tutorials.com/demos/409/index2.html


// =================================================================================
// Bug: Original MTL file fails with new Three.js  
// The original "castle.mtl" works with old Three.js but fails with new version.
// Solution: Edit the MTL file. See note here:
// https://ancientbrain.com/docs.three.php
// =================================================================================




	const TEXTURE_AGENT 	= '/uploads/starter/pacman.jpg' ;
	const TEXTURE_ENEMY 	= '/uploads/starter/ghost.3.png' ;

// credits:
// https://commons.wikimedia.org/wiki/Category:Pac-Man_icons
// https://commons.wikimedia.org/wiki/Category:Skull_and_crossbone_icons
	
	const MUSIC_BACK  = '/uploads/starter/Defense.Line.mp3' ;
	
	
const SKYCOLOR 		= 0xffffcc ;				// a number, not a string 
const LIGHTCOLOR 	= 0xffffff ;

const squaresize 	= 50;							// size of cube length  


// basic castle model size 
// see debug line to compute model size below 
// Xsize: 6158.792236328125 Ysize: 703.5784759521484 Zsize: 3791.05908203125

const MODELLENGTH 			= 6159;							
const MODELWIDTH			= 3791; 

const SCALE_CASTLE			= 0.5;							// scale it by this		 
const SCALEDMODELLENGTH 	= MODELLENGTH * SCALE_CASTLE;	 			 
const SCALEDMODELWIDTH	 	= MODELWIDTH  * SCALE_CASTLE;	 			 

const startRadiusConst 		= SCALEDMODELLENGTH * 0.5 ; 
const maxRadiusConst 		= SCALEDMODELLENGTH * 10 ; 


	// camera points at 0,0 
	// where to put castle? 
	// fit bottom LHS corner to here:
	
	const CX 	= - ( SCALEDMODELLENGTH * 0.3 );		 
	const CZ 	=   ( SCALEDMODELWIDTH  * 0.3 );		 

	
// how far can the agent and enemy wander inside the castle
// experiment to see where the inner box fits inside the model 

const MIN_X 	= CX + ( SCALEDMODELLENGTH * 0.1 ) ;
const MAX_X		= CX + ( SCALEDMODELLENGTH * 0.55 ) ;

const MIN_Z 	= CZ - ( SCALEDMODELWIDTH * 0.45 ) ;
const MAX_Z		= CZ - ( SCALEDMODELWIDTH * 0.1 ) ;



// how much do they move each step 

const MOVE = squaresize ;

// how close do they have to be to lose score 

const BADCLOSE = squaresize * 5;



//--- change ABWorld defaults: -------------------------------

ABHandler.GROUNDZERO		= true;						// "ground" exists at altitude zero

ABWorld.drawCameraControls = false; 


// ===================================================================================================================
// === End of tweaker's box ==========================================================================================
// ===================================================================================================================


// You will need to be some sort of JavaScript programmer to change things below the tweaker's box.








	
 // we have a splash screen
 // behind the splash screen, newRun is running, loading resources 
 // do not start the run loop until resources ready AND splash screen is dismissed 

	var resourcesLoaded = false;
	var splashClicked = false;
	
	

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


	

 

var thecastle, theagent, theenemy;		  

var agent_texture, enemy_texture; 

var badsteps;
var goodsteps;

	

function loadResources()		// asynchronous file loads - call initScene when finished 
{

	// load castle model OBJ and materials MTL (which reference JPEGs)
	
	var m = new THREE.MTLLoader();
	m.setResourcePath ( OBJPATH );
	m.setPath         ( OBJPATH );
	
	m.load ( MTLNAME, function ( materials ) 
	{
		materials.preload(); 
		var o = new THREE.OBJLoader();
		o.setMaterials ( materials );
		o.setPath ( OBJPATH );
		
		o.load ( OBJNAME, function ( object ) 
		{
			thecastle = object;
			if ( asynchFinished() )	initScene();		 
		});
	});
	
	
	// load cube textures 
	var loader1 = new THREE.TextureLoader();
	var loader2 = new THREE.TextureLoader();
		
	loader1.load ( TEXTURE_AGENT, function ( thetexture )  	 
	{
		thetexture.minFilter  = THREE.LinearFilter;
		agent_texture = thetexture;
		if ( asynchFinished() )	initScene();		 
	});	
	
	loader2.load ( TEXTURE_ENEMY, function ( thetexture )  
	{
		thetexture.minFilter  = THREE.LinearFilter;
		enemy_texture = thetexture;
		if ( asynchFinished() )	initScene();		 
	});
}


function asynchFinished()		 
{
	if ( thecastle && agent_texture && enemy_texture )  return true;  
	else return false;
}	
	
	
	



function initScene()		// file loads have returned 
{
	
 // add castle object to scene  

	thecastle.scale.multiplyScalar ( SCALE_CASTLE );    	  

    thecastle.position.y = - (squaresize * 0.5);				// adjust so cubes (centred on 0) appear exactly above castle floor 
    thecastle.position.x = CX;
    thecastle.position.z = CZ;  
 
   	ABWorld.scene.add ( thecastle );
			
	// calculate OBJ object size:
	//	console.log ( "Xsize: " + ABWorld.objectXsize(thecastle) + " Ysize: " + ABWorld.objectYsize(thecastle) + " Zsize: " + ABWorld.objectZsize(thecastle) );

	

 // set up enemy  
   
	 shape    = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );			 
	 theenemy = new THREE.Mesh( shape );
 	 theenemy.material =  new THREE.MeshBasicMaterial( { map: enemy_texture } );
	 ABWorld.scene.add(theenemy);

   // set position 
	 theenemy.position.y = 0;
	 theenemy.position.x = AB.randomIntAtoB( MIN_X, MAX_X );
	 theenemy.position.z = AB.randomIntAtoB( MIN_Z, MAX_Z );
	  
	/*
	// examine where different points are and are they inside the castle:
	
	 theenemy.position.x = 0;
	 theenemy.position.z = 0;
	
	 theenemy.position.x = MIN_X;
	 theenemy.position.z = MIN_Z;

	 theenemy.position.x = MAX_X;
	 theenemy.position.z = MAX_Z;
	*/
	
	
	
 // set up agent 
   
	 shape    = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );			 
	 theagent = new THREE.Mesh( shape );
	 theagent.material =  new THREE.MeshBasicMaterial( { map: agent_texture } );
	 ABWorld.scene.add(theagent);
	 
   // put agent some distance from enemy 
	 do
	 {
	  x = AB.randomIntAtoB( MIN_X, MAX_X );
	  z = AB.randomIntAtoB( MIN_Z, MAX_Z );
	 }
	 while ( AB.distance2D ( x, z, theenemy.position.x, theenemy.position,z ) < BADCLOSE );  	
	 
	 theagent.position.y = 0;
	 theagent.position.x = x;
	 theagent.position.z = z;
	 
	 
 // ready to start run loop? 
	
	ABWorld.render();

	console.log ( "Resources loaded." );
	resourcesLoaded = true;
	
	if ( resourcesLoaded && splashClicked ) 
		AB.runReady = true;  		// start run loop 
	
}



// --- check a proposed move is inside castle -----------------------------------
	
function withinBounds ( x, z )
{
	return ( 	( x > MIN_X ) &&	( x < MAX_X ) &&
				( z > MIN_Z ) &&	( z < MAX_Z ) );
}
 
 

// --- take actions -----------------------------------


function moveLogicalEnemy()
{ 

// move towards agent 
// put some randomness in so it won't get stuck with barriers 
 
// proposed move   

	var x = theenemy.position.x;
	var z = theenemy.position.z;
 
		if ( x < theagent.position.x ) 	x = x + AB.randomIntAtoB ( 0, MOVE ) ; 
		else						 	x = x - AB.randomIntAtoB ( 0, MOVE ) ; 

		if ( z < theagent.position.z ) 	z = z + AB.randomIntAtoB ( 0, MOVE ) ; 
		else 						 	z = z - AB.randomIntAtoB ( 0, MOVE ) ; 
	
	if ( withinBounds ( x, z ) )		// if in bounds 
	  if ( AB.distance2D ( x, z, theagent.position.x, theagent.position.z ) > squaresize * 2 )  	// limit to how close we go to agent 
	  {
		theenemy.position.x = x;
		theenemy.position.z = z;
	  }
	// else just miss a turn
}



function moveLogicalAgent( a )			// this is called by the infrastructure that gets action a from the Mind 
{ 
// proposed move

 var x = theagent.position.x;
 var z = theagent.position.z;
 
      if ( a == ACTION_LEFT ) 	x = x - MOVE ;
 else if ( a == ACTION_RIGHT ) 	x = x + MOVE ;
 else if ( a == ACTION_UP ) 	z = z + MOVE ;
 else if ( a == ACTION_DOWN ) 	z = z - MOVE ;

 if ( withinBounds ( x, z ) )		 
   if ( AB.distance2D ( x, z, theenemy.position.x, theenemy.position.z ) >= squaresize   )  	// limit to how close we go to enemy 
   {
		theagent.position.x = x;
		theagent.position.z = z;
   }
 // else ignore action 
}



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


function badstep()			 
{
 if ( AB.distance2D ( theagent.position.x, theagent.position.z, theenemy.position.x, theenemy.position.z ) < BADCLOSE ) return true;
 else return false;
}

 
function   updateStatus()		 
{
 var score = AB.world.getScore();
 AB.msg ( "Step: " + AB.step + " out of " + AB.maxSteps + ". Score: " + score ); 
}






AB.world.newRun = function() 
{
	badsteps = 0;		
	goodsteps = 0;

	ABWorld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR  ); 

	// newRun can run behind splash screen 
	// do not start run loop until resources ready AND splash screen is dismissed 
	
	AB.runReady = false; 
	
	loadResources();		// aynch file loads		
							// calls initScene() when it returns 
 
    var thelight = new THREE.DirectionalLight ( LIGHTCOLOR, 3 );
    thelight.position.set ( startRadiusConst, startRadiusConst, startRadiusConst );
    ABWorld.scene.add(thelight);
		
};



AB.world.getState = function()
{
 var x = [ theagent.position.x, theagent.position.z, theenemy.position.x, theenemy.position.z ];
  return ( x );  
};


AB.world.takeAction = function ( a )
{
  moveLogicalAgent(a);

 // if ( ( AB.step % 2 ) == 0 )              // slow the enemy down to every nth step
   moveLogicalEnemy();

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

   updateStatus();
};


AB.world.getScore = function()
{
 return goodsteps;
};





 


	
// --- Splash screen --------------------------------------------------------------
// Splash screen is to get audio started on mobile/Chrome by user interaction.
// This should works on all platforms - plays background music.

	
	// display standard splash screen (World title, World image, Start button) 
	
	AB.newSplash();

	
	// when user clicks/touches button on splash screen, audio starts and run starts:
		
	AB.splashClick ( function ()        
	{
		// audio linked to user interaction:  
		AB.backgroundMusic ( MUSIC_BACK );	
		
		AB.removeSplash();			// remove splash screen 
		
		// ready to start run loop? 
		
		splashClicked = true;
		
		if ( resourcesLoaded && splashClicked ) 
			AB.runReady = true;  		// start run loop 
		
	});