Code viewer for World: Lion Chase
// Cloned by Martin Derwin on 30 Nov 2022 from World "User-controlled Model 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.
// ====================================================================================================================

// User controlled (not Mind controlled) 3D model World with various features:
// - User controlled UP/LEFT/RIGHT arrows move model
// - "First Person View" - Camera rotates with direction you are facing
//   Best effect is with "Move with" camera
// - Can have one or multiple lions chasing you

// Smooth movement
// UP moves forward in whatever angle you are at
// LEFT/RIGHT rotate by small angle

// Uses x, y, z rather than grid of squares
// Has collision detection for lion moves

// ===================================================================================================================
// === 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?

/* PROJECT DESCRIPTION:
Authors: Patricija Shalkauskaite, Martin Derwin
How to play:
- You play as a mouse running from lions
- Move Player 1 with the arrow keys and Player 2 with WASD keys
- If a player is caught by a lion, they lose and the other player wins
*/

AB.clockTick       = 100;    		 // Speed of run: Step every n milliseconds. Default 100.
AB.maxSteps        = 10000;  		 // Length of run: Maximum length of run in steps. Default 1000.
AB.screenshotStep  = 100;    	   	 // Take screenshot on this step. (All resources should have finished loading.) Default 50.
AB.drawRunControls = true;   		 // Turn on the Run/Step/Pause controls
ABWorld.drawCameraControls = false;  // Scrap the Track/Move with/Normal camera controls


//---- global constants: -------------------------------------------------------
// number of lions, an array for each player:
const NO_LIONS_1 = 30;
const NO_LIONS_2 = 30;

const OBJ_LIONS 	= '/uploads/martind/Lion-obj.OBJ' ; // credit: https://www.turbosquid.com/3d-models/3d-lioness-animation/1114520

const OBJPATH = "/uploads/martind/";	// path of OBJ and MTL (Peter Parker model)
const OBJNAME = "rat.obj";				// credit: https://www.turbosquid.com/3d-models/mouse-mammal-rodent-max/603885
const MTLNAME = "player2texture.jpg"	// rat object mtl file didn't work

// multiply model sizes by some amount:
const SCALE_RAT     = 30;

// random size LION with each run:
const SCALE_LIONS 	= AB.randomFloatAtoB ( 1, 3 );

const TEXTURE_LION = '/uploads/martind/fur.jpg' ;

const MAXPOS 		= 3500;			        // length of one side of the arena
const SKYCOLOR 		= 0xffffcc;				// a number, not a string
const LIGHTCOLOR 	= 0xffffff ;

const startRadiusConst	 	= MAXPOS ;			// distance from centre to start the camera at
const maxRadiusConst 		= MAXPOS * 10 ;		// maximum distance from camera we will render things

const	AGENT_STEP = 100;  // how much agent moves each step
const	ENEMY_STEP = 50; // how much enemy moves each step

// this is as close as models come to other models
const 	CLOSE_LIMIT = 100;

//--- change ABWorld defaults: -------------------------------
ABHandler.MAXCAMERAPOS 	= maxRadiusConst ;
ABHandler.GROUNDZERO		= true;	        // "ground" exists at altitude zero

// --- Rotations -----------------------

// rotate by some amount of radians from the normal position
// default is 0 which has the model facing DOWN (towards increasing z value) as far as the initial camera sees
const ROTATE_AMOUNT      =  Math.PI / 20 ;      // rotate amount in radians (PI = 180 degrees)

//--- skybox: -------------------------------
// space skybox, credit:
// http://en.spaceengine.org/forum/21-514-1

const SKYBOX_ARRAY = [			// skybox
    "/uploads/pat/cheese.png",
    "/uploads/pat/cheese.png",
    "/uploads/pat/cheese.png",
    "/uploads/pat/cheese.png",
    "/uploads/pat/cheese.png",
    "/uploads/pat/cheese.png"
    ];

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

// You will need to be some sort of JavaScript programmer to change things below the tweaker's box.
var THELIONS1 = new Array ( NO_LIONS_1 );
var THELIONS2 = new Array ( NO_LIONS_2 );

