// Cloned by Niall Kelly on 29 Nov 2022 from World "Castle World" by Starter user
// Please leave this clone trail here.
// ==== 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
});