diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..d31033e --- /dev/null +++ b/dist/index.html @@ -0,0 +1,33 @@ + + + + + CodePen - Tower Blocks + + + + + + + +
+
+
0
+
Click (or press the spacebar) to place the block
+
+

Game Over

+

You did great, you're the best.

+

Click or spacebar to start again

+
+
+
Start
+
+
+
+ + + + + + + diff --git a/dist/script.js b/dist/script.js new file mode 100644 index 0000000..77006dc --- /dev/null +++ b/dist/script.js @@ -0,0 +1,298 @@ +"use strict"; +console.clear(); +class Stage { + constructor() { + // container + this.render = function () { + this.renderer.render(this.scene, this.camera); + }; + this.add = function (elem) { + this.scene.add(elem); + }; + this.remove = function (elem) { + this.scene.remove(elem); + }; + this.container = document.getElementById('game'); + // renderer + this.renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: false + }); + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setClearColor('#D0CBC7', 1); + this.container.appendChild(this.renderer.domElement); + // scene + this.scene = new THREE.Scene(); + // camera + let aspect = window.innerWidth / window.innerHeight; + let d = 20; + this.camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, -100, 1000); + this.camera.position.x = 2; + this.camera.position.y = 2; + this.camera.position.z = 2; + this.camera.lookAt(new THREE.Vector3(0, 0, 0)); + //light + this.light = new THREE.DirectionalLight(0xffffff, 0.5); + this.light.position.set(0, 499, 0); + this.scene.add(this.light); + this.softLight = new THREE.AmbientLight(0xffffff, 0.4); + this.scene.add(this.softLight); + window.addEventListener('resize', () => this.onResize()); + this.onResize(); + } + setCamera(y, speed = 0.3) { + TweenLite.to(this.camera.position, speed, { y: y + 4, ease: Power1.easeInOut }); + TweenLite.to(this.camera.lookAt, speed, { y: y, ease: Power1.easeInOut }); + } + onResize() { + let viewSize = 30; + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.camera.left = window.innerWidth / -viewSize; + this.camera.right = window.innerWidth / viewSize; + this.camera.top = window.innerHeight / viewSize; + this.camera.bottom = window.innerHeight / -viewSize; + this.camera.updateProjectionMatrix(); + } +} +class Block { + constructor(block) { + // set size and position + this.STATES = { ACTIVE: 'active', STOPPED: 'stopped', MISSED: 'missed' }; + this.MOVE_AMOUNT = 12; + this.dimension = { width: 0, height: 0, depth: 0 }; + this.position = { x: 0, y: 0, z: 0 }; + this.targetBlock = block; + this.index = (this.targetBlock ? this.targetBlock.index : 0) + 1; + this.workingPlane = this.index % 2 ? 'x' : 'z'; + this.workingDimension = this.index % 2 ? 'width' : 'depth'; + // set the dimensions from the target block, or defaults. + this.dimension.width = this.targetBlock ? this.targetBlock.dimension.width : 10; + this.dimension.height = this.targetBlock ? this.targetBlock.dimension.height : 2; + this.dimension.depth = this.targetBlock ? this.targetBlock.dimension.depth : 10; + this.position.x = this.targetBlock ? this.targetBlock.position.x : 0; + this.position.y = this.dimension.height * this.index; + this.position.z = this.targetBlock ? this.targetBlock.position.z : 0; + this.colorOffset = this.targetBlock ? this.targetBlock.colorOffset : Math.round(Math.random() * 100); + // set color + if (!this.targetBlock) { + this.color = 0x333344; + } + else { + let offset = this.index + this.colorOffset; + var r = Math.sin(0.3 * offset) * 55 + 200; + var g = Math.sin(0.3 * offset + 2) * 55 + 200; + var b = Math.sin(0.3 * offset + 4) * 55 + 200; + this.color = new THREE.Color(r / 255, g / 255, b / 255); + } + // state + this.state = this.index > 1 ? this.STATES.ACTIVE : this.STATES.STOPPED; + // set direction + this.speed = -0.1 - (this.index * 0.005); + if (this.speed < -4) + this.speed = -4; + this.direction = this.speed; + // create block + let geometry = new THREE.BoxGeometry(this.dimension.width, this.dimension.height, this.dimension.depth); + geometry.applyMatrix(new THREE.Matrix4().makeTranslation(this.dimension.width / 2, this.dimension.height / 2, this.dimension.depth / 2)); + this.material = new THREE.MeshToonMaterial({ color: this.color, shading: THREE.FlatShading }); + this.mesh = new THREE.Mesh(geometry, this.material); + this.mesh.position.set(this.position.x, this.position.y + (this.state == this.STATES.ACTIVE ? 0 : 0), this.position.z); + if (this.state == this.STATES.ACTIVE) { + this.position[this.workingPlane] = Math.random() > 0.5 ? -this.MOVE_AMOUNT : this.MOVE_AMOUNT; + } + } + reverseDirection() { + this.direction = this.direction > 0 ? this.speed : Math.abs(this.speed); + } + place() { + this.state = this.STATES.STOPPED; + let overlap = this.targetBlock.dimension[this.workingDimension] - Math.abs(this.position[this.workingPlane] - this.targetBlock.position[this.workingPlane]); + let blocksToReturn = { + plane: this.workingPlane, + direction: this.direction + }; + if (this.dimension[this.workingDimension] - overlap < 0.3) { + overlap = this.dimension[this.workingDimension]; + blocksToReturn.bonus = true; + this.position.x = this.targetBlock.position.x; + this.position.z = this.targetBlock.position.z; + this.dimension.width = this.targetBlock.dimension.width; + this.dimension.depth = this.targetBlock.dimension.depth; + } + if (overlap > 0) { + let choppedDimensions = { width: this.dimension.width, height: this.dimension.height, depth: this.dimension.depth }; + choppedDimensions[this.workingDimension] -= overlap; + this.dimension[this.workingDimension] = overlap; + let placedGeometry = new THREE.BoxGeometry(this.dimension.width, this.dimension.height, this.dimension.depth); + placedGeometry.applyMatrix(new THREE.Matrix4().makeTranslation(this.dimension.width / 2, this.dimension.height / 2, this.dimension.depth / 2)); + let placedMesh = new THREE.Mesh(placedGeometry, this.material); + let choppedGeometry = new THREE.BoxGeometry(choppedDimensions.width, choppedDimensions.height, choppedDimensions.depth); + choppedGeometry.applyMatrix(new THREE.Matrix4().makeTranslation(choppedDimensions.width / 2, choppedDimensions.height / 2, choppedDimensions.depth / 2)); + let choppedMesh = new THREE.Mesh(choppedGeometry, this.material); + let choppedPosition = { + x: this.position.x, + y: this.position.y, + z: this.position.z + }; + if (this.position[this.workingPlane] < this.targetBlock.position[this.workingPlane]) { + this.position[this.workingPlane] = this.targetBlock.position[this.workingPlane]; + } + else { + choppedPosition[this.workingPlane] += overlap; + } + placedMesh.position.set(this.position.x, this.position.y, this.position.z); + choppedMesh.position.set(choppedPosition.x, choppedPosition.y, choppedPosition.z); + blocksToReturn.placed = placedMesh; + if (!blocksToReturn.bonus) + blocksToReturn.chopped = choppedMesh; + } + else { + this.state = this.STATES.MISSED; + } + this.dimension[this.workingDimension] = overlap; + return blocksToReturn; + } + tick() { + if (this.state == this.STATES.ACTIVE) { + let value = this.position[this.workingPlane]; + if (value > this.MOVE_AMOUNT || value < -this.MOVE_AMOUNT) + this.reverseDirection(); + this.position[this.workingPlane] += this.direction; + this.mesh.position[this.workingPlane] = this.position[this.workingPlane]; + } + } +} +class Game { + constructor() { + this.STATES = { + 'LOADING': 'loading', + 'PLAYING': 'playing', + 'READY': 'ready', + 'ENDED': 'ended', + 'RESETTING': 'resetting' + }; + this.blocks = []; + this.state = this.STATES.LOADING; + this.stage = new Stage(); + this.mainContainer = document.getElementById('container'); + this.scoreContainer = document.getElementById('score'); + this.startButton = document.getElementById('start-button'); + this.instructions = document.getElementById('instructions'); + this.scoreContainer.innerHTML = '0'; + this.newBlocks = new THREE.Group(); + this.placedBlocks = new THREE.Group(); + this.choppedBlocks = new THREE.Group(); + this.stage.add(this.newBlocks); + this.stage.add(this.placedBlocks); + this.stage.add(this.choppedBlocks); + this.addBlock(); + this.tick(); + this.updateState(this.STATES.READY); + document.addEventListener('keydown', e => { + if (e.keyCode == 32) + this.onAction(); + }); + document.addEventListener('click', e => { + this.onAction(); + }); + document.addEventListener('touchstart', e => { + e.preventDefault(); + // this.onAction(); + // ☝️ this triggers after click on android so you + // insta-lose, will figure it out later. + }); + } + updateState(newState) { + for (let key in this.STATES) + this.mainContainer.classList.remove(this.STATES[key]); + this.mainContainer.classList.add(newState); + this.state = newState; + } + onAction() { + switch (this.state) { + case this.STATES.READY: + this.startGame(); + break; + case this.STATES.PLAYING: + this.placeBlock(); + break; + case this.STATES.ENDED: + this.restartGame(); + break; + } + } + startGame() { + if (this.state != this.STATES.PLAYING) { + this.scoreContainer.innerHTML = '0'; + this.updateState(this.STATES.PLAYING); + this.addBlock(); + } + } + restartGame() { + this.updateState(this.STATES.RESETTING); + let oldBlocks = this.placedBlocks.children; + let removeSpeed = 0.2; + let delayAmount = 0.02; + for (let i = 0; i < oldBlocks.length; i++) { + TweenLite.to(oldBlocks[i].scale, removeSpeed, { x: 0, y: 0, z: 0, delay: (oldBlocks.length - i) * delayAmount, ease: Power1.easeIn, onComplete: () => this.placedBlocks.remove(oldBlocks[i]) }); + TweenLite.to(oldBlocks[i].rotation, removeSpeed, { y: 0.5, delay: (oldBlocks.length - i) * delayAmount, ease: Power1.easeIn }); + } + let cameraMoveSpeed = removeSpeed * 2 + (oldBlocks.length * delayAmount); + this.stage.setCamera(2, cameraMoveSpeed); + let countdown = { value: this.blocks.length - 1 }; + TweenLite.to(countdown, cameraMoveSpeed, { value: 0, onUpdate: () => { this.scoreContainer.innerHTML = String(Math.round(countdown.value)); } }); + this.blocks = this.blocks.slice(0, 1); + setTimeout(() => { + this.startGame(); + }, cameraMoveSpeed * 1000); + } + placeBlock() { + let currentBlock = this.blocks[this.blocks.length - 1]; + let newBlocks = currentBlock.place(); + this.newBlocks.remove(currentBlock.mesh); + if (newBlocks.placed) + this.placedBlocks.add(newBlocks.placed); + if (newBlocks.chopped) { + this.choppedBlocks.add(newBlocks.chopped); + let positionParams = { y: '-=30', ease: Power1.easeIn, onComplete: () => this.choppedBlocks.remove(newBlocks.chopped) }; + let rotateRandomness = 10; + let rotationParams = { + delay: 0.05, + x: newBlocks.plane == 'z' ? ((Math.random() * rotateRandomness) - (rotateRandomness / 2)) : 0.1, + z: newBlocks.plane == 'x' ? ((Math.random() * rotateRandomness) - (rotateRandomness / 2)) : 0.1, + y: Math.random() * 0.1, + }; + if (newBlocks.chopped.position[newBlocks.plane] > newBlocks.placed.position[newBlocks.plane]) { + positionParams[newBlocks.plane] = '+=' + (40 * Math.abs(newBlocks.direction)); + } + else { + positionParams[newBlocks.plane] = '-=' + (40 * Math.abs(newBlocks.direction)); + } + TweenLite.to(newBlocks.chopped.position, 1, positionParams); + TweenLite.to(newBlocks.chopped.rotation, 1, rotationParams); + } + this.addBlock(); + } + addBlock() { + let lastBlock = this.blocks[this.blocks.length - 1]; + if (lastBlock && lastBlock.state == lastBlock.STATES.MISSED) { + return this.endGame(); + } + this.scoreContainer.innerHTML = String(this.blocks.length - 1); + let newKidOnTheBlock = new Block(lastBlock); + this.newBlocks.add(newKidOnTheBlock.mesh); + this.blocks.push(newKidOnTheBlock); + this.stage.setCamera(this.blocks.length * 2); + if (this.blocks.length >= 5) + this.instructions.classList.add('hide'); + } + endGame() { + this.updateState(this.STATES.ENDED); + } + tick() { + this.blocks[this.blocks.length - 1].tick(); + this.stage.render(); + requestAnimationFrame(() => { this.tick(); }); + } +} +let game = new Game(); \ No newline at end of file diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 0000000..6b25cf1 --- /dev/null +++ b/dist/style.css @@ -0,0 +1,106 @@ +@import url("https://fonts.googleapis.com/css?family=Comfortaa"); +html, body { + margin: 0; + overflow: hidden; + height: 100%; + width: 100%; + position: relative; + font-family: "Comfortaa", cursive; +} + +#container { + width: 100%; + height: 100%; +} +#container #score { + position: absolute; + top: 20px; + width: 100%; + text-align: center; + font-size: 10vh; + transition: transform 0.5s ease; + color: #333344; + transform: translatey(-200px) scale(1); +} +#container #game { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +#container .game-over { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 85%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +#container .game-over * { + transition: opacity 0.5s ease, transform 0.5s ease; + opacity: 0; + transform: translatey(-50px); + color: #333344; +} +#container .game-over h2 { + margin: 0; + padding: 0; + font-size: 40px; +} +#container .game-ready { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; +} +#container .game-ready #start-button { + transition: opacity 0.5s ease, transform 0.5s ease; + opacity: 0; + transform: translatey(-50px); + border: 3px solid #333344; + padding: 10px 20px; + background-color: transparent; + color: #333344; + font-size: 30px; +} +#container #instructions { + position: absolute; + width: 100%; + top: 16vh; + left: 0; + text-align: center; + transition: opacity 0.5s ease, transform 0.5s ease; + opacity: 0; +} +#container #instructions.hide { + opacity: 0 !important; +} +#container.playing #score, #container.resetting #score { + transform: translatey(0px) scale(1); +} +#container.playing #instructions { + opacity: 1; +} +#container.ready .game-ready #start-button { + opacity: 1; + transform: translatey(0); +} +#container.ended #score { + transform: translatey(6vh) scale(1.5); +} +#container.ended .game-over * { + opacity: 1; + transform: translatey(0); +} +#container.ended .game-over p { + transition-delay: 0.3s; +} \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..0fe6067 --- /dev/null +++ b/src/index.html @@ -0,0 +1,16 @@ + + +
+
+
0
+
Click (or press the spacebar) to place the block
+
+

Game Over

+

You did great, you're the best.

+

Click or spacebar to start again

+
+
+
Start
+
+
+
diff --git a/src/script.ts b/src/script.ts new file mode 100644 index 0000000..e958c63 --- /dev/null +++ b/src/script.ts @@ -0,0 +1,457 @@ +console.clear(); + +interface BlockReturn +{ + placed?:any; + chopped?:any; + plane: 'x' | 'y' | 'z'; + direction: number; + bonus?: boolean; +} + +class Stage +{ + private container: any; + private camera: any; + private scene: any; + private renderer: any; + private light: any; + private softLight: any; + private group: any; + + constructor() + { + // container + + this.container = document.getElementById('game'); + + // renderer + + this.renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: false + }); + + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setClearColor('#D0CBC7', 1); + this.container.appendChild( this.renderer.domElement ); + + // scene + + this.scene = new THREE.Scene(); + + // camera + + let aspect = window.innerWidth / window.innerHeight; + let d = 20; + this.camera = new THREE.OrthographicCamera( - d * aspect, d * aspect, d, - d, -100, 1000); + this.camera.position.x = 2; + this.camera.position.y = 2; + this.camera.position.z = 2; + this.camera.lookAt(new THREE.Vector3(0, 0, 0)); + + //light + + this.light = new THREE.DirectionalLight(0xffffff, 0.5); + this.light.position.set(0, 499, 0); + this.scene.add(this.light); + + this.softLight = new THREE.AmbientLight( 0xffffff, 0.4 ); + this.scene.add(this.softLight) + + window.addEventListener('resize', () => this.onResize()); + this.onResize(); + } + + setCamera(y:number, speed:number = 0.3) + { + TweenLite.to(this.camera.position, speed, {y: y + 4, ease: Power1.easeInOut}); + TweenLite.to(this.camera.lookAt, speed, {y: y, ease: Power1.easeInOut}); + } + + onResize() + { + let viewSize = 30; + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.camera.left = window.innerWidth / - viewSize; + this.camera.right = window.innerWidth / viewSize; + this.camera.top = window.innerHeight / viewSize; + this.camera.bottom = window.innerHeight / - viewSize; + this.camera.updateProjectionMatrix(); + } + + render = function() + { + this.renderer.render(this.scene, this.camera); + } + + add = function(elem) + { + this.scene.add(elem); + } + + remove = function(elem) + { + this.scene.remove(elem); + } +} + +class Block +{ + const STATES = {ACTIVE: 'active', STOPPED: 'stopped', MISSED: 'missed'}; + const MOVE_AMOUNT = 12; + + dimension = { width: 0, height: 0, depth: 0} + position = {x: 0, y: 0, z: 0}; + + mesh:any; + state:string; + index:number; + speed:number; + direction:number; + colorOffset:number; + color:number; + material:any; + + workingPlane:string; + workingDimension:string; + + targetBlock:Block; + + constructor(block:Block) + { + // set size and position + + this.targetBlock = block; + + this.index = (this.targetBlock ? this.targetBlock.index : 0) + 1; + this.workingPlane = this.index % 2 ? 'x' : 'z'; + this.workingDimension = this.index % 2 ? 'width' : 'depth'; + + // set the dimensions from the target block, or defaults. + + this.dimension.width = this.targetBlock ? this.targetBlock.dimension.width : 10; + this.dimension.height = this.targetBlock ? this.targetBlock.dimension.height : 2; + this.dimension.depth = this.targetBlock ? this.targetBlock.dimension.depth : 10; + + this.position.x = this.targetBlock ? this.targetBlock.position.x : 0; + this.position.y = this.dimension.height * this.index; + this.position.z = this.targetBlock ? this.targetBlock.position.z : 0; + + this.colorOffset = this.targetBlock ? this.targetBlock.colorOffset : Math.round(Math.random() * 100); + + // set color + if(!this.targetBlock) + { + this.color = 0x333344; + } + else + { + let offset = this.index + this.colorOffset; + var r = Math.sin(0.3 * offset) * 55 + 200; + var g = Math.sin(0.3 * offset + 2) * 55 + 200; + var b = Math.sin(0.3 * offset + 4) * 55 + 200; + this.color = new THREE.Color( r / 255, g / 255, b / 255 ); + } + + // state + + this.state = this.index > 1 ? this.STATES.ACTIVE : this.STATES.STOPPED; + + // set direction + + this.speed = -0.1 - (this.index * 0.005); + if(this.speed < -4) this.speed = -4; + this.direction = this.speed; + + // create block + + let geometry = new THREE.BoxGeometry( this.dimension.width, this.dimension.height, this.dimension.depth); + geometry.applyMatrix( new THREE.Matrix4().makeTranslation(this.dimension.width/2, this.dimension.height/2, this.dimension.depth/2) ); + this.material = new THREE.MeshToonMaterial( {color: this.color, shading: THREE.FlatShading} ); + this.mesh = new THREE.Mesh( geometry, this.material ); + this.mesh.position.set(this.position.x, this.position.y + (this.state == this.STATES.ACTIVE ? 0 : 0), this.position.z); + + if(this.state == this.STATES.ACTIVE) + { + this.position[this.workingPlane] = Math.random() > 0.5 ? -this.MOVE_AMOUNT : this.MOVE_AMOUNT; + } + } + + reverseDirection() + { + this.direction = this.direction > 0 ? this.speed : Math.abs(this.speed); + } + + place():BlockReturn + { + this.state = this.STATES.STOPPED; + + let overlap = this.targetBlock.dimension[this.workingDimension] - Math.abs(this.position[this.workingPlane] - this.targetBlock.position[this.workingPlane]); + + let blocksToReturn:BlockReturn = { + plane: this.workingPlane, + direction: this.direction + }; + + if(this.dimension[this.workingDimension] - overlap < 0.3) + { + overlap = this.dimension[this.workingDimension]; + blocksToReturn.bonus = true; + this.position.x = this.targetBlock.position.x; + this.position.z = this.targetBlock.position.z; + this.dimension.width = this.targetBlock.dimension.width; + this.dimension.depth = this.targetBlock.dimension.depth; + } + + if(overlap > 0) + { + let choppedDimensions = { width: this.dimension.width, height: this.dimension.height, depth: this.dimension.depth }; + choppedDimensions[this.workingDimension] -= overlap; + this.dimension[this.workingDimension] = overlap; + + let placedGeometry = new THREE.BoxGeometry( this.dimension.width, this.dimension.height, this.dimension.depth); + placedGeometry.applyMatrix( new THREE.Matrix4().makeTranslation(this.dimension.width/2, this.dimension.height/2, this.dimension.depth/2) ); + let placedMesh = new THREE.Mesh( placedGeometry, this.material ); + + let choppedGeometry = new THREE.BoxGeometry( choppedDimensions.width, choppedDimensions.height, choppedDimensions.depth); + choppedGeometry.applyMatrix( new THREE.Matrix4().makeTranslation(choppedDimensions.width/2, choppedDimensions.height/2, choppedDimensions.depth/2) ); + let choppedMesh = new THREE.Mesh( choppedGeometry, this.material ); + + let choppedPosition = { + x: this.position.x, + y: this.position.y, + z: this.position.z + } + + if(this.position[this.workingPlane] < this.targetBlock.position[this.workingPlane]) + { + this.position[this.workingPlane] = this.targetBlock.position[this.workingPlane] + } + else + { + choppedPosition[this.workingPlane] += overlap; + } + + placedMesh.position.set(this.position.x, this.position.y, this.position.z); + choppedMesh.position.set(choppedPosition.x, choppedPosition.y, choppedPosition.z); + + blocksToReturn.placed = placedMesh; + if(!blocksToReturn.bonus) blocksToReturn.chopped = choppedMesh; + } + else + { + this.state = this.STATES.MISSED; + } + + this.dimension[this.workingDimension] = overlap; + + return blocksToReturn; + } + + tick() + { + if(this.state == this.STATES.ACTIVE) + { + let value = this.position[this.workingPlane]; + if(value > this.MOVE_AMOUNT || value < -this.MOVE_AMOUNT) this.reverseDirection(); + this.position[this.workingPlane] += this.direction; + this.mesh.position[this.workingPlane] = this.position[this.workingPlane]; + } + } +} + +class Game +{ + const STATES = { + 'LOADING': 'loading', + 'PLAYING': 'playing', + 'READY': 'ready', + 'ENDED': 'ended', + 'RESETTING': 'resetting' + } + blocks:Block[] = []; + state:string = this.STATES.LOADING; + + // groups + + newBlocks:any; + placedBlocks:any; + choppedBlocks:any; + + // UI elements + + scoreContainer:any; + mainContainer:any; + startButton:any; + instructions:any; + + constructor() + { + this.stage = new Stage(); + + this.mainContainer = document.getElementById('container'); + this.scoreContainer = document.getElementById('score'); + this.startButton = document.getElementById('start-button'); + this.instructions = document.getElementById('instructions'); + this.scoreContainer.innerHTML = '0'; + + this.newBlocks = new THREE.Group(); + this.placedBlocks = new THREE.Group(); + this.choppedBlocks = new THREE.Group(); + + this.stage.add(this.newBlocks); + this.stage.add(this.placedBlocks); + this.stage.add(this.choppedBlocks); + + this.addBlock(); + this.tick(); + + this.updateState(this.STATES.READY); + + document.addEventListener('keydown', e => + { + if(e.keyCode == 32) this.onAction() + }); + + document.addEventListener('click', e => + { + this.onAction(); + }); + + document.addEventListener('touchstart', e => + { + e.preventDefault(); + // this.onAction(); + + // ☝️ this triggers after click on android so you + // insta-lose, will figure it out later. + }); + } + + updateState(newState) + { + for(let key in this.STATES) this.mainContainer.classList.remove(this.STATES[key]); + this.mainContainer.classList.add(newState); + this.state = newState; + } + + onAction() + { + switch(this.state) + { + case this.STATES.READY: + this.startGame(); + break; + case this.STATES.PLAYING: + this.placeBlock(); + break; + case this.STATES.ENDED: + this.restartGame(); + break; + } + } + + startGame() + { + if(this.state != this.STATES.PLAYING) + { + this.scoreContainer.innerHTML = '0'; + this.updateState(this.STATES.PLAYING); + this.addBlock(); + } + } + + restartGame() + { + this.updateState(this.STATES.RESETTING); + + let oldBlocks = this.placedBlocks.children; + let removeSpeed = 0.2; + let delayAmount = 0.02; + for(let i = 0; i < oldBlocks.length; i++) + { + TweenLite.to(oldBlocks[i].scale, removeSpeed, {x: 0, y: 0, z: 0, delay: (oldBlocks.length - i) * delayAmount, ease: Power1.easeIn, onComplete: () => this.placedBlocks.remove(oldBlocks[i])}) + TweenLite.to(oldBlocks[i].rotation, removeSpeed, {y: 0.5, delay: (oldBlocks.length - i) * delayAmount, ease: Power1.easeIn}) + } + let cameraMoveSpeed = removeSpeed * 2 + (oldBlocks.length * delayAmount); + this.stage.setCamera(2, cameraMoveSpeed); + + let countdown = {value: this.blocks.length - 1}; + TweenLite.to(countdown, cameraMoveSpeed, {value: 0, onUpdate: () => {this.scoreContainer.innerHTML = String(Math.round(countdown.value))}}) + + this.blocks = this.blocks.slice(0, 1); + + setTimeout(() => { + this.startGame(); + }, cameraMoveSpeed * 1000) + + } + + placeBlock() + { + let currentBlock = this.blocks[this.blocks.length - 1]; + let newBlocks:BlockReturn = currentBlock.place(); + this.newBlocks.remove(currentBlock.mesh); + if(newBlocks.placed) this.placedBlocks.add(newBlocks.placed); + if(newBlocks.chopped) + { + this.choppedBlocks.add(newBlocks.chopped); + let positionParams = {y: '-=30', ease: Power1.easeIn, onComplete: () => this.choppedBlocks.remove(newBlocks.chopped)} + let rotateRandomness = 10; + let rotationParams = { + delay: 0.05, + x: newBlocks.plane == 'z' ? ((Math.random() * rotateRandomness) - (rotateRandomness/2)) : 0.1, + z: newBlocks.plane == 'x' ? ((Math.random() * rotateRandomness) - (rotateRandomness/2)) : 0.1, + y: Math.random() * 0.1, + }; + if(newBlocks.chopped.position[newBlocks.plane] > newBlocks.placed.position[newBlocks.plane]) + { + positionParams[newBlocks.plane] = '+=' + (40 * Math.abs(newBlocks.direction)); + } + else + { + positionParams[newBlocks.plane] = '-=' + (40 * Math.abs(newBlocks.direction)); + } + TweenLite.to(newBlocks.chopped.position, 1, positionParams); + TweenLite.to(newBlocks.chopped.rotation, 1, rotationParams); + + } + + this.addBlock(); + } + + addBlock() + { + let lastBlock = this.blocks[this.blocks.length - 1]; + + if(lastBlock && lastBlock.state == lastBlock.STATES.MISSED) + { + return this.endGame(); + } + + this.scoreContainer.innerHTML = String(this.blocks.length - 1); + + let newKidOnTheBlock = new Block(lastBlock); + this.newBlocks.add(newKidOnTheBlock.mesh); + this.blocks.push(newKidOnTheBlock); + + this.stage.setCamera(this.blocks.length * 2); + + if(this.blocks.length >= 5) this.instructions.classList.add('hide'); + } + + endGame() + { + this.updateState(this.STATES.ENDED); + } + + tick() + { + this.blocks[this.blocks.length - 1].tick(); + this.stage.render(); + requestAnimationFrame(() => {this.tick()}); + } +} + +let game = new Game(); \ No newline at end of file diff --git a/src/style.scss b/src/style.scss new file mode 100644 index 0000000..d2d8991 --- /dev/null +++ b/src/style.scss @@ -0,0 +1,167 @@ +@import url('https://fonts.googleapis.com/css?family=Comfortaa'); + +$color-dark: #333344; + +html, body +{ + margin: 0; + overflow: hidden; + height: 100%; + width: 100%; + position: relative; + font-family: 'Comfortaa', cursive; +} + +#container +{ + width: 100%; + height: 100%; + + #score + { + position: absolute; + top: 20px; + width: 100%; + text-align: center; + font-size: 10vh; + transition: transform 0.5s ease; + color: $color-dark; + transform: translatey(-200px) scale(1); + } + + #game + { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + .game-over + { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 85%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + * + { + transition: opacity 0.5s ease, transform 0.5s ease; + opacity: 0; + transform: translatey(-50px); + color: $color-dark; + } + + h2 + { + margin: 0; + padding: 0; + font-size: 40px; + } + } + + .game-ready + { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; + + #start-button + { + transition: opacity 0.5s ease, transform 0.5s ease; + opacity: 0; + transform: translatey(-50px); + + border: 3px solid $color-dark; + padding: 10px 20px; + background-color: transparent; + color: $color-dark; + font-size: 30px; + } + } + + #instructions + { + position: absolute; + width: 100%; + top: 16vh; + left: 0; + text-align: center; + transition: opacity 0.5s ease, transform 0.5s ease; + + opacity: 0; + + &.hide + { + opacity: 0 !important; + } + } + + &.playing, &.resetting + { + #score + { + transform: translatey(0px) scale(1); + } + } + + &.playing + { + #instructions + { + opacity: 1; + } + } + + &.ready + { + + + .game-ready + { + #start-button + { + opacity: 1; + transform: translatey(0); + } + } + } + + &.ended + { + #score + { + transform: translatey(6vh) scale(1.5); + } + + .game-over + { + * + { + opacity: 1; + transform: translatey(0); + } + + p + { + transition-delay: 0.3s; + } + } + } +} + + + +