var theagent, theagent2, theenemy; // player1, player2, lions
var p1lose = false;                // when this is true, player 1 has lost
var p2lose = false;                // when this is true, player 2 has lost

var agentRotation = 0 ;
var agentRotation2 = 0 ;
var enemyRotation = 0 ;
var enemyRotation2 = 0 ;

var lion_texture;

function loadResources()		// asynchronous file loads - call initScene() when all finished
{
	// load lion - OBJ that we will paint with a texture
	var loader = new THREE.OBJLoader ( new THREE.LoadingManager() );

	loader.load ( OBJ_LIONS, function ( object )
	{
		theenemy = object;						// canonical enemy object - may be copied multiple times
		if ( asynchFinished() )	initScene();
	});

	// load RAT model - OBJ plus MTL (plus TGA files)
	THREE.DefaultLoadingManager.addHandler ( /\.tga$/i, new THREE.TGALoader() );

	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 )
		{
			theagent = object;
			if ( asynchFinished() )	initScene();
		});
	});

	var n = new THREE.MTLLoader();
	n.setResourcePath(OBJPATH);
	n.setPath(OBJPATH);
	n.load(MTLNAME, function (materials)
	{
	    materials.preload();
	    var p = new THREE.OBJLoader();
	    p.setMaterials (materials);
	    p.setPath(OBJPATH);
	    p.load(OBJNAME, function (object)
	    {
	        theagent2 = object;
	        if (asynchFinished() ) initScene();
	    });
	});


 	// load textures
	var loader2 = new THREE.TextureLoader();

	loader2.load ( TEXTURE_LION, function ( thetexture )
	{
		thetexture.minFilter  = THREE.LinearFilter;
		lion_texture = thetexture;
		if ( asynchFinished() )	initScene();
	});
}


function asynchFinished()		 // all file loads returned
{
	if ( lion_texture && theenemy && theagent && theagent2)   return true;
	else return false;
}

function initScene()		// all file loads have returned
{
	// add agent model
	// start at center
 	theagent.position.y = 0;
	theagent.position.x = 0;
	theagent.position.z = 0;

	theagent2.position.y = 0;
	theagent2.position.x = 300;
	theagent2.position.z = 0;

	theagent.scale.multiplyScalar ( SCALE_RAT );    	  	// scale it
	ABWorld.scene.add( theagent );
	theagent2.scale.multiplyScalar ( SCALE_RAT);
	ABWorld.scene.add( theagent2 );

	// add enemies
	// first paint and size the canonical enemy object
	theenemy.traverse ( function ( child )
	{
		if ( child instanceof THREE.Mesh )
			child.material.map = lion_texture ;
	});

	theenemy.scale.multiplyScalar ( SCALE_LIONS );    	// scale it

	// add perhaps multiple enemy models, starting near outside of arena
	// LIONS for player 1
	for ( var i = 0; i < NO_LIONS_1 ; i++ )
	{
		var object = theenemy.clone();         // copy the object multiple times

		object.position.y = 0;
		object.position.x = AB.randomPick (		AB.randomIntAtoB ( -MAXPOS/2, -MAXPOS/3 ), 		AB.randomIntAtoB ( MAXPOS/3, MAXPOS/2 ) 		);
		object.position.z = AB.randomPick (		AB.randomIntAtoB ( -MAXPOS/2, -MAXPOS/3 ), 		AB.randomIntAtoB ( MAXPOS/3, MAXPOS/2 ) 		);

		ABWorld.scene.add( object );

		// save in array for later
		THELIONS1[i] = object;
	}

	// LIONS for player 2
	for ( var i = 0; i < NO_LIONS_2 ; i++ )
	{
		var object = theenemy.clone();         // copy the object multiple times

		object.position.y = 0;
		object.position.x = AB.randomPick (		AB.randomIntAtoB ( -MAXPOS/2, -MAXPOS/3 ), 		AB.randomIntAtoB ( MAXPOS/3, MAXPOS/2 ) 		);
		object.position.z = AB.randomPick (		AB.randomIntAtoB ( -MAXPOS/2, -MAXPOS/3 ), 		AB.randomIntAtoB ( MAXPOS/3, MAXPOS/2 ) 		);

		ABWorld.scene.add( object );

		// save in array for later
		THELIONS2[i] = object;
	}

 	// finally skybox
	ABWorld.scene.background = new THREE.CubeTextureLoader().load ( SKYBOX_ARRAY,	function()
	{
	ABWorld.render();
	AB.removeLoading();
	AB.runReady = true;
	});
}

