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