diff --git a/app.js b/app.js index 8e0720f..c0953d4 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,6 @@ import * as THREE from 'three' import { scheduler } from './jsm/scheduler.js' -import { TRANSLATION, ROTATION, environnement, Matrix, HoldQueue, NextQueue } from './jsm/gamelogic.js' +import { TRANSLATION, ROTATION, environnement, Playfield, HoldQueue, NextQueue } from './jsm/gamelogic.js' import { Settings } from './jsm/Settings.js' import { Stats } from './jsm/Stats.js' import { TetraGUI } from './jsm/TetraGUI.js' @@ -31,13 +31,13 @@ let game = { holdQueue.remove(holdQueue.piece) holdQueue.piece = undefined if (nextQueue.pieces) nextQueue.pieces.forEach(piece => nextQueue.remove(piece)) - matrix.init() + playfield.init() - scene.remove(matrix.piece) - if (matrix.piece) matrix.remove(matrix.piece) - matrix.piece = null + scene.remove(playfield.piece) + if (playfield.piece) playfield.remove(playfield.piece) + playfield.piece = null scene.music.currentTime = 0 - matrix.visible = true + playfield.visible = true this.playing = true stats.clock.start() @@ -65,16 +65,16 @@ let game = { stats.clock.elapsedTime = stats.elapsedTime scene.music.play() - if (matrix.piece) scheduler.setInterval(game.fall, stats.fallPeriod) + if (playfield.piece) scheduler.setInterval(game.fall, stats.fallPeriod) else this.generate() }, generate: function(nextPiece=nextQueue.shift()) { nextPiece.lockDelay = stats.lockDelay - matrix.piece = nextPiece - matrix.piece.onLockDown = game.lockDown + playfield.piece = nextPiece + playfield.piece.onLockDown = game.lockDown - if (matrix.piece.canMove(TRANSLATION.NONE)) { + if (playfield.piece.canMove(TRANSLATION.NONE)) { scheduler.setInterval(game.fall, stats.fallPeriod) } else { game.over() // block out @@ -82,17 +82,17 @@ let game = { }, fall: function() { - matrix.piece.move(TRANSLATION.DOWN) + playfield.piece.move(TRANSLATION.DOWN) }, lockDown: function() { scheduler.clearTimeout(game.lockDown) scheduler.clearInterval(game.fall) - if (matrix.lock(matrix.piece)) { - let tSpin = matrix.piece.tSpin - let nbClearedLines = matrix.clearLines() - matrix.remove(matrix.piece) + if (playfield.lock(playfield.piece)) { + let tSpin = playfield.piece.tSpin + let nbClearedLines = playfield.clearLines() + playfield.remove(playfield.piece) if (settings.sfxVolume) { if (nbClearedLines == 4 || (tSpin && nbClearedLines)) { scene.tetrisSound.currentTime = 0 @@ -130,7 +130,7 @@ let game = { }, over: function() { - matrix.piece.locking = false + playfield.piece.locking = false document.onkeydown = null window.onblur = null @@ -151,16 +151,16 @@ let game = { /* Handle player inputs */ let playerActions = { - moveLeft: () => matrix.piece.move(TRANSLATION.LEFT), + moveLeft: () => playfield.piece.move(TRANSLATION.LEFT), - moveRight: () => matrix.piece.move(TRANSLATION.RIGHT), + moveRight: () => playfield.piece.move(TRANSLATION.RIGHT), - rotateCW: () => matrix.piece.rotate(ROTATION.CW), + rotateCW: () => playfield.piece.rotate(ROTATION.CW), - rotateCCW: () => matrix.piece.rotate(ROTATION.CCW), + rotateCCW: () => playfield.piece.rotate(ROTATION.CCW), softDrop: function () { - if (matrix.piece.move(TRANSLATION.DOWN)) stats.score++ + if (playfield.piece.move(TRANSLATION.DOWN)) stats.score++ }, hardDrop: function () { @@ -170,19 +170,19 @@ let playerActions = { scene.hardDropSound.currentTime = 0 scene.hardDropSound.play() } - while (matrix.piece.move(TRANSLATION.DOWN)) stats.score += 2 + while (playfield.piece.move(TRANSLATION.DOWN)) stats.score += 2 game.lockDown() - matrix.hardDropAnimation.reset() - matrix.hardDropAnimation.play() + playfield.hardDropAnimation.reset() + playfield.hardDropAnimation.play() }, hold: function () { - if (matrix.piece.holdEnabled) { + if (playfield.piece.holdEnabled) { scheduler.clearInterval(game.fall) scheduler.clearTimeout(game.lockDown) let heldpiece = holdQueue.piece - holdQueue.piece = matrix.piece + holdQueue.piece = playfield.piece game.generate(heldpiece) } }, @@ -290,8 +290,8 @@ const clock = new THREE.Clock() const holdQueue = new HoldQueue() scene.add(holdQueue) -const matrix = new Matrix() -scene.add(matrix) +const playfield = new Playfield() +scene.add(playfield) const nextQueue = new NextQueue() scene.add(nextQueue) @@ -306,7 +306,7 @@ function animate() { const delta = clock.getDelta() scene.update(delta) - matrix.update(delta) + playfield.update(delta) controls.update() gui.update() diff --git a/jsm/TetraGUI.js b/jsm/TetraGUI.js index 47475c2..b41e13f 100644 --- a/jsm/TetraGUI.js +++ b/jsm/TetraGUI.js @@ -1,10 +1,10 @@ import * as THREE from 'three' import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import * as FPS from 'three/addons/libs/stats.module.js' -import { COLORS, environnement, I, J, L, O, S, T, Z } from './gamelogic.js' +import { COLORS, environnement, minoMaterial, I, J, L, O, S, T, Z, Tetromino } from './gamelogic.js' -class TetraGUI extends GUI { +export class TetraGUI extends GUI { constructor(game, settings, stats, scene) { super({title: "teTra"}) this.domElement.tabIndex = 1 @@ -135,6 +135,11 @@ class TetraGUI extends GUI { directionalLightPosition.add(scene.directionalLight.position, "x") directionalLightPosition.add(scene.directionalLight.position, "y") directionalLightPosition.add(scene.directionalLight.position, "z") + + let directionalLightTargetPosition = this.debug.addFolder("directionalLight.target").close() + directionalLightTargetPosition.add(scene.directionalLight.target.position, "x") + directionalLightTargetPosition.add(scene.directionalLight.target.position, "y") + directionalLightTargetPosition.add(scene.directionalLight.target.position, "z") let light = this.debug.addFolder("lights intensity").close() light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20) @@ -144,125 +149,18 @@ class TetraGUI extends GUI { vortex.add(scene.vortex.darkCylinder.material, "opacity").name("dark").min(0).max(1) vortex.add(scene.vortex.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1) - let materialParams = { - type: "MeshBasicMaterial", - opacity: 0.95, - reflectivity: 0.8, - roughness: 0.1, - metalness: 0.5, - attenuationDistance: 0.5, - ior: 2, - sheen: 0, - sheenRoughness: 1, - specularIntensity: 1, - thickness: 5, - transmission: 1, - } let material = this.debug.addFolder("minoes material").close() - let type = material.add(materialParams, "type", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]) - let opacity = material.add(materialParams, "opacity").min(0).max(1) - let reflectivity = material.add(materialParams, "reflectivity").min(0).max(1) - let roughness = material.add(materialParams, "roughness").min(0).max(1).hide() - let metalness = material.add(materialParams, "metalness").min(0).max(1).hide() - let attenuationDistance = material.add(materialParams, "attenuationDistance").min(0).max(1).hide() - let ior = material.add(materialParams, "ior").min(1).max(2).hide() - let sheen = material.add(materialParams, "sheen").min(0).max(1).hide() - let sheenRoughness = material.add(materialParams, "sheenRoughness").min(0).max(1).hide() - let specularIntensity = material.add(materialParams, "specularIntensity").min(0).max(1).hide() - let thickness = material.add(materialParams, "thickness").min(0).max(5).hide() - let transmission = material.add(materialParams, "transmission").min(0).max(1).hide() - type.onChange(type => { - switch(type) { - case "MeshBasicMaterial": - reflectivity.show() - roughness.hide() - metalness.hide() - attenuationDistance.hide() - ior.hide() - sheen.hide() - sheenRoughness.hide() - specularIntensity.hide() - thickness.hide() - transmission.hide() - break - case "MeshStandardMaterial": - reflectivity.hide() - roughness.show() - metalness.show() - attenuationDistance.hide() - ior.hide() - sheen.hide() - sheenRoughness.hide() - specularIntensity.hide() - thickness.hide() - transmission.hide() - break - case "MeshPhysicalMaterial": - reflectivity.hide() - roughness.show() - metalness.show() - attenuationDistance.show() - ior.show() - sheen.show() - sheenRoughness.show() - specularIntensity.show() - thickness.show() - transmission.show() - break - } - }) - material.onChange(() => { - let minoMaterialFactory - switch(materialParams.type) { - case "MeshBasicMaterial": - minoMaterialFactory = color => new THREE.MeshBasicMaterial({ - color : color, - envMap : environnement, - reflectivity: materialParams.reflectivity, - transparent : true, - opacity : materialParams.opacity, - side : THREE.DoubleSide, - }) - break - case "MeshStandardMaterial": - minoMaterialFactory = color => new THREE.MeshStandardMaterial({ - color : color, - envMap : environnement, - transparent: true, - opacity : materialParams.opacity, - side : THREE.DoubleSide, - roughness : materialParams.roughness, - metalness : materialParams.metalness, - }) - break - case "MeshPhysicalMaterial": - minoMaterialFactory = color => new THREE.MeshPhysicalMaterial({ - color : "white", - envMap : environnement, - transparent : true, - opacity : materialParams.opacity, - side : THREE.DoubleSide, - roughness : materialParams.roughness, - metalness : materialParams.metalness, - attenuationColor : color, - attenuationDistance: materialParams.attenuationDistance, - ior : materialParams.ior, - sheen : materialParams.sheen, - sheenRoughness : materialParams.sheenRoughness, - specularIntensity : materialParams.specularIntensity, - thickness : materialParams.thickness, - transmission : materialParams.transmission, - }) - break - } - I.prototype.material = minoMaterialFactory(COLORS.I) - J.prototype.material = minoMaterialFactory(COLORS.J) - L.prototype.material = minoMaterialFactory(COLORS.L) - O.prototype.material = minoMaterialFactory(COLORS.O) - S.prototype.material = minoMaterialFactory(COLORS.S) - T.prototype.material = minoMaterialFactory(COLORS.T) - Z.prototype.material = minoMaterialFactory(COLORS.Z) - }) + material.add(minoMaterial, "opacity").min(0).max(1) + //material.add(minoMaterial, "reflectivity").min(0).max(1) + material.add(minoMaterial, "roughness").min(0).max(1) + material.add(minoMaterial, "metalness").min(0).max(1) + //material.add(minoMaterial, "attenuationDistance").min(0).max(1).hide() + //material.add(minoMaterial, "ior").min(1).max(2).hide() + //material.add(minoMaterial, "sheen").min(0).max(1).hide() + //material.add(minoMaterial, "sheenRoughness").min(0).max(1).hide() + //material.add(minoMaterial, "specularIntensity").min(0).max(1).hide() + //material.add(minoMaterial, "thickness").min(0).max(5).hide() + //material.add(minoMaterial, "transmission").min(0).max(1).hide() this.fps = new FPS.default() document.body.appendChild(this.fps.dom) @@ -294,7 +192,4 @@ class TetraGUI extends GUI { update() { this.fps?.update() } -} - - -export { TetraGUI } \ No newline at end of file +} \ No newline at end of file diff --git a/jsm/TetraScene.js b/jsm/TetraScene.js index 10ddea1..c85cf14 100644 --- a/jsm/TetraScene.js +++ b/jsm/TetraScene.js @@ -2,7 +2,7 @@ import * as THREE from 'three' import { Vortex } from './Vortex.js' -class TetraScene extends THREE.Scene { +export class TetraScene extends THREE.Scene { constructor(loadingManager, settings) { super() @@ -12,12 +12,15 @@ class TetraScene extends THREE.Scene { this.vortex = new Vortex(loadingManager) this.add(this.vortex) - this.ambientLight = new THREE.AmbientLight(0xffffff, 1) + this.ambientLight = new THREE.AmbientLight(0xffffff, .5) this.add(this.ambientLight) - this.directionalLight = new THREE.DirectionalLight(0xffffff, 20) - this.directionalLight.position.set(5, -100, -20) + this.directionalLight = new THREE.DirectionalLight(0xffffff, 6) + this.directionalLight.position.set(5, -100, 0) this.add(this.directionalLight) + this.directionalLight.target = new THREE.Object3D() + this.directionalLight.target.position.set(5, -50, 20) + this.add(this.directionalLight.target) /* Sounds */ @@ -53,7 +56,4 @@ class TetraScene extends THREE.Scene { update(delta) { this.vortex.update(delta) } -} - - -export { TetraScene } \ No newline at end of file +} \ No newline at end of file diff --git a/jsm/gamelogic.js b/jsm/gamelogic.js index 823145f..0efe469 100644 --- a/jsm/gamelogic.js +++ b/jsm/gamelogic.js @@ -16,6 +16,8 @@ const COLORS = { S: 0xC8FBA8, T: 0xedb2ff, Z: 0xffb8c5, + LOCKING: "white", + GHOST: "white", } const TRANSLATION = { @@ -52,20 +54,6 @@ environnement.camera = new THREE.CubeCamera(1, 1000, envRenderTarget) environnement.camera.position.set(5, 10) -class Mino extends THREE.Mesh { - constructor() { - super(Mino.prototype.geometry) - this.velocity = P(50 - 100 * Math.random(), 50 - 100 * Math.random(), 50 - 100 * Math.random()) - this.rotationAngle = P(Math.random(), Math.random(), Math.random()).normalize() - this.angularVelocity = 5 - 10 * Math.random() - } - - update(delta) { - this.velocity.y += delta * GRAVITY - this.position.addScaledVector(this.velocity, delta) - this.rotateOnWorldAxis(this.rotationAngle, delta * this.angularVelocity) - } -} const minoFaceShape = new THREE.Shape() minoFaceShape.moveTo(.1, .1) minoFaceShape.lineTo(.1, .9) @@ -81,67 +69,46 @@ const minoExtrudeSettings = { bevelOffset: 0, bevelSegments: 1 } -Mino.prototype.geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings) +let minoGeometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings) + +let minoMaterial = new THREE.MeshStandardMaterial({ + envMap: environnement, + side: THREE.DoubleSide, + transparent: true, + opacity: 0.8, + //reflectivity: 0.8, + roughness: 0.1, + metalness: 0.9, + //attenuationDistance: 0.5, + //ior: 2, + //sheen: 0, + //sheenRoughness: 1, + //specularIntensity: 1, + //thickness: 5, + //transmission: 1, +}) -class MinoMaterial extends THREE.MeshBasicMaterial { - constructor(color) { - super({ - color: color, - envMap: environnement, - reflectivity: 0.9, - transparent: true, - opacity: 0.8, - side: THREE.DoubleSide, - }) - } -} - -class GhostMaterial extends THREE.MeshBasicMaterial { - constructor(color) { - super({ - color: color, - envMap: environnement, - reflectivity: 0.9, - transparent: true, - opacity: 0.15, - side: THREE.DoubleSide, - }) - } -} - - -class AbstractTetromino extends THREE.Group { - constructor() { +class Mino extends THREE.Object3D { + constructor(color, x, y, z=0) { super() - for (let i = 0; i < 4; i++) { - this.add(new Mino()) - } + this.color = color + this.position.set(x, y, z) + this.velocity = P(50 - 100 * Math.random(), 50 - 100 * Math.random(), 50 - 100 * Math.random()) + this.rotationAngle = P(Math.random(), Math.random(), Math.random()).normalize() + this.angularVelocity = 5 - 10 * Math.random() } - set facing(facing) { - this._facing = facing - this.minoesPosition[this.facing].forEach( - (position, i) => this.children[i].position.copy(position) - ) - } - - get facing() { - return this._facing - } - - canMove(translation, facing=this.facing) { - let testPosition = this.position.clone().add(translation) - return this.minoesPosition[facing].every(minoPosition => this.parent.cellIsEmpty(minoPosition.clone().add(testPosition))) + update(delta) { + this.velocity.y += delta * GRAVITY + this.position.addScaledVector(this.velocity, delta) + this.rotateOnWorldAxis(this.rotationAngle, delta * this.angularVelocity) + this.updateMatrix() } } -class Ghost extends AbstractTetromino {} -Ghost.prototype.minoesPosition = [ - [P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)], -] -class Tetromino extends AbstractTetromino { +class Tetromino extends THREE.InstancedMesh { static randomBag = [] static get random() { if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z] @@ -149,14 +116,49 @@ class Tetromino extends AbstractTetromino { } constructor() { - super() + super(minoGeometry, undefined, 4) + this.material = this.minoMaterial + this.facing = FACING.NORTH this.rotatedLast = false this.rotationPoint4Used = false this.holdEnabled = true - this.facing = 0 this.locking = false } + set facing(facing) { + this._facing = facing + let matrix4 = new THREE.Matrix4() + this.minoesPosition[this.facing].forEach((position, i) => { + matrix4.setPosition(position) + this.setMatrixAt(i, matrix4) + }) + this.instanceMatrix.needsUpdate = true + } + + get facing() { + return this._facing + } + + set locking(locking) { + if (locking) { + this.color = this.lockingColor + } else { + this.color = this.freeColor + } + } + + set color(color) { + for (let i = 0; i < this.count; i++) { + this.setColorAt(i, color) + } + this.instanceColor.needsUpdate = true + } + + canMove(translation, facing=this.facing) { + let testPosition = this.position.clone().add(translation) + return this.minoesPosition[facing].every(minoPosition => this.parent.cellIsEmpty(minoPosition.clone().add(testPosition))) + } + move(translation, rotatedFacing, rotationPoint) { if (this.canMove(translation, rotatedFacing)) { this.position.add(translation) @@ -167,14 +169,16 @@ class Tetromino extends AbstractTetromino { } if (this.canMove(TRANSLATION.DOWN)) { this.locking = false + this.parent.ghost.visible = true + this.parent.ghost.copy(this) scheduler.clearTimeout(this.onLockDown) } else { scheduler.resetTimeout(this.onLockDown, this.lockDelay) this.locking = true + this.parent.ghost.visible = false } - if (this.ghost.visible) this.updateGhost() return true - } else { + } else if (translation == TRANSLATION.DOWN) { this.locked = true if (!scheduler.timeoutTasks.has(this.onLockDown)) scheduler.setTimeout(this.onLockDown, this.lockDelay) @@ -189,27 +193,19 @@ class Tetromino extends AbstractTetromino { ) } - set locking(locking) { - if (locking) { - this.children.forEach(mino => mino.material = this.lockedMaterial) - this.ghost.visible = false - } else { - this.children.forEach(mino => mino.material = this.material) - this.ghost.visible = true - } - } - - updateGhost() { - this.ghost.position.copy(this.position) - this.ghost.minoesPosition = this.minoesPosition - this.ghost.facing = this.facing - while (this.ghost.canMove(TRANSLATION.DOWN)) this.ghost.position.y-- - } - get tSpin() { return T_SPIN.NONE } + + copy(piece) { + this.position.copy(piece.position) + this.minoesPosition = piece.minoesPosition + this.facing = piece.facing + while (this.canMove(TRANSLATION.DOWN)) this.position.y-- + } } +Tetromino.prototype.minoMaterial = minoMaterial +Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING) // Super Rotation System // freedom of movement = srs[this.parent.piece.facing][rotation] Tetromino.prototype.srs = [ @@ -218,9 +214,21 @@ Tetromino.prototype.srs = [ { [ROTATION.CW]: [P(0, 0), P(1, 0), P(1, 1), P(0, -2), P(1, -2)], [ROTATION.CCW]: [P(0, 0), P(-1, 0), P(-1, 1), P(0, -2), P(-1, -2)] }, { [ROTATION.CW]: [P(0, 0), P(-1, 0), P(-1, -1), P(0, 2), P(-1, 2)], [ROTATION.CCW]: [P(0, 0), P(-1, 0), P(-1, -1), P(0, 2), P(-1, 2)] }, ] -Tetromino.prototype.lockedMaterial = new MinoMaterial(0xffffff) Tetromino.prototype.lockDelay = 500 -Tetromino.prototype.ghost = new Ghost() + + +class Ghost extends Tetromino {} +Ghost.prototype.minoMaterial = new THREE.MeshBasicMaterial({ + envMap: environnement, + reflectivity: 0.9, + transparent: true, + opacity: 0.15, + side: THREE.DoubleSide, +}) +Ghost.prototype.freeColor = new THREE.Color(COLORS.GHOST) +Ghost.prototype.minoesPosition = [ + [P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)], +] class I extends Tetromino { } @@ -236,8 +244,7 @@ I.prototype.srs = [ { [ROTATION.CW]: [P(0, 0), P(2, 0), P(-1, 0), P(2, 1), P(-1, -2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(-2, 0), P(1, -2), P(-2, 1)] }, { [ROTATION.CW]: [P(0, 0), P(1, 0), P(-2, 0), P(1, -2), P(-2, 1)], [ROTATION.CCW]: [P(0, 0), P(-2, 0), P(1, 0), P(-2, -1), P(1, 2)] }, ] -I.prototype.material = new MinoMaterial(COLORS.I) -I.prototype.ghostMaterial = new GhostMaterial(COLORS.I) +I.prototype.freeColor = new THREE.Color(COLORS.I) class J extends Tetromino { } J.prototype.minoesPosition = [ @@ -246,8 +253,7 @@ J.prototype.minoesPosition = [ [P(1, -1), P(-1, 0), P(0, 0), P(1, 0)], [P(0, 1), P(-1, -1), P(0, 0), P(0, -1)], ] -J.prototype.material = new MinoMaterial(COLORS.J) -J.prototype.ghostMaterial = new GhostMaterial(COLORS.J) +J.prototype.freeColor = new THREE.Color(COLORS.J) class L extends Tetromino { } L.prototype.minoesPosition = [ @@ -256,8 +262,7 @@ L.prototype.minoesPosition = [ [P(-1, 0), P(0, 0), P(1, 0), P(-1, -1)], [P(0, 1), P(0, 0), P(0, -1), P(-1, 1)], ] -L.prototype.material = new MinoMaterial(COLORS.L) -L.prototype.ghostMaterial = new GhostMaterial(COLORS.L) +L.prototype.freeColor = new THREE.Color(COLORS.L) class O extends Tetromino { } O.prototype.minoesPosition = [ @@ -266,8 +271,7 @@ O.prototype.minoesPosition = [ O.prototype.srs = [ { [ROTATION.CW]: [], [ROTATION.CCW]: [] } ] -O.prototype.material = new MinoMaterial(COLORS.O) -O.prototype.ghostMaterial = new GhostMaterial(COLORS.O) +O.prototype.freeColor = new THREE.Color(COLORS.O) class S extends Tetromino { } S.prototype.minoesPosition = [ @@ -276,8 +280,7 @@ S.prototype.minoesPosition = [ [P(-1, -1), P(0, 0), P(1, 0), P(0, -1)], [P(-1, 1), P(0, 0), P(-1, 0), P(0, -1)], ] -S.prototype.material = new MinoMaterial(COLORS.S) -S.prototype.ghostMaterial = new GhostMaterial(COLORS.S) +S.prototype.freeColor = new THREE.Color(COLORS.S) class T extends Tetromino { get tSpin() { @@ -304,8 +307,7 @@ T.prototype.tSlots = [ [P(1, -1), P(-1, -1), P(-1, 1), P(1, 1)], [P(-1, -1), P(-1, 1), P(1, 1), P(1, -1)], ] -T.prototype.material = new MinoMaterial(COLORS.T) -T.prototype.ghostMaterial = new GhostMaterial(COLORS.T) +T.prototype.freeColor = new THREE.Color(COLORS.T) class Z extends Tetromino { } Z.prototype.minoesPosition = [ @@ -314,8 +316,7 @@ Z.prototype.minoesPosition = [ [P(-1, 0), P(0, 0), P(0, -1), P(1, -1)], [P(0, 1), P(-1, 0), P(0, 0), P(-1, -1)] ] -Z.prototype.material = new MinoMaterial(COLORS.Z) -Z.prototype.ghostMaterial = new GhostMaterial(COLORS.Z) +Z.prototype.freeColor = new THREE.Color(COLORS.Z) const ROWS = 24 @@ -323,7 +324,7 @@ const SKYLINE = 20 const COLUMNS = 10 -class Matrix extends THREE.Group { +class Playfield extends THREE.Group { constructor() { super() this.visible = false @@ -364,13 +365,24 @@ class Matrix extends THREE.Group { this.hardDropAnimation.loop = THREE.LoopOnce this.hardDropAnimation.setDuration(0.2) + this.ghost = new Ghost() + this.add(this.ghost) + this.ghost.visible = false + + this.lockedMeshes = new THREE.InstancedMesh(minoGeometry, minoMaterial, 200) + this.add(this.lockedMeshes) + + this.freedMinoes = [] + this.freedMeshes = new THREE.InstancedMesh(minoGeometry, minoMaterial, 200) + this.freedMeshes.count = 0 + this.add(this.freedMeshes) + this.init() } init() { - while(this.children.length > 1 ) this.remove(this.children[1]) this.cells = Array(ROWS).fill().map(() => Array(COLUMNS)) - this.unlockedMinoes = new Set() + this.lockedMeshes.count = 0 } cellIsEmpty(p) { @@ -383,11 +395,9 @@ class Matrix extends THREE.Group { if (piece) { this.add(piece) piece.position.set(4, SKYLINE) - this.add(piece.ghost) - piece.ghost.children.forEach((mino) => { - mino.material = piece.ghostMaterial - }) - piece.updateGhost() + this.ghost.color = piece.freeColor + this.ghost.copy(piece) + this.ghost.visible = true } this._piece = piece } @@ -397,50 +407,66 @@ class Matrix extends THREE.Group { } lock() { - this.piece.locking = false - let minoes = Array.from(this.piece.children) - minoes.forEach(mino => { - mino.position.add(this.piece.position) - this.add(mino) - if (this.cellIsEmpty(mino.position)) { - this.cells[mino.position.y][mino.position.x] = mino + this.piece.minoesPosition[this.piece.facing].forEach(position => { + position = position.clone() + position.add(this.piece.position) + if (this.cellIsEmpty(position)) { + this.cells[position.y][position.x] = this.piece.freeColor } }) - return minoes.some(mino => mino.position.y < SKYLINE) + this.updateLockedMinoes() + return this.piece.minoesPosition[this.piece.facing].every(position => position.y + this.piece.position.y < SKYLINE) } clearLines() { let nbClearedLines = this.cells.reduceRight((nbClearedLines, row, y) => { - if (row.filter(mino => mino).length == COLUMNS) { - row.forEach(mino => this.unlockedMinoes.add(mino)) + if (row.filter(color => color).length == COLUMNS) { + row.forEach((color, x) => { + this.freedMinoes.push(new Mino(color, x, y)) + }) this.cells.splice(y, 1) this.cells.push(Array(COLUMNS)) return ++nbClearedLines } return nbClearedLines }, 0) - if (nbClearedLines) { - this.cells.forEach((rows, y) => { - rows.forEach((mino, x) => { - mino.position.set(x, y) - }) - }) - } + this.updateLockedMinoes() return nbClearedLines } - updateUnlockedMinoes(delta) { - this.unlockedMinoes.forEach(mino => { - mino.update(delta) - if (Math.sqrt(mino.position.x * mino.position.x + mino.position.z * mino.position.z) > 40 || mino.position.y < -50) { - this.remove(mino) - this.unlockedMinoes.delete(mino) - } - }) + updateLockedMinoes() { + let i = 0 + let matrix4 = new THREE.Matrix4() + this.cells.forEach((row, y) => row.forEach((color, x) => { + matrix4.setPosition(x, y, 0) + this.lockedMeshes.setMatrixAt(i, matrix4) + this.lockedMeshes.setColorAt(i, color) + i++ + })) + this.lockedMeshes.count = i + this.lockedMeshes.instanceMatrix.needsUpdate = true + this.lockedMeshes.instanceColor.needsUpdate = true + } + + updateFreedMinoes(delta) { + this.freedMinoes.forEach(mino => mino.update(delta)) + this.freedMinoes = this.freedMinoes.filter(mino => + Math.sqrt(mino.position.x * mino.position.x + mino.position.z * mino.position.z) <= 40 && mino.position.y > -50 + ) || [] + + this.freedMeshes.count = this.freedMinoes.length + if (this.freedMeshes.count) { + this.freedMinoes.forEach((mino, i) => { + this.freedMeshes.setMatrixAt(i, mino.matrix) + this.freedMeshes.setColorAt(i, mino.color) + }) + this.freedMeshes.instanceMatrix.needsUpdate = true + this.freedMeshes.instanceColor.needsUpdate = true + } } update(delta) { - this.updateUnlockedMinoes(delta) + this.updateFreedMinoes(delta) this.mixer?.update(delta) } } @@ -499,4 +525,4 @@ class NextQueue extends THREE.Group { NextQueue.prototype.positions = [P(0, 0), P(0, -3), P(0, -6), P(0, -9), P(0, -12), P(0, -15), P(0, -18)] -export { T_SPIN, FACING, TRANSLATION, ROTATION, COLORS, environnement, Tetromino, I, J, L, O, S, T, Z, Matrix, HoldQueue, NextQueue } \ No newline at end of file +export { T_SPIN, FACING, TRANSLATION, ROTATION, COLORS, environnement, minoMaterial, Tetromino, I, J, L, O, S, T, Z, Playfield, HoldQueue, NextQueue } \ No newline at end of file