// --- enemy move -----------------------------------

// angle between two points (x1,y1) and (x2,y2)
function angleTwoPoints ( x1, y1, x2, y2 )
{
	return  Math.atan2 ( x2 - x1,  y2 - y1  );
}

function dist ( a, b  )		    // distance between them when a,b are Three vectors
{
	return ( a.distanceTo ( b ) );
}

function collision ( proposedMove, k )  		 // proposed move FOR lion k
{
	if ( dist ( proposedMove, theagent.position ) < CLOSE_LIMIT )
	{
		p1lose = true; // if a lion touches player 1, they lose
		AB.abortRun = true;
	}

  for ( var i=0 ; i < NO_LIONS_1 ; i++ )
   if ( i != k )
    if ( dist ( proposedMove, THELIONS1[i].position ) < CLOSE_LIMIT )
 	 return true;

 // else
 return false;
}

function collision2 ( proposedMove, k )  		 // proposed move FOR lion k
{
	if ( dist ( proposedMove, theagent2.position ) < CLOSE_LIMIT )
	{
		p2lose = true; // if a lion touches player 2, they lose
		AB.abortRun = true;
	}

  for ( var i=0 ; i < NO_LIONS_2 ; i++ )
   if ( i != k )
    if ( dist ( proposedMove, THELIONS2[i].position ) < CLOSE_LIMIT )
 	 return true;

 // else
 return false;
}

function moveEnemy()
{
  for ( var i = 0; i < NO_LIONS_1 ; i++ )
  {
		var e = THELIONS1[i];

		// rotate enemy to face agent
		enemyRotation =  angleTwoPoints ( e.position.x, e.position.z, theagent.position.x, theagent.position.z  );

		e.rotation.set ( 0, enemyRotation, 0 );

		// move along that line, if no collision with any other model

		var  proposedMove = new THREE.Vector3
		(
			e.position.x + ( ENEMY_STEP *  Math.sin(enemyRotation) ),
			e.position.y,
			e.position.z + ( ENEMY_STEP *  Math.cos(enemyRotation) )
		);

		if ( ! collision ( proposedMove, i ) )
			e.position.copy ( proposedMove );
		// else enemy i just misses a turn
  }
}

function moveEnemy2()
{
  for ( var i = 0; i < NO_LIONS_2 ; i++ )
  {
        var e2 = THELIONS2[i];

		// rotate enemy to face agent
        enemyRotation2 =  angleTwoPoints ( e2.position.x, e2.position.z, theagent2.position.x, theagent2.position.z  );

        e2.rotation.set ( 0, enemyRotation2, 0 );

		// move along that line, if no collision with any other model

		var  proposedMove = new THREE.Vector3
		(
			e2.position.x + ( ENEMY_STEP *  Math.sin(enemyRotation2) ),
			e2.position.y,
			e2.position.z + ( ENEMY_STEP *  Math.cos(enemyRotation2) )
		);

		if ( ! collision2 ( proposedMove, i ) )
			e2.position.copy ( proposedMove );
		// else enemy i just misses a turn
  }
}

//--- key control of agent -------------------------------------------------------------
const KEY_UP    = 38;
const KEY_LEFT  = 37;
const KEY_RIGHT = 39;
const KEY_A     = 65;
const KEY_W     = 87;
const KEY_D     = 68;

var OURKEYS = [ KEY_UP, KEY_LEFT, KEY_RIGHT, KEY_A, KEY_W, KEY_D ];

function ourKeys(event) { // checks if a key pressed is either WASD or arrow keys
    return(OURKEYS.includes(event.keyCode));
}

// UP moves forward in whatever angle you are at
// LEFT/RIGHT rotate by small angle
function keyHandler ( event )
{
	if ( ! AB.runReady ) return true; 		// not ready yet

	// if not handling this key, send it to default:

	if ( ! ourKeys ( event ) ) return true;

	// else handle it and prevent default:

    // cases for each different key
    // sendData first sends to other worlds what to do, then does it locally using movePlayer
    if(event.keyCode == KEY_A) sendData(KEY_A, 2);
    if(event.keyCode == KEY_W) sendData(KEY_W, 2);
    if(event.keyCode == KEY_D) sendData(KEY_D, 2);

    if(event.keyCode == KEY_LEFT)
    {
        console.log("Player 1 is moving left!!!");
        sendData(KEY_LEFT, 1);
    }
    if(event.keyCode == KEY_UP) sendData(KEY_UP, 1);
    if(event.keyCode == KEY_RIGHT) sendData(KEY_RIGHT, 1);

	event.stopPropagation(); event.preventDefault(); return false;
}

AB.socketStart(); // start sockets
AB.socketIn = function(data) // incoming data on socket, i.e. key movements of other players
{
	action = data[0]
	player = data[1]
	movePlayer(action, player);
};

function sendData(action, player) // function to send out the move to do, then do the move locally
{
    data = [action, player];
    AB.socketOut(data);
    movePlayer(action, player);
};

function movePlayer(action, player)
{
    if (player == 1)
    {
        if ( action == KEY_UP ) 		  	// move a bit along angle we are facing
        {
        	theagent.position.x = theagent.position.x + ( AGENT_STEP *  Math.sin(agentRotation) );
        	theagent.position.z = theagent.position.z + ( AGENT_STEP *  Math.cos(agentRotation) );
        }

         if ( action == KEY_LEFT )   		// rotate in place
         {
        	agentRotation = agentRotation + ROTATE_AMOUNT;
        	theagent.rotation.set ( 0, agentRotation, 0 );
         }

         if ( action == KEY_RIGHT )
         {
        	agentRotation = agentRotation - ROTATE_AMOUNT;
        	theagent.rotation.set ( 0, agentRotation, 0 );
         }
    }
    else  // Player 2
    {
        if (action == KEY_W)
         {
             theagent2.position.x = theagent2.position.x + (AGENT_STEP * Math.sin(agentRotation2));
             theagent2.position.z = theagent2.position.z + (AGENT_STEP * Math.cos(agentRotation2));
         }

         if (action == KEY_A)
         {
            agentRotation2 = agentRotation2 + ROTATE_AMOUNT;
        	theagent2.rotation.set ( 0, agentRotation2, 0 );
         }

         if (action == KEY_D)
         {
            agentRotation2 = agentRotation2 - ROTATE_AMOUNT;
        	theagent2.rotation.set ( 0, agentRotation2, 0 );
         }
    }
}

AB.world.newRun = function()
{
	AB.loadingScreen();
	AB.runReady = false;
	ABWorld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR  );

	loadResources();		                 // aynch file loads, returns the function call initScene()

    var ambient = new THREE.AmbientLight();  // light
   	ABWorld.scene.add( ambient );
	var thelight = new THREE.DirectionalLight ( LIGHTCOLOR, -1 );
	thelight.position.set ( startRadiusConst, startRadiusConst, startRadiusConst );
	ABWorld.scene.add(thelight);

	document.onkeydown = keyHandler;
};

AB.world.nextStep = function()
{
	moveEnemy();   	// enemies moves on their own clock
    moveEnemy2();   // enemies moves on their own clock
};

AB.world.endRun = function()
{
  if ( AB.abortRun && p1lose)		AB.msg ( " <br> <font color=red> <B>   Player 1 was caught! Player 2 wins.  </B> </font>    ", 2  );
  else if ( AB.abortRun && p2lose)		AB.msg ( " <br> <font color=red> <B>   Player 2 was caught! Player 1 wins.  </B> </font>    ", 2  );
};