import* as THREE from '/api/threemodule/libs/three.module.js';import'/uploads/alexandru/cannon.js';
window.focus();// Capture keys right away (by default focus is on editor)/**
* This is a game where you have to stack boxes on top of each other.
* The game ends when a box falls down.
* You can play it in two modes:
* - autopilot: the game will play itself
* - manual: you can place a box by clicking on the screen
*/
let camera, scene, renderer;// ThreeJS globals
let world;// CannonJs world
let lastTime;// Last timestamp of animation
let stack;// Parts that stay solid on top of each other
let overhangs;// Overhanging parts that fall downconst boxHeight =1;// Height of each layerconst originalBoxSize =3;// Original width and height of a box
let autopilot =true;
let gameEnded;
let robotPrecision;// Determines how precise the game is on autopilot
let canStart =false//audio
let tap ='/uploads/alexandru/stackSound.wav';// Determines how precise the game is on autopilotfunction setRobotPrecision(){
robotPrecision =Math.random()*1-0.5;}
window.onbeforeunload =function(){AB.socket.destroy();}function init(){
gameEnded =false;
lastTime =0;
stack =[];
overhangs =[];
setRobotPrecision();
AB.socketStart();// Initialize CannonJS
world =new CANNON.World();
world.gravity.set(0,15,0);// Gravity pulls things up
world.broadphase =new CANNON.NaiveBroadphase();
world.solver.iterations =40;// Initialize ThreeJsconst aspect = window.innerWidth / window.innerHeight;const width =10;const height = width / aspect;// If you want to use perspective camera instead, uncomment these lines
camera =new THREE.PerspectiveCamera(45,// field of view
aspect,// aspect ratio1,// near plane100// far plane);
camera.position.set(4,4,4);
camera.lookAt(0,0,0);
scene =new THREE.Scene();// Foundation
addLayer(0,0, originalBoxSize, originalBoxSize);// First layer
addLayer(-10,0, originalBoxSize, originalBoxSize,"x");// Set up lightsconst ambientLight =new THREE.AmbientLight(0xffffff,0.6);
scene.add(ambientLight);const dirLight =new THREE.DirectionalLight(0xffffff,0.6);
dirLight.position.set(10,20,0);
scene.add(dirLight);// Set up renderer
renderer =new THREE.WebGLRenderer({ antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);}function startGame(){
autopilot =false;
gameEnded =false;
lastTime =0;
stack =[];
overhangs =[];
users =[];if(world){while(world.bodies.length >0){
world.remove(world.bodies[0]);}}if(scene){// Remove every Mesh from the scenewhile(scene.children.find((c)=> c.type =="Mesh")){const mesh = scene.children.find((c)=> c.type =="Mesh");
scene.remove(mesh);}// Foundation
addLayer(0,0, originalBoxSize, originalBoxSize);// First layer
addLayer(-10,0, originalBoxSize, originalBoxSize,"x");}if(camera){// Reset camera positions
camera.position.set(4,4,4);
camera.lookAt(0,0,0);}}function addLayer(x, z, width, depth, direction){const y = boxHeight * stack.length;// Add the new box one layer higherconst layer = generateBox(x, y, z, width, depth,false);
layer.direction = direction;
stack.push(layer);}function addOverhang(x, z, width, depth){const y = boxHeight *(stack.length -1);// Add the new box one the same layerconst overhang = generateBox(x, y, z, width, depth,true);
overhangs.push(overhang);}function generateBox(x, y, z, width, depth, falls){// ThreeJSconst geometry =new THREE.BoxGeometry(width, boxHeight, depth);const color =new THREE.Color(`hsl(${30+ stack.length *4},100%,50%)`);const material =new THREE.MeshLambertMaterial({ color });const mesh =new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
scene.add(mesh);// CannonJSconst shape =new CANNON.Box(new CANNON.Vec3(width /2, boxHeight /2, depth /2));
let mass = falls ?5:0;// If it shouldn't fall then setting the mass to zero will keep it stationary
mass *= width / originalBoxSize;// Reduce mass proportionately by size
mass *= depth / originalBoxSize;// Reduce mass proportionately by sizeconst body =new CANNON.Body({ mass, shape });
body.position.set(x, y, z);
world.addBody(body);return{
threejs: mesh,
cannonjs: body,
width,
depth
};}function cutBox(topLayer, overlap, size, delta){const direction = topLayer.direction;const newWidth = direction =="x"? overlap : topLayer.width;const newDepth = direction =="z"? overlap : topLayer.depth;// Update metadata
topLayer.width = newWidth;
topLayer.depth = newDepth;// Update ThreeJS model
topLayer.threejs.scale[direction]= overlap / size;
topLayer.threejs.position[direction]-= delta /2;// Update CannonJS model
topLayer.cannonjs.position[direction]-= delta /2;// Replace shape to a smaller one (in CannonJS you can't simply just scale a shape)const shape =new CANNON.Box(new CANNON.Vec3(newWidth /2, boxHeight /2, newDepth /2));
topLayer.cannonjs.shapes =[];
topLayer.cannonjs.addShape(shape);}
window.addEventListener("mousedown", eventHandler);
window.addEventListener("touchstart", eventHandler);
window.addEventListener("keydown",function(event){if(event.key ==" "){
event.preventDefault();
eventHandler();return;}if(event.key =="R"|| event.key =="r"){
event.preventDefault();
buttonStartGame();
AB.socketOut({ event:"restart"});return;}});function eventHandler(){if(autopilot) startGame();else splitBlockAndAddNextOneIfOverlaps();}function splitBlockAndAddNextOneIfOverlaps(){if(gameEnded)return;const topLayer = stack[stack.length -1];const previousLayer = stack[stack.length -2];const direction = topLayer.direction;const size = direction =="x"? topLayer.width : topLayer.depth;const delta =
topLayer.threejs.position[direction]-
previousLayer.threejs.position[direction];const overhangSize =Math.abs(delta);const overlap = size - overhangSize;if(overlap >0){
cutBox(topLayer, overlap, size, delta);// Overhangconst overhangShift =(overlap /2+ overhangSize /2)*Math.sign(delta);const overhangX =
direction =="x"? topLayer.threejs.position.x + overhangShift
: topLayer.threejs.position.x;const overhangZ =
direction =="z"? topLayer.threejs.position.z + overhangShift
: topLayer.threejs.position.z;const overhangWidth = direction =="x"? overhangSize : topLayer.width;const overhangDepth = direction =="z"? overhangSize : topLayer.depth;
addOverhang(overhangX, overhangZ, overhangWidth, overhangDepth);// Next layerconst nextX = direction =="x"? topLayer.threejs.position.x :-10;const nextZ = direction =="z"? topLayer.threejs.position.z :-10;const newWidth = topLayer.width;// New layer has the same size as the cut top layerconst newDepth = topLayer.depth;// New layer has the same size as the cut top layerconst nextDirection = direction =="x"?"z":"x";
addLayer(nextX, nextZ, newWidth, newDepth, nextDirection);}else{
missedTheSpot();
console.log(getScore());
AB.socketOut({ event:"gameFinished", score: getScore(), username: AB.myuserid });
console.log("gameFinished emit");}}function missedTheSpot(){const topLayer = stack[stack.length -2];// Turn to top layer into an overhang and let it fall down
addOverhang(
topLayer.threejs.position.x,
topLayer.threejs.position.z,
topLayer.width,
topLayer.depth
);
world.remove(topLayer.cannonjs);
scene.remove(topLayer.threejs);
gameEnded =true;}function animation(time){if(lastTime){const timePassed = time - lastTime;const speed =0.008;const topLayer = stack[stack.length -1];const previousLayer = stack[stack.length -2];// The top level box should move if the game has not ended AND// it's either NOT in autopilot or it is in autopilot and the box did not yet reach the robot positionconst boxShouldMove =!gameEnded &&(!autopilot ||(autopilot && topLayer.threejs.position[topLayer.direction]< previousLayer.threejs.position[topLayer.direction]+ robotPrecision));if(boxShouldMove){// Keep the position visible on UI and the position in the model in sync
topLayer.threejs.position[topLayer.direction]+= speed * timePassed;
topLayer.cannonjs.position[topLayer.direction]+= speed * timePassed;// If the box went beyond the stack then show up the fail screenif(topLayer.threejs.position[topLayer.direction]>10){
missedTheSpot();}}else{// If it shouldn't move then is it because the autopilot reached the correct position?// Because if so then next level is comingif(autopilot){
splitBlockAndAddNextOneIfOverlaps();
setRobotPrecision();}}// 4 is the initial camera heightif(camera.position.y < boxHeight *(stack.length -2)+4){
camera.position.y += speed * timePassed;}
updatePhysics(timePassed);
renderer.render(scene, camera);}
lastTime = time;}function updatePhysics(timePassed){
world.step(timePassed /1000);// Step the physics world// Copy coordinates from Cannon.js to Three.js
overhangs.forEach((element)=>{
element.threejs.position.copy(element.cannonjs.position);
element.threejs.quaternion.copy(element.cannonjs.quaternion);});}function getScore(){return(stack.length !=0? stack.length -2:0);}
window.addEventListener("resize",()=>{// Adjust camera
console.log("resize", window.innerWidth, window.innerHeight);const aspect = window.innerWidth / window.innerHeight;const width =10;const height = width / aspect;
camera.top = height /2;
camera.bottom = height /-2;// Reset renderer
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);});function buttonStartGame(){
autopilot =false;
AB.removeSplash();
startGame();}function restartButton(){
AB.removeSplash();
AB.socketOut({ event:"restart", username: AB.myuserid })
startGame();}function start(){
AB.newSplash();
autopilot =true;
init();
AB.splashClick(buttonStartGame)}
start();
let users =[];classUser{
constructor(username, score =0){this.username = username;this.score = getScore();}}
AB.socketUserlist =function(array){
array.forEach(elem =>{
users.push(newUser(elem[0]));});};function getUsers(){return users;}
AB.socketIn =function(data){
console.log("socketIn", data);if(data.event =="gameFinished"){
console.log("gameFinished received");
gameEnded =true;
console.log("scoreUpdate out");
AB.socketOut({ event:"scoreUpdate", score: getScore(), username: AB.myuserid });
users.push(newUser(data.username, data.score));
let text ="";
users.forEach(u =>{
text +=`<p>${u.username}'s score ${u.score}</p>`;});
AB.newSplash(`<h1>${data.username} just lost!</h1>${text}<p>Your score ${getScore()}</p><p>Press R to restart</p>`);
AB.splashClick(restartButton);
gameEnded =true;}if(data.event =="scoreUpdate"){
console.log("scoreUpdate received");
users.push(newUser(data.username, data.score));
let text ="";
users.forEach(u =>{
text +=`<p>${u.username}'s score ${u.score}</p>`;});
AB.newSplash(`${text}<p>Your score ${getScore()}</p><p>Press R to restart</p>`);
AB.splashClick(restartButton);}if(data.event =="restart"){
console.log("restart received");
AB.removeSplash();
startGame();}};//references:// https://schteppe.github.io/cannon.js/docs/// https://threejs.org/docs/#manual/en/introduction/Creating-a-scene// https://blog.devgenius.io/build-a-stack-game-using-three-js-ea4d09963a62// https://sbcode.net/threejs/physics-cannonjs/// https://codesandbox.io/examples/package/cannon// https://jeromeetienne.github.io/slides/howtomakeagame-nextgamefrontier-2014/// https://www.freecodecamp.org/news/create-a-squid-game-javascript-game-with-three-js/