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
+
+
+
+
+
+
+
+
+
+
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
+
+
+
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;
+ }
+ }
+ }
+}
+
+
+
+