Code viewer for World: Ch.5. Click on location.
 
// Over a few chapters, we are working up to a "Battleship" style Websocket game.

// Ch.5 
// Click on the opponent's grid gets a grid reference that we can send to the opponent as our guess.
// Test that we can click on any square to select it - no matter how we rotate the camera 

// In normal Battleships game, we often have numbered squares and call out a square by number 
// https://en.wikipedia.org/wiki/Battleship_(game)
// Here we will select squares by clicking not typing 
// No need to have numbers on them



// === start of code ===========================================================================================================

const gridsize 		= 10;							// changed grid size 	   
const squaresize 	= 100;							 

const MAXPOS 		= gridsize * squaresize;	  

const startRadiusConst	 	= MAXPOS * 2 ;		      
const maxRadiusConst 		= MAXPOS * 10 ;			   
	
const SKYCOLOR 		= 'lightyellow';			      


AB.maxSteps                 = 1000000;         
AB.drawRunControls          = false;
ABWorld.drawCameraControls  = false;        // remove some controls to change the camera mode 



// added a "fire" image

 const TEXTURE_WALL 	= '/uploads/chapters/stone.png' ;
 const TEXTURE_SHIP 	= '/uploads/chapters/ship.png' ;
 const TEXTURE_FIRE 	= '/uploads/chapters/fire.png' ;
 
// credit:
// https://commons.wikimedia.org/wiki/File:Square_stone_brick_Texture.jpg
// https://commons.wikimedia.org/wiki/File:Regina_Maris.JPG
// https://commons.wikimedia.org/wiki/File:FIRE_01.JPG

var wall_texture, ship_texture, fire_texture; 

var thesea;     // we will make a "sea" - a plane - and use this to tell what points we are clicking on 




	ABWorld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR  ); 

	loadTextures();		 	 
							
    
    
	
function loadTextures()		  // now has 3 textures to load 
{
	var loader1 = new THREE.TextureLoader();
	var loader2 = new THREE.TextureLoader();
	var loader3 = new THREE.TextureLoader();
		
	loader1.load ( TEXTURE_SHIP, function ( texture )  	 
	{
		ship_texture = texture;                  
		if ( ship_texture && wall_texture && fire_texture )	makeEverything();		// if all textures loaded from files  
	});	

	loader2.load ( TEXTURE_WALL, function ( texture )  		
	{
		wall_texture = texture;                  
		if ( ship_texture && wall_texture && fire_texture )	makeEverything();		  
	});	
	
	loader3.load ( TEXTURE_FIRE, function ( texture )  		
	{
		fire_texture = texture;                  
		if ( ship_texture && wall_texture && fire_texture )	makeEverything();		  
	});	
}




//--- two 2D grids  -------------------------------------------------------------------------------

function translateMy ( i, j )			 
// the absolute 3D position of an (i,j) square on my grid is:           
//  width x = (i * squaresize)      
// height y = 0                                
//  depth z = (j * squaresize) 
{
	var v = new THREE.Vector3();
	
	v.x = (i * squaresize) ;   		 
	v.y = 0 ;	
	v.z = (j * squaresize) ;   	
	
	return v;
}


function translateOpponent ( i, j )		
// put 2nd grid beside the 1st grid, offset on the x dimension 
{
	var v = new THREE.Vector3();

	v.x = (i * squaresize) + MAXPOS ;   		 
	v.y = 0 ;	
	v.z = (j * squaresize) ;   	

	return v;
}



//--- translate 3D point to a square in opponent's grid ----------------------------------
// this looks more complicated than it is!
// user will click a point in 3D space 
// we want to translate that point to the (i,j) square on the opponent's grid 
// return the "null" value if click is not in opponent's grid
// click on wall returns null 

// first some "homework" to work out the exact pixel dimensions of the grid
// if we exclude walls, then first square of the non-wall grid has these x dimensions:
// 1st square LHS edge:             x = MAXPOS +            (1 * squaresize) - squaresize/2
// 1st square centre:               x = MAXPOS +            (1 * squaresize)
// 1st square RHS edge:             x = MAXPOS +            (1 * squaresize) + squaresize/2
// ...
// (gridsize-2) square LHS edge:    x = MAXPOS + ((gridsize-2) * squaresize) - squaresize/2
// (gridsize-2) square centre:      x = MAXPOS + ((gridsize-2) * squaresize)
// (gridsize-2) square RHS edge:    x = MAXPOS + ((gridsize-2) * squaresize) + squaresize/2
// for the z dimension it is the same without MAXPOS added 
// therefore we have the following limits for x and z values:


const minOpponentX = MAXPOS +            (1 * squaresize) - squaresize/2;
const maxOpponentX = MAXPOS + ((gridsize-2) * squaresize) + squaresize/2;

const minOpponentZ =                     (1 * squaresize) - squaresize/2;
const maxOpponentZ =          ((gridsize-2) * squaresize) + squaresize/2;


function point2address ( p )
{
    if ( p.x < minOpponentX ) return null;
    if ( p.x > maxOpponentX ) return null;
    if ( p.z < minOpponentZ ) return null;
    if ( p.z > maxOpponentZ ) return null;
    
    // else we are within the non-wall part of the opponent's grid 
    var i = ( p.x - MAXPOS ) / squaresize;
    var j = p.z / squaresize;
    
    // these may give floating point numbers not integers, since we may not click exactly in the centre of the square
    // we can be slightly above or below the centre 
    // solved using Math.round
    // https://www.w3schools.com/jsref/jsref_round.asp
    
    i =  Math.round ( i );
    j =  Math.round ( j );
    return ( new THREE.Vector2 ( i, j ) );
}



	
function makeEverything()		 
{
    makeGrids();
    makeSea();
    makeShips();
}


function makeGrids()
{
	var i,j, shape, thecube,   position, lookat;
	shape    = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );			 
	 
	// make the two grids together:
	 for ( i = 0; i < gridsize ; i++ )           
	  for ( j = 0; j < gridsize ; j++ ) 
		if ( ( i==0 ) || ( i==gridsize-1 ) || ( j==0 ) || ( j==gridsize-1 ) )         
		{
			thecube = new THREE.Mesh( shape );
			thecube.material = new THREE.MeshBasicMaterial( { map: wall_texture } );
			position = translateMy(i,j);                
			thecube.position.copy ( position ); 		
			ABWorld.scene.add(thecube);
			
			thecube = new THREE.Mesh( shape );
			thecube.material = new THREE.MeshBasicMaterial( { map: wall_texture } );
			position = translateOpponent(i,j);                
			thecube.position.copy ( position ); 		
			ABWorld.scene.add(thecube);
		}

// with two grids, need to figure where to put camera, and what to point it at 
// point it at approximately middle of the two grids:

    lookat = translateMy ( gridsize,  gridsize/2  );

// experiment to find some camera position off to the bottom:
//  position = new THREE.Vector3 (x, y, z);
    
    position = new THREE.Vector3 ( MAXPOS,   MAXPOS * 1.2,   MAXPOS * 1.3 );

   ABWorld.cameraCustom ( position, lookat );       // set up customised camera at this position to look at this point 
}

 
function makeSea()      
// make a plane for the "sea"
// "thesea" is a global variable that we can access later 
{
  var shape = new THREE.PlaneGeometry ( MAXPOS * 2,  MAXPOS  );      // make a "sea" plane for 2 grids side by side   
  thesea = new THREE.Mesh ( shape );
  thesea.material    = new THREE.MeshBasicMaterial ( { color: 'lightblue', side: THREE.DoubleSide } );
  thesea.rotation.set ( Math.PI / 2, 0, 0 );        // rotate it 90 degrees (pi/2 radians) from default 
  
  // takes a bit of thinking to see where sea should go into position
  // "position" is centre of the sea
  // y value should not be 0, that is centre of the cubes, not bottom of the cubes 
  // so use y = - squaresize * 0.5
  // in fact graphics looks nicer if sea is slightly higher than the base
  
  thesea.position.set ( MAXPOS - squaresize/2,      - squaresize * 0.4 ,      MAXPOS/2 - squaresize/2 );  
  
  ABWorld.scene.add ( thesea );
}

 
function makeShips()    
// make randomised ships in my grid
{
	 var  p, i, j, shape, thecube, position;
	 shape = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );	
	 
	 for ( p = 1; p <=5 ; p++ )    
	 {
    	 thecube = new THREE.Mesh( shape );
     	 thecube.material =  new THREE.MeshBasicMaterial( { map: ship_texture } );
     	 i = AB.randomIntAtoB ( 1, gridsize-2 );    // positions 0 and (gridsize-1) are walls 
     	 j = AB.randomIntAtoB ( 1, gridsize-2 ); 
     	 position = translateMy ( i, j );        
     	 thecube.position.copy ( position ); 
    	 ABWorld.scene.add(thecube);
	 }
}
 


// Mouse click moves the camera by default.
// To make it do other jobs as well, we need to redefine the mouse handling functions.
// See the docs for this API. In the Editor, select Docs - This API
// Then see "Custom mouse and touch handling"


ABHandler.initMouseDrag = function ( x, y )  
{ 
    trySquare ( x, y );      
    
    // also call default camera control:
    ABHandler.initCameraDrag ( x, y );  
};


function trySquare ( x, y )
// click at this point - does it select a square?
// the process of translating 2D mouse click to 3D point is called "raycasting"
// https://threejs.org/docs/#api/en/core/Raycaster
// See API docs for the "hitsObject" and "hitsObjectPoint" functionality which is set up for you.
{
    // look at console to debug clicking in the wrong place 
    console.log ( "click at x,y = " + x + " " + y );
    
    // find intersection with the "sea" plane we made earlier 
    if ( ABWorld.hitsObject ( x, y, thesea ) )
    {
        var p = ABWorld.hitsObjectPoint ( x, y, thesea );  
        console.log ( "hits sea at x,y,z = " + p.x + " " + p.y + " " + p.z );
        
        // translate 3D point to an (i,j) address in opponent's grid
        // return null if not in opponent's grid:
        
        var a = point2address ( p );
        
        if ( a )
        {
            // write some HTML to the "run header"
            AB.msg ( "<h3 style='color:green'> select opponent square (" + a.x + "," + a.y + ") </h3>" );
            
            // make a square of fire at that location 
	        var shape = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );	
            var thecube = new THREE.Mesh( shape );
         	thecube.material =  new THREE.MeshBasicMaterial( { map: fire_texture } );
         	var position = translateOpponent ( a.x, a.y );        
         	thecube.position.copy ( position ); 
        	ABWorld.scene.add(thecube);
        }
    }
}


// === end of code ===========================================================================================================



 
// Exercises:
// Change the fire image.
// Make a much bigger sea that extends to the horizon.
// Make a "nuclear" click that sets fire to multiple squares at once.  
// Redefine other functions. You can redefine almost anything!
// Advanced exercise: Set it up for touch control (e.g. on mobile), not just for mouse click. Read the API docs for which function you need to redefine.
// Advanced exercise: Set up a game where you click on your own ships and set them on fire.
 
// Outcomes: Student can:
// Learn how "raycasting" can translate a click on a 2D screen into a point in 3D space.
// Learn how to use console.log and (on this site) AB.msg to output debug information.
// Learn how to assign a function to a new definition in JavaScript.
// If they do advanced exercise: Learn how to manage touch events in JavaScript.