diff --git a/app.js b/app.js
index 773e7e8..b942e44 100644
--- a/app.js
+++ b/app.js
@@ -1,12 +1,12 @@
import * as THREE from 'three'
-import { T_SPIN } from './jsm/common.js'
+import { scheduler } from './jsm/scheduler.js'
+import { TRANSLATION, ROTATION, environnement, Matrix, HoldQueue, NextQueue } from './jsm/gamelogic.js'
import { Settings } from './jsm/Settings.js'
import { Stats } from './jsm/Stats.js'
-import { Scheduler } from './jsm/utils.js'
import { TetraGUI } from './jsm/TetraGUI.js'
import { TetraControls } from './jsm/TetraControls.js'
+import { Vortex } from './jsm/Vortex.js'
-let P = (x, y, z = 0) => new THREE.Vector3(x, y, z)
Array.prototype.pick = function () { return this.splice(Math.floor(Math.random() * this.length), 1)[0] }
@@ -19,439 +19,7 @@ HTMLElement.prototype.addNewChild = function (tag, properties) {
}
-/* Constants */
-
-const ROWS = 24
-const SKYLINE = 20
-const COLUMNS = 10
-
-const COLORS = {
- I: 0xafeff9,
- J: 0xb8b4ff,
- L: 0xfdd0b7,
- O: 0xffedac,
- S: 0xC8FBA8,
- T: 0xedb2ff,
- Z: 0xffb8c5,
-}
-
-const FACING = {
- NORTH: 0,
- EAST: 1,
- SOUTH: 2,
- WEST: 3,
-}
-
-const TRANSLATION = {
- NONE : P( 0, 0),
- LEFT : P(-1, 0),
- RIGHT: P( 1, 0),
- UP : P( 0, 1),
- DOWN : P( 0, -1),
-}
-
-const ROTATION = {
- CW: 1, // ClockWise
- CCW: 3, // CounterClockWise
-}
-
-
-class Matrix extends THREE.Group {
- constructor() {
- super()
-
- const edgeMaterial = new THREE.MeshBasicMaterial({
- color: 0x88abe0,
- envMap: envRenderTarget.texture,
- transparent: true,
- opacity: 0.4,
- reflectivity: 0.9,
- refractionRatio: 0.5
- })
-
- const edgeShape = new THREE.Shape()
- edgeShape.moveTo(-.3, SKYLINE)
- edgeShape.lineTo(0, SKYLINE)
- edgeShape.lineTo(0, 0)
- edgeShape.lineTo(COLUMNS, 0)
- edgeShape.lineTo(COLUMNS, SKYLINE)
- edgeShape.lineTo(COLUMNS + .3, SKYLINE)
- edgeShape.lineTo(COLUMNS + .3, -.3)
- edgeShape.lineTo(-.3, -.3)
- edgeShape.moveTo(-.3, SKYLINE)
- this.edge = new THREE.Mesh(
- new THREE.ExtrudeGeometry(edgeShape, {
- depth: 1,
- bevelEnabled: false,
- }),
- edgeMaterial
- )
- this.edge.visible = false
-
- const positionKF = new THREE.VectorKeyframeTrack('.position', [0, 1, 2], [0, 0, 0, 0, -0.2, 0, 0, 0, 0])
- const clip = new THREE.AnimationClip('HardDrop', 3, [positionKF])
- const animationGroup = new THREE.AnimationObjectGroup()
- animationGroup.add(this)
- animationGroup.add(this.edge)
- this.mixer = new THREE.AnimationMixer(animationGroup)
- const hardDroppedMatrix = this.mixer.clipAction(clip)
- hardDroppedMatrix.loop = THREE.LoopOnce
- hardDroppedMatrix.setDuration(0.2)
-
- this.init()
- }
-
- init() {
- this.cells = Array(ROWS).fill().map(() => Array(COLUMNS))
- this.unlockedMinoes = new Set()
- }
-
- cellIsEmpty(p) {
- return 0 <= p.x && p.x < COLUMNS &&
- 0 <= p.y && p.y < ROWS &&
- !this.cells[p.y][p.x]
- }
-
- 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
- }
- })
- return minoes.some(mino => mino.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))
- 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)
- })
- })
- }
- 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) > 25) {
- this.remove(mino)
- this.unlockedMinoes.delete(mino)
- }
- })
- }
-
- update(delta) {
- this.updateUnlockedMinoes(delta)
- this.mixer?.update(delta)
- }
-}
-
-
-class NextQueue extends THREE.Group {
- init() {
- this.pieces = this.positions.map((position) => {
- let piece = new Tetromino.random()
- piece.position.copy(position)
- this.add(piece)
- return piece
- })
- }
-
- shift() {
- let fistPiece = this.pieces.shift()
- let lastPiece = new Tetromino.random()
- this.add(lastPiece)
- this.pieces.push(lastPiece)
- this.positions.forEach((position, i) => {
- this.pieces[i].position.copy(position)
- })
- return fistPiece
- }
-
-}
-NextQueue.prototype.positions = [P(0, 0), P(0, -3), P(0, -6), P(0, -9), P(0, -12), P(0, -16)]
-
-const GRAVITY = -20
-
-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()
- scene.add(this)
- }
-
- 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)
-minoFaceShape.lineTo(.9, .9)
-minoFaceShape.lineTo(.9, .1)
-minoFaceShape.lineTo(.1, .1)
-const minoExtrudeSettings = {
- steps: 1,
- depth: .8,
- bevelEnabled: true,
- bevelThickness: .1,
- bevelSize: .1,
- bevelOffset: 0,
- bevelSegments: 1
-}
-Mino.prototype.geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
-
-
-const envRenderTarget = new THREE.WebGLCubeRenderTarget(256)
-envRenderTarget.texture.type = THREE.HalfFloatType
-const envCamera = new THREE.CubeCamera(1, 1000, envRenderTarget)
-envCamera.position.set(5, 10)
-
-class MinoMaterial extends THREE.MeshBasicMaterial {
- constructor(color) {
- super({
- side: THREE.DoubleSide,
- color: color,
- envMap: envRenderTarget.texture,
- reflectivity: 0.9,
- })
- }
-}
-
-class GhostMaterial extends THREE.MeshBasicMaterial {
- constructor(color) {
- super({
- side: THREE.DoubleSide,
- color: color,
- envMap: envRenderTarget.texture,
- reflectivity: 0.9,
- transparent: true,
- opacity: 0.2
- })
- }
-}
-
-
-class AbstractTetromino extends THREE.Group {
- constructor() {
- super()
- for (let i = 0; i < 4; i++) {
- this.add(new Mino())
- }
- }
-
- 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 => matrix.cellIsEmpty(minoPosition.clone().add(testPosition)))
- }
-}
-
-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 {
- static randomBag = []
- static get random() {
- if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
- return this.randomBag.pick()
- }
-
- constructor() {
- super()
- this.rotatedLast = false
- this.rotationPoint4Used = false
- this.holdEnabled = true
- this.facing = 0
- this.locking = false
- }
-
- move(translation, rotatedFacing, rotationPoint) {
- if (this.canMove(translation, rotatedFacing)) {
- this.position.add(translation)
- this.rotatedLast = rotatedFacing
- if (rotatedFacing != undefined) {
- this.facing = rotatedFacing
- if (rotationPoint == 4) this.rotationPoint4Used = true
- }
- if (this.canMove(TRANSLATION.DOWN)) {
- this.locking = false
- scheduler.clearTimeout(game.lockDown)
- } else {
- scheduler.resetTimeout(game.lockDown, this.lockDelay)
- this.locking = true
- }
- this.updateGhost()
- return true
- }
- }
-
- rotate(rotation) {
- let testFacing = (this.facing + rotation) % 4
- return this.srs[this.facing][rotation].some(
- (translation, rotationPoint) => this.move(translation, testFacing, rotationPoint)
- )
- }
-
- set locking(locking) {
- if (locking) {
- this.children.forEach(mino => mino.material = this.lockedMaterial)
- scene.remove(this.ghost)
- } else {
- this.children.forEach(mino => mino.material = this.material)
- scene.add(this.ghost)
- }
- }
-
- 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
- }
-}
-// Super Rotation System
-// freedom of movement = srs[matrix.piece.facing][rotation]
-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)] },
- { [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
-
-class I extends Tetromino { }
-I.prototype.minoesPosition = [
- [P(-1, 0), P(0, 0), P(1, 0), P(2, 0)],
- [P(1, 1), P(1, 0), P(1, -1), P(1, -2)],
- [P(-1, -1), P(0, -1), P(1, -1), P(2, -1)],
- [P(0, 1), P(0, 0), P(0, -1), P(0, -2)],
-]
-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)] },
- { [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)
-
-class J extends Tetromino { }
-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)],
- [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)
-
-class L extends Tetromino { }
-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)],
- [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)
-
-class O extends Tetromino { }
-O.prototype.minoesPosition = [
- [P(0, 0), P(1, 0), P(0, 1), P(1, 1)]
-]
-O.prototype.srs = [
- { [ROTATION.CW]: [], [ROTATION.CCW]: [] }
-]
-O.prototype.material = new MinoMaterial(COLORS.O)
-O.prototype.ghostMaterial = new GhostMaterial(COLORS.O)
-
-class S extends Tetromino { }
-S.prototype.minoesPosition = [
- [P(-1, 0), P(0, 0), P(0, 1), P(1, 1)],
- [P(0, 1), P(0, 0), P(1, 0), P(1, -1)],
- [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)
-
-class T extends Tetromino {
- get tSpin() {
- if (this.rotatedLast) {
- let [a, b, c, d] = this.tSlots[matrix.piece.facing]
- .map(p => !matrix.cellIsEmpty(p.clone().add(this.position)))
- if (a && b && (c || d))
- return T_SPIN.T_SPIN
- else if (c && d && (a || b))
- return this.rotationPoint4Used ? T_SPIN.T_SPIN : T_SPIN.MINI
- }
- return T_SPIN.NONE
- }
-}
-T.prototype.minoesPosition = [
- [P(-1, 0), P(0, 0), P(1, 0), P(0, 1)],
- [P(0, 1), P(0, 0), P(1, 0), P(0, -1)],
- [P(-1, 0), P(0, 0), P(1, 0), P(0, -1)],
- [P(0, 1), P(0, 0), P(0, -1), P(-1, 0)],
-]
-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)],
- [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)
-
-class Z extends Tetromino { }
-Z.prototype.minoesPosition = [
- [P(-1, 1), P(0, 1), P(0, 0), P(1, 0)],
- [P(1, 1), P(1, 0), P(0, 0), P(0, -1)],
- [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)
-
-
-/* world */
+/* Scene */
const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
@@ -469,10 +37,11 @@ loadingManager.onError = function (url) {
loadingPercent.innerText = "Erreur"
}
-const world = {}
-
const scene = new THREE.Scene()
+scene.vortex = new Vortex(loadingManager)
+scene.add(scene.vortex)
+
const renderer = new THREE.WebGLRenderer({
powerPreference: "high-performance",
antialias: true,
@@ -483,99 +52,22 @@ renderer.setClearColor(0x000000, 10)
renderer.toneMapping = THREE.ACESFilmicToneMapping
document.body.appendChild(renderer.domElement)
-world.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
-world.camera.position.set(5, 0, 16)
+scene.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
+scene.camera.position.set(5, 0, 16)
+
+scene.ambientLight = new THREE.AmbientLight(0xffffff, 0.2)
+scene.add(scene.ambientLight)
-const controls = new TetraControls(world.camera, renderer.domElement)
+scene.directionalLight = new THREE.DirectionalLight(0xffffff, 20)
+scene.directionalLight.position.set(5, -100, -16)
+scene.add(scene.directionalLight)
-
-const GLOBAL_ROTATION = 0.028
-
-const darkTextureRotation = 0.006
-const darkMoveForward = 0.007
-
-const colorFullTextureRotation = 0.006
-const colorFullMoveForward = 0.02
-
-const commonCylinderGeometry = new THREE.CylinderGeometry(25, 25, 500, 12, 1, true)
-
-world.darkCylinder = new THREE.Mesh(
- commonCylinderGeometry,
- new THREE.MeshLambertMaterial({
- side: THREE.BackSide,
- map: new THREE.TextureLoader(loadingManager).load("images/plasma.jpg", (texture) => {
- texture.wrapS = THREE.RepeatWrapping
- texture.wrapT = THREE.MirroredRepeatWrapping
- texture.repeat.set(1, 1)
- }),
- blending: THREE.AdditiveBlending,
- opacity: 0.1
- })
-)
-world.darkCylinder.position.set(5, 10, -10)
-scene.add(world.darkCylinder)
-
-world.colorFullCylinder = new THREE.Mesh(
- commonCylinderGeometry,
- new THREE.MeshBasicMaterial({
- side: THREE.BackSide,
- map: new THREE.TextureLoader(loadingManager).load("images/plasma2.jpg", (texture) => {
- texture.wrapS = THREE.RepeatWrapping
- texture.wrapT = THREE.MirroredRepeatWrapping
- texture.repeat.set(2, 1)
- }),
- blending: THREE.AdditiveBlending,
- opacity: 0.6
- })
-)
-world.colorFullCylinder.position.set(5, 10, -10)
-scene.add(world.colorFullCylinder)
-
-world.ambientLight = new THREE.AmbientLight(0xffffff, 0.2)
-scene.add(world.ambientLight)
-
-world.directionalLight = new THREE.DirectionalLight(0xffffff, 20)
-world.directionalLight.position.set(5, -100, -16)
-scene.add(world.directionalLight)
-
-const holdQueue = new THREE.Group()
-holdQueue.position.set(-4, SKYLINE - 2)
+const holdQueue = new HoldQueue()
scene.add(holdQueue)
const matrix = new Matrix()
scene.add(matrix)
-scene.add(matrix.edge)
const nextQueue = new NextQueue()
-nextQueue.position.set(13, SKYLINE - 2)
scene.add(nextQueue)
-Tetromino.prototype.ghost = new Ghost()
-
-let clock = new THREE.Clock()
-
-function animate() {
-
- const delta = clock.getDelta()
-
- world.darkCylinder.rotation.y += GLOBAL_ROTATION * delta
- world.darkCylinder.material.map.offset.y += darkMoveForward * delta
- world.darkCylinder.material.map.offset.x += darkTextureRotation * delta
-
- world.colorFullCylinder.rotation.y += GLOBAL_ROTATION * delta
- world.colorFullCylinder.material.map.offset.y += colorFullMoveForward * delta
- world.colorFullCylinder.material.map.offset.x += colorFullTextureRotation * delta
-
- controls.update()
- matrix.update(delta)
- gui.update()
-
- renderer.render(scene, world.camera)
- envCamera.update(renderer, scene)
-}
-
-window.addEventListener("resize", () => {
- renderer.setSize(window.innerWidth, window.innerHeight)
- world.camera.aspect = window.innerWidth / window.innerHeight
- world.camera.updateProjectionMatrix()
-})
messagesSpan.onanimationend = function (event) {
event.target.remove()
@@ -596,19 +88,17 @@ let game = {
holdQueue.remove(holdQueue.piece)
holdQueue.piece = undefined
if (nextQueue.pieces) nextQueue.pieces.forEach(piece => nextQueue.remove(piece))
- while(matrix.children.length) matrix.remove(matrix.children[0])
matrix.init()
scene.remove(matrix.piece)
matrix.piece = null
- world.music.currentTime = 0
- matrix.edge.visible = true
+ scene.music.currentTime = 0
+ matrix.visible = true
this.playing = true
stats.clock.start()
renderer.domElement.tabIndex = 1
- renderer.domElement.onblur = this.pause
gui.domElement.tabIndex = 1
gui.domElement.onfocus = game.pause
@@ -621,6 +111,7 @@ let game = {
resume: function() {
document.onkeydown = onkeydown
document.onkeyup = onkeyup
+ window.onblur = game.pause
document.body.classList.remove("pause")
gui.resumeButton.hide()
@@ -628,22 +119,16 @@ let game = {
stats.clock.start()
stats.clock.elapsedTime = stats.elapsedTime
- world.music.play()
+ scene.music.play()
if (matrix.piece) scheduler.setInterval(game.fall, stats.fallPeriod)
else this.generate()
},
generate: function(nextPiece=nextQueue.shift()) {
+ nextPiece.lockDelay = stats.lockDelay
matrix.piece = nextPiece
- matrix.piece.position.set(4, SKYLINE)
- matrix.piece.lockDelay = stats.lockDelay
- scene.add(matrix.piece)
- matrix.piece.updateGhost()
- matrix.piece.ghost.children.forEach((mino) => {
- mino.material = matrix.piece.ghostMaterial
- })
- scene.add(matrix.piece.ghost)
+ matrix.piece.onlockdown = game.lockDown
if (matrix.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(game.fall, stats.fallPeriod)
@@ -661,16 +146,16 @@ let game = {
scheduler.clearInterval(game.fall)
if (matrix.lock(matrix.piece)) {
- scene.remove(matrix.piece)
let tSpin = matrix.piece.tSpin
let nbClearedLines = matrix.clearLines()
+ matrix.remove(matrix.piece)
if (settings.sfxVolume) {
if (nbClearedLines == 4 || (tSpin && nbClearedLines)) {
- world.tetrisSound.currentTime = 0
- world.tetrisSound.play()
+ scene.tetrisSound.currentTime = 0
+ scene.tetrisSound.play()
} else if (nbClearedLines || tSpin) {
- world.lineClearSound.currentTime = 0
- world.lineClearSound.play()
+ scene.lineClearSound.currentTime = 0
+ scene.lineClearSound.play()
}
}
stats.lockDown(nbClearedLines, tSpin)
@@ -690,8 +175,9 @@ let game = {
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
- world.music.pause()
+ scene.music.pause()
document.onkeydown = null
+ window.onblur = null
pauseSpan.onfocus = game.resume
document.body.classList.add("pause")
@@ -703,13 +189,12 @@ let game = {
matrix.piece.locking = false
document.onkeydown = null
- renderer.domElement.onblur = null
+ window.onblur = null
renderer.domElement.onfocus = null
gui.domElement.onfocus = null
game.playing = false
- world.music.pause()
+ scene.music.pause()
stats.clock.stop()
- localStorage["teTraHighScore"] = stats.highScore
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `
GAME
OVER
` })
gui.pauseButton.hide()
@@ -718,7 +203,10 @@ let game = {
},
}
-window.addEventListener("pieceLocked", game.lockDown)
+
+/* Handle player inputs */
+
+const controls = new TetraControls(scene.camera, renderer.domElement)
let playerActions = {
moveLeft: () => matrix.piece.move(TRANSLATION.LEFT),
@@ -735,15 +223,15 @@ let playerActions = {
hardDrop: function () {
scheduler.clearTimeout(game.lockDown)
- world.hardDropSound.play()
+ scene.hardDropSound.play()
if (settings.sfxVolume) {
- world.hardDropSound.currentTime = 0
- world.hardDropSound.play()
+ scene.hardDropSound.currentTime = 0
+ scene.hardDropSound.play()
}
while (matrix.piece.move(TRANSLATION.DOWN)) stats.score += 2
game.lockDown()
- hardDroppedMatrix.reset()
- hardDroppedMatrix.play()
+ matrix.hardDropAnimation.reset()
+ matrix.hardDropAnimation.play()
},
hold: function () {
@@ -753,11 +241,6 @@ let playerActions = {
let heldpiece = holdQueue.piece
holdQueue.piece = matrix.piece
- holdQueue.piece.holdEnabled = false
- holdQueue.piece.locking = false
- holdQueue.piece.position.set(0, 0)
- holdQueue.piece.facing = FACING.NORTH
- holdQueue.add(holdQueue.piece)
game.generate(heldpiece)
}
},
@@ -765,7 +248,6 @@ let playerActions = {
pause: game.pause,
}
-// Handle player inputs
const REPEATABLE_ACTIONS = [
playerActions.moveLeft,
playerActions.moveRight,
@@ -828,46 +310,65 @@ function onkeyup(event) {
/* Sounds */
const listener = new THREE.AudioListener()
-world.camera.add( listener )
+scene.camera.add( listener )
const audioLoader = new THREE.AudioLoader(loadingManager)
-world.music = new THREE.Audio(listener)
+
+scene.music = new THREE.Audio(listener)
audioLoader.load('audio/Tetris_CheDDer_OC_ReMix.mp3', function( buffer ) {
- world.music.setBuffer(buffer)
- world.music.setLoop(true)
- world.music.setVolume(settings.musicVolume/100)
- if (game.playing) world.music.play()
+ scene.music.setBuffer(buffer)
+ scene.music.setLoop(true)
+ scene.music.setVolume(settings.musicVolume/100)
+ if (game.playing) scene.music.play()
})
-world.lineClearSound = new THREE.Audio(listener)
+scene.lineClearSound = new THREE.Audio(listener)
audioLoader.load('audio/line-clear.ogg', function( buffer ) {
- world.lineClearSound.setBuffer(buffer)
- world.lineClearSound.setVolume(settings.sfxVolume/100)
+ scene.lineClearSound.setBuffer(buffer)
+ scene.lineClearSound.setVolume(settings.sfxVolume/100)
})
-world.tetrisSound = new THREE.Audio(listener)
+scene.tetrisSound = new THREE.Audio(listener)
audioLoader.load('audio/tetris.ogg', function( buffer ) {
- world.tetrisSound.setBuffer(buffer)
- world.tetrisSound.setVolume(settings.sfxVolume/100)
+ scene.tetrisSound.setBuffer(buffer)
+ scene.tetrisSound.setVolume(settings.sfxVolume/100)
})
-world.hardDropSound = new THREE.Audio(listener)
+scene.hardDropSound = new THREE.Audio(listener)
audioLoader.load('audio/hard-drop.wav', function( buffer ) {
- world.hardDropSound.setBuffer(buffer)
- world.hardDropSound.setVolume(settings.sfxVolume/100)
+ scene.hardDropSound.setBuffer(buffer)
+ scene.hardDropSound.setVolume(settings.sfxVolume/100)
})
-let scheduler = new Scheduler()
-let stats = new Stats()
-let settings = new Settings(playerActions)
+let stats = new Stats()
+let settings = new Settings()
-var gui = new TetraGUI(game, settings, stats, world)
+var gui = new TetraGUI(game, settings, stats, scene)
-gui.load()
+const clock = new THREE.Clock()
+
+function animate() {
+
+ const delta = clock.getDelta()
+
+ scene.vortex.update(delta)
+ matrix.update(delta)
+ controls.update()
+ gui.update()
+
+ renderer.render(scene, scene.camera)
+ environnement.camera.update(renderer, scene)
+}
+
+window.addEventListener("resize", () => {
+ renderer.setSize(window.innerWidth, window.innerHeight)
+ scene.camera.aspect = window.innerWidth / window.innerHeight
+ scene.camera.updateProjectionMatrix()
+})
window.onbeforeunload = function (event) {
gui.save()
- if (game.playing) return false
+ localStorage["teTraHighScore"] = stats.highScore
+ return !game.playing
}
-
if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('jsm/service-worker.js');
+ navigator.serviceWorker.register('./jsm/service-worker.js');
}
\ No newline at end of file
diff --git a/jsm/Stats.js b/jsm/Stats.js
index 24f63ec..7b47c08 100644
--- a/jsm/Stats.js
+++ b/jsm/Stats.js
@@ -1,165 +1,186 @@
import { Clock } from 'three'
-import { DELAY, T_SPIN, AWARDED_LINE_CLEARS, CLEARED_LINES_NAMES } from './common.js'
+import { T_SPIN } from './gamelogic.js'
+// score = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
+const AWARDED_LINE_CLEARS = {
+ [T_SPIN.NONE] : [0, 1, 3, 5, 8],
+ [T_SPIN.MINI] : [1, 2],
+ [T_SPIN.T_SPIN]: [4, 8, 12, 16]
+}
+
+const CLEARED_LINES_NAMES = [
+ "",
+ "SOLO",
+ "DUO",
+ "TRIO",
+ "TETRA",
+]
+
+const DELAY = {
+ LOCK: 500,
+ FALL: 1000,
+}
+
+
class Stats {
- constructor() {
- this.clock = new Clock(false)
- this.clock.timeFormat = new Intl.DateTimeFormat("fr-FR", {
- hour: "numeric",
- minute: "2-digit",
- second: "2-digit",
- timeZone: "UTC"
- })
- this.elapsedTime = 0
+ constructor() {
+ this.clock = new Clock(false)
+ this.timeFormat = new Intl.DateTimeFormat("fr-FR", {
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ timeZone: "UTC"
+ })
+ this.elapsedTime = 0
- this.init()
- }
+ this.init()
+ }
- init() {
- this._level = 0
- this._score = 0
- this.goal = 0
- this.highScore = Number(localStorage["teTraHighScore"]) || 0
- this.combo = 0
- this.b2b = 0
- this.startTime = new Date()
- this.lockDelay = DELAY.LOCK
- this.totalClearedLines = 0
- this.nbTetra = 0
- this.nbTSpin = 0
- this.maxCombo = 0
- this.maxB2B = 0
- }
+ init() {
+ this._level = 0
+ this._score = 0
+ this.goal = 0
+ this.highScore = Number(localStorage["teTraHighScore"]) || 0
+ this.combo = 0
+ this.b2b = 0
+ this.startTime = new Date()
+ this.lockDelay = DELAY.LOCK
+ this.totalClearedLines = 0
+ this.nbTetra = 0
+ this.nbTSpin = 0
+ this.maxCombo = 0
+ this.maxB2B = 0
+ }
- set score(score) {
- this._score = score
- if (score > this.highScore) {
- this.highScore = score
- }
- }
+ set score(score) {
+ this._score = score
+ if (score > this.highScore) {
+ this.highScore = score
+ }
+ }
- get score() {
- return this._score
- }
+ get score() {
+ return this._score
+ }
- set level(level) {
- this._level = level
- this.goal += level * 5
- if (level <= 20) this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1)
- if (level > 15) this.lockDelay = 500 * Math.pow(0.9, level - 15)
- messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `NIVEAU
${this.level}
` })
- }
+ set level(level) {
+ this._level = level
+ this.goal += level * 5
+ if (level <= 20) this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1)
+ if (level > 15) this.lockDelay = 500 * Math.pow(0.9, level - 15)
+ messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `NIVEAU
${this.level}
` })
+ }
- get level() {
- return this._level
- }
+ get level() {
+ return this._level
+ }
- set combo(combo) {
- this._combo = combo
- if (combo > this.maxCombo) this.maxCombo = combo
- }
+ set combo(combo) {
+ this._combo = combo
+ if (combo > this.maxCombo) this.maxCombo = combo
+ }
- get combo() {
- return this._combo
- }
+ get combo() {
+ return this._combo
+ }
- set b2b(b2b) {
- this._b2b = b2b
- if (b2b > this.maxB2B) this.maxB2B = b2b
- }
+ set b2b(b2b) {
+ this._b2b = b2b
+ if (b2b > this.maxB2B) this.maxB2B = b2b
+ }
- get b2b() {
- return this._b2b
- }
+ get b2b() {
+ return this._b2b
+ }
- get time() {
- return this.clock.timeFormat.format(this.clock.getElapsedTime() * 1000)
- }
+ get time() {
+ return this.timeFormat.format(this.clock.getElapsedTime() * 1000)
+ }
- lockDown(nbClearedLines, tSpin) {
- this.totalClearedLines += nbClearedLines
- if (nbClearedLines == 4) this.nbTetra++
- if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
+ lockDown(nbClearedLines, tSpin) {
+ this.totalClearedLines += nbClearedLines
+ if (nbClearedLines == 4) this.nbTetra++
+ if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
- // Cleared lines & T-Spin
- let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
- let patternScore = 100 * this.level * awardedLineClears
- if (tSpin) messagesSpan.addNewChild("div", {
- className: "rotate-in-animation",
- innerHTML: tSpin
- })
- if (nbClearedLines) messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
- })
- if (patternScore) {
- messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- style: "animation-delay: .2s",
- innerHTML: patternScore
- })
- this.score += patternScore
- }
+ // Cleared lines & T-Spin
+ let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
+ let patternScore = 100 * this.level * awardedLineClears
+ if (tSpin) messagesSpan.addNewChild("div", {
+ className: "rotate-in-animation",
+ innerHTML: tSpin
+ })
+ if (nbClearedLines) messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
+ })
+ if (patternScore) {
+ messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ style: "animation-delay: .2s",
+ innerHTML: patternScore
+ })
+ this.score += patternScore
+ }
- // Combo
- if (nbClearedLines) {
- this.combo++
- if (this.combo >= 1) {
- let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
- if (this.combo == 1) {
- messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- style: "animation-delay: .4s",
- innerHTML: `COMBO
${comboScore}`
- })
- } else {
- messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- style: "animation-delay: .4s",
- innerHTML: `COMBO x${this.combo}
${comboScore}`
- })
- }
- this.score += comboScore
- }
- } else {
- this.combo = -1
- }
+ // Combo
+ if (nbClearedLines) {
+ this.combo++
+ if (this.combo >= 1) {
+ let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
+ if (this.combo == 1) {
+ messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ style: "animation-delay: .4s",
+ innerHTML: `COMBO
${comboScore}`
+ })
+ } else {
+ messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ style: "animation-delay: .4s",
+ innerHTML: `COMBO x${this.combo}
${comboScore}`
+ })
+ }
+ this.score += comboScore
+ }
+ } else {
+ this.combo = -1
+ }
- // Back to back sequence
- if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
- this.b2b++
- if (this.b2b >= 1) {
- let b2bScore = patternScore / 2
- if (this.b2b == 1) {
- messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- style: "animation-delay: .4s",
- innerHTML: `BOUT À BOUT
${b2bScore}`
- })
- } else {
- messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- style: "animation-delay: .4s",
- innerHTML: `BOUT À BOUT x${this.b2b}
${b2bScore}`
- })
- }
- this.score += b2bScore
- }
- } else if (nbClearedLines && !tSpin) {
- if (this.b2b >= 1) {
- messagesSpan.addNewChild("div", {
- className: "zoom-in-animation",
- style: "animation-delay: .4s",
- innerHTML: `FIN DU BOUT À BOUT`
- })
- }
- this.b2b = -1
- }
+ // Back to back sequence
+ if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
+ this.b2b++
+ if (this.b2b >= 1) {
+ let b2bScore = patternScore / 2
+ if (this.b2b == 1) {
+ messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ style: "animation-delay: .4s",
+ innerHTML: `BOUT À BOUT
${b2bScore}`
+ })
+ } else {
+ messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ style: "animation-delay: .4s",
+ innerHTML: `BOUT À BOUT x${this.b2b}
${b2bScore}`
+ })
+ }
+ this.score += b2bScore
+ }
+ } else if (nbClearedLines && !tSpin) {
+ if (this.b2b >= 1) {
+ messagesSpan.addNewChild("div", {
+ className: "zoom-in-animation",
+ style: "animation-delay: .4s",
+ innerHTML: `FIN DU BOUT À BOUT`
+ })
+ }
+ this.b2b = -1
+ }
- this.goal -= awardedLineClears
- if (this.goal <= 0) this.level++
- }
+ this.goal -= awardedLineClears
+ if (this.goal <= 0) this.level++
+ }
}
diff --git a/jsm/TetraGUI.js b/jsm/TetraGUI.js
index 7a827e1..d95477f 100644
--- a/jsm/TetraGUI.js
+++ b/jsm/TetraGUI.js
@@ -1,9 +1,10 @@
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
-import * as FPS from 'three/addons/libs/stats.module.js';
+import * as FPS from 'three/addons/libs/stats.module.js'
+import { I, J, L, O, S, T, Z } from './gamelogic.js'
class TetraGUI extends GUI {
- constructor(game, settings, stats, world) {
+ constructor(game, settings, stats, scene) {
super({title: "teTra"})
this.startButton = this.add(game, "start").name("Jouer").hide()
@@ -51,44 +52,46 @@ class TetraGUI extends GUI {
this.settings.volume = this.settings.addFolder("Volume").open()
this.settings.volume.add(settings,"musicVolume").name("Musique").min(0).max(100).step(1).onChange((volume) => {
if (volume) {
- world.music.setVolume(volume/100)
- if (game.playing) world.music.play()
+ scene.music.setVolume(volume/100)
+ if (game.playing) scene.music.play()
} else {
- world.music.pause()
+ scene.music.pause()
}
})
this.settings.volume.add(settings,"sfxVolume").name("Effets").min(0).max(100).step(1).onChange((volume) => {
- world.lineClearSound.setVolume(volume/100)
- world.tetrisSound.setVolume(volume/100)
- world.hardDropSound.setVolume(volume/100)
+ scene.lineClearSound.setVolume(volume/100)
+ scene.tetrisSound.setVolume(volume/100)
+ scene.hardDropSound.setVolume(volume/100)
})
if (window.location.search.includes("debug")) {
this.debug = this.addFolder("debug")
let cameraPositionFolder = this.debug.addFolder("camera.position")
- cameraPositionFolder.add(world.camera.position, "x")
- cameraPositionFolder.add(world.camera.position, "y")
- cameraPositionFolder.add(world.camera.position, "z")
+ cameraPositionFolder.add(scene.camera.position, "x")
+ cameraPositionFolder.add(scene.camera.position, "y")
+ cameraPositionFolder.add(scene.camera.position, "z")
let lightFolder = this.debug.addFolder("lights intensity")
- lightFolder.add(world.ambientLight, "intensity").name("ambient").min(0).max(20)
- lightFolder.add(world.directionalLight, "intensity").name("directional").min(0).max(20)
+ lightFolder.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20)
+ lightFolder.add(scene.directionalLight, "intensity").name("directional").min(0).max(20)
let materialsFolder = this.debug.addFolder("materials opacity")
- materialsFolder.add(world.darkCylinder.material, "opacity").name("dark").min(0).max(1)
- materialsFolder.add(world.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1)
- /*materialsFolder.add(I.prototype.material, "reflectivity").min(0).max(2).onChange(() => {
+ materialsFolder.add(scene.vortex.darkCylinder.material, "opacity").name("dark").min(0).max(1)
+ materialsFolder.add(scene.vortex.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1)
+ materialsFolder.add(I.prototype.material, "reflectivity").min(0).max(2).onChange(() => {
J.prototype.material.reflectivity = I.prototype.material.reflectivity
L.prototype.material.reflectivity = I.prototype.material.reflectivity
O.prototype.material.reflectivity = I.prototype.material.reflectivity
S.prototype.material.reflectivity = I.prototype.material.reflectivity
T.prototype.material.reflectivity = I.prototype.material.reflectivity
Z.prototype.material.reflectivity = I.prototype.material.reflectivity
- })*/
+ })
this.fps = new FPS.default()
document.body.appendChild(this.fps.dom)
}
+
+ this.load()
}
load() {
diff --git a/jsm/Vortex.js b/jsm/Vortex.js
new file mode 100644
index 0000000..d5574d8
--- /dev/null
+++ b/jsm/Vortex.js
@@ -0,0 +1,64 @@
+import * as THREE from 'three'
+
+
+const GLOBAL_ROTATION = 0.028
+
+const darkTextureRotation = 0.006
+const darkMoveForward = 0.007
+
+const colorFullTextureRotation = 0.006
+const colorFullMoveForward = 0.02
+
+
+class Vortex extends THREE.Group {
+ constructor(loadingManager) {
+ super()
+
+ const commonCylinderGeometry = new THREE.CylinderGeometry(25, 25, 500, 12, 1, true)
+
+ this.darkCylinder = new THREE.Mesh(
+ commonCylinderGeometry,
+ new THREE.MeshLambertMaterial({
+ side: THREE.BackSide,
+ map: new THREE.TextureLoader(loadingManager).load("images/plasma.jpg", (texture) => {
+ texture.wrapS = THREE.RepeatWrapping
+ texture.wrapT = THREE.MirroredRepeatWrapping
+ texture.repeat.set(1, 1)
+ }),
+ blending: THREE.AdditiveBlending,
+ opacity: 0.1
+ })
+ )
+ this.darkCylinder.position.set(5, 10, -10)
+ this.add(this.darkCylinder)
+
+ this.colorFullCylinder = new THREE.Mesh(
+ commonCylinderGeometry,
+ new THREE.MeshBasicMaterial({
+ side: THREE.BackSide,
+ map: new THREE.TextureLoader(loadingManager).load("images/plasma2.jpg", (texture) => {
+ texture.wrapS = THREE.RepeatWrapping
+ texture.wrapT = THREE.MirroredRepeatWrapping
+ texture.repeat.set(2, 1)
+ }),
+ blending: THREE.AdditiveBlending,
+ opacity: 0.6
+ })
+ )
+ this.colorFullCylinder.position.set(5, 10, -10)
+ this.add(this.colorFullCylinder)
+ }
+
+ update(delta) {
+ this.darkCylinder.rotation.y += GLOBAL_ROTATION * delta
+ this.darkCylinder.material.map.offset.y += darkMoveForward * delta
+ this.darkCylinder.material.map.offset.x += darkTextureRotation * delta
+
+ this.colorFullCylinder.rotation.y += GLOBAL_ROTATION * delta
+ this.colorFullCylinder.material.map.offset.y += colorFullMoveForward * delta
+ this.colorFullCylinder.material.map.offset.x += colorFullTextureRotation * delta
+ }
+}
+
+
+export { Vortex }
\ No newline at end of file
diff --git a/jsm/common.js b/jsm/common.js
deleted file mode 100644
index 121918d..0000000
--- a/jsm/common.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const DELAY = {
- LOCK: 500,
- FALL: 1000,
-}
-
-const T_SPIN = {
- NONE: "",
- MINI: "PETITE
PIROUETTE",
- T_SPIN: "PIROUETTE"
-}
-
-// score = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
-const AWARDED_LINE_CLEARS = {
- [T_SPIN.NONE]: [0, 1, 3, 5, 8],
- [T_SPIN.MINI]: [1, 2],
- [T_SPIN.T_SPIN]: [4, 8, 12, 16]
-}
-
-const CLEARED_LINES_NAMES = [
- "",
- "SOLO",
- "DUO",
- "TRIO",
- "TETRA",
-]
-
-
-export { DELAY, T_SPIN, AWARDED_LINE_CLEARS, CLEARED_LINES_NAMES }
\ No newline at end of file
diff --git a/jsm/gamelogic.js b/jsm/gamelogic.js
new file mode 100644
index 0000000..750f20c
--- /dev/null
+++ b/jsm/gamelogic.js
@@ -0,0 +1,493 @@
+import * as THREE from 'three'
+import { scheduler } from './scheduler.js'
+
+
+let P = (x, y, z = 0) => new THREE.Vector3(x, y, z)
+
+const GRAVITY = -20
+
+const COLORS = {
+ I: 0xafeff9,
+ J: 0xb8b4ff,
+ L: 0xfdd0b7,
+ O: 0xffedac,
+ S: 0xC8FBA8,
+ T: 0xedb2ff,
+ Z: 0xffb8c5,
+}
+
+const TRANSLATION = {
+ NONE : P( 0, 0),
+ LEFT : P(-1, 0),
+ RIGHT: P( 1, 0),
+ UP : P( 0, 1),
+ DOWN : P( 0, -1),
+}
+
+const ROTATION = {
+ CW: 1, // ClockWise
+ CCW: 3, // CounterClockWise
+}
+
+const T_SPIN = {
+ NONE: "",
+ MINI: "PETITE
PIROUETTE",
+ T_SPIN: "PIROUETTE"
+}
+
+const FACING = {
+ NORTH: 0,
+ EAST: 1,
+ SOUTH: 2,
+ WEST: 3,
+}
+
+
+const envRenderTarget = new THREE.WebGLCubeRenderTarget(256)
+const environnement = envRenderTarget.texture
+environnement.type = THREE.HalfFloatType
+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)
+minoFaceShape.lineTo(.9, .9)
+minoFaceShape.lineTo(.9, .1)
+minoFaceShape.lineTo(.1, .1)
+const minoExtrudeSettings = {
+ steps: 1,
+ depth: .8,
+ bevelEnabled: true,
+ bevelThickness: .1,
+ bevelSize: .1,
+ bevelOffset: 0,
+ bevelSegments: 1
+}
+Mino.prototype.geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
+
+
+class MinoMaterial extends THREE.MeshBasicMaterial {
+ constructor(color) {
+ super({
+ side: THREE.DoubleSide,
+ color: color,
+ envMap: environnement,
+ reflectivity: 0.9,
+ })
+ }
+}
+
+class GhostMaterial extends THREE.MeshBasicMaterial {
+ constructor(color) {
+ super({
+ side: THREE.DoubleSide,
+ color: color,
+ envMap: environnement,
+ reflectivity: 0.9,
+ transparent: true,
+ opacity: 0.2
+ })
+ }
+}
+
+
+class AbstractTetromino extends THREE.Group {
+ constructor() {
+ super()
+ for (let i = 0; i < 4; i++) {
+ this.add(new Mino())
+ }
+ }
+
+ 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)))
+ }
+}
+
+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 {
+ static randomBag = []
+ static get random() {
+ if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
+ return this.randomBag.pick()
+ }
+
+ constructor() {
+ super()
+ this.rotatedLast = false
+ this.rotationPoint4Used = false
+ this.holdEnabled = true
+ this.facing = 0
+ this.locking = false
+ }
+
+ move(translation, rotatedFacing, rotationPoint) {
+ if (this.canMove(translation, rotatedFacing)) {
+ this.position.add(translation)
+ this.rotatedLast = rotatedFacing
+ if (rotatedFacing != undefined) {
+ this.facing = rotatedFacing
+ if (rotationPoint == 4) this.rotationPoint4Used = true
+ }
+ if (this.canMove(TRANSLATION.DOWN)) {
+ this.locking = false
+ scheduler.clearTimeout(this.onlockdown)
+ } else {
+ scheduler.resetTimeout(this.onlockdown, this.lockDelay)
+ this.locking = true
+ }
+ if (this.ghost.visible) this.updateGhost()
+ return true
+ }
+ }
+
+ rotate(rotation) {
+ let testFacing = (this.facing + rotation) % 4
+ return this.srs[this.facing][rotation].some(
+ (translation, rotationPoint) => this.move(translation, testFacing, rotationPoint)
+ )
+ }
+
+ 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
+ }
+}
+// Super Rotation System
+// freedom of movement = srs[this.parent.piece.facing][rotation]
+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)] },
+ { [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 I extends Tetromino { }
+I.prototype.minoesPosition = [
+ [P(-1, 0), P(0, 0), P(1, 0), P(2, 0)],
+ [P(1, 1), P(1, 0), P(1, -1), P(1, -2)],
+ [P(-1, -1), P(0, -1), P(1, -1), P(2, -1)],
+ [P(0, 1), P(0, 0), P(0, -1), P(0, -2)],
+]
+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)] },
+ { [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)
+
+class J extends Tetromino { }
+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)],
+ [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)
+
+class L extends Tetromino { }
+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)],
+ [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)
+
+class O extends Tetromino { }
+O.prototype.minoesPosition = [
+ [P(0, 0), P(1, 0), P(0, 1), P(1, 1)]
+]
+O.prototype.srs = [
+ { [ROTATION.CW]: [], [ROTATION.CCW]: [] }
+]
+O.prototype.material = new MinoMaterial(COLORS.O)
+O.prototype.ghostMaterial = new GhostMaterial(COLORS.O)
+
+class S extends Tetromino { }
+S.prototype.minoesPosition = [
+ [P(-1, 0), P(0, 0), P(0, 1), P(1, 1)],
+ [P(0, 1), P(0, 0), P(1, 0), P(1, -1)],
+ [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)
+
+class T extends Tetromino {
+ get tSpin() {
+ if (this.rotatedLast) {
+ let [a, b, c, d] = this.tSlots[this.facing]
+ .map(p => !this.parent.cellIsEmpty(p.clone().add(this.position)))
+ if (a && b && (c || d))
+ return T_SPIN.T_SPIN
+ else if (c && d && (a || b))
+ return this.rotationPoint4Used ? T_SPIN.T_SPIN : T_SPIN.MINI
+ }
+ return T_SPIN.NONE
+ }
+}
+T.prototype.minoesPosition = [
+ [P(-1, 0), P(0, 0), P(1, 0), P(0, 1)],
+ [P(0, 1), P(0, 0), P(1, 0), P(0, -1)],
+ [P(-1, 0), P(0, 0), P(1, 0), P(0, -1)],
+ [P(0, 1), P(0, 0), P(0, -1), P(-1, 0)],
+]
+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)],
+ [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)
+
+class Z extends Tetromino { }
+Z.prototype.minoesPosition = [
+ [P(-1, 1), P(0, 1), P(0, 0), P(1, 0)],
+ [P(1, 1), P(1, 0), P(0, 0), P(0, -1)],
+ [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)
+
+
+const ROWS = 24
+const SKYLINE = 20
+const COLUMNS = 10
+
+
+class Matrix extends THREE.Group {
+ constructor() {
+ super()
+ this.visible = false
+
+ const edgeMaterial = new THREE.MeshBasicMaterial({
+ color: 0x88abe0,
+ envMap: environnement,
+ transparent: true,
+ opacity: 0.4,
+ reflectivity: 0.9,
+ refractionRatio: 0.5
+ })
+ const edgeShape = new THREE.Shape()
+ .moveTo(-.3, SKYLINE)
+ .lineTo(0, SKYLINE)
+ .lineTo(0, 0)
+ .lineTo(COLUMNS, 0)
+ .lineTo(COLUMNS, SKYLINE)
+ .lineTo(COLUMNS + .3, SKYLINE)
+ .lineTo(COLUMNS + .3, -.3)
+ .lineTo(-.3, -.3)
+ .moveTo(-.3, SKYLINE)
+ const edge = new THREE.Mesh(
+ new THREE.ExtrudeGeometry(edgeShape, {
+ depth: 1,
+ bevelEnabled: false,
+ }),
+ edgeMaterial
+ )
+ this.add(edge)
+
+ const positionKF = new THREE.VectorKeyframeTrack('.position', [0, 1, 2], [0, 0, 0, 0, -0.2, 0, 0, 0, 0])
+ const clip = new THREE.AnimationClip('HardDrop', 3, [positionKF])
+ const animationGroup = new THREE.AnimationObjectGroup()
+ animationGroup.add(this)
+ this.mixer = new THREE.AnimationMixer(animationGroup)
+ this.hardDropAnimation = this.mixer.clipAction(clip)
+ this.hardDropAnimation.loop = THREE.LoopOnce
+ this.hardDropAnimation.setDuration(0.2)
+
+ 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()
+ }
+
+ cellIsEmpty(p) {
+ return 0 <= p.x && p.x < COLUMNS &&
+ 0 <= p.y && p.y < ROWS &&
+ !this.cells[p.y][p.x]
+ }
+
+ set piece(piece) {
+ 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._piece = piece
+ }
+
+ get piece() {
+ return this._piece
+ }
+
+ 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
+ }
+ })
+ return minoes.some(mino => mino.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))
+ 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)
+ })
+ })
+ }
+ 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) > 25) {
+ this.remove(mino)
+ this.unlockedMinoes.delete(mino)
+ }
+ })
+ }
+
+ update(delta) {
+ this.updateUnlockedMinoes(delta)
+ this.mixer?.update(delta)
+ }
+}
+
+
+class HoldQueue extends THREE.Group {
+ constructor() {
+ super()
+ this.position.set(-4, SKYLINE - 2)
+ }
+
+ set piece(piece) {
+ if(piece) {
+ piece.holdEnabled = false
+ piece.locking = false
+ piece.position.set(0, 0)
+ piece.facing = FACING.NORTH
+ this.add(piece)
+ }
+ this._piece = piece
+ }
+
+ get piece() {
+ return this._piece
+ }
+}
+
+
+class NextQueue extends THREE.Group {
+ constructor() {
+ super()
+ this.position.set(13, SKYLINE - 2)
+ }
+
+ init() {
+ this.pieces = this.positions.map((position) => {
+ let piece = new Tetromino.random()
+ piece.position.copy(position)
+ this.add(piece)
+ return piece
+ })
+ }
+
+ shift() {
+ let fistPiece = this.pieces.shift()
+ let lastPiece = new Tetromino.random()
+ this.add(lastPiece)
+ this.pieces.push(lastPiece)
+ this.positions.forEach((position, i) => {
+ this.pieces[i].position.copy(position)
+ })
+ return fistPiece
+ }
+
+}
+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, environnement, Tetromino, I, J, L, O, S, T, Z, Matrix, HoldQueue, NextQueue }
\ No newline at end of file
diff --git a/jsm/scheduler.js b/jsm/scheduler.js
new file mode 100644
index 0000000..da29b7e
--- /dev/null
+++ b/jsm/scheduler.js
@@ -0,0 +1,39 @@
+class Scheduler {
+ constructor() {
+ this.intervalTasks = new Map()
+ this.timeoutTasks = new Map()
+ }
+
+ setInterval(func, delay, ...args) {
+ this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
+ }
+
+ clearInterval(func) {
+ if (this.intervalTasks.has(func)) {
+ window.clearInterval(this.intervalTasks.get(func))
+ this.intervalTasks.delete(func)
+ }
+ }
+
+ setTimeout(func, delay, ...args) {
+ this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
+ }
+
+ clearTimeout(func) {
+ if (this.timeoutTasks.has(func)) {
+ window.clearTimeout(this.timeoutTasks.get(func))
+ this.timeoutTasks.delete(func)
+ }
+ }
+
+ resetTimeout(func, delay, ...args) {
+ this.clearTimeout(func)
+ this.setTimeout(func, delay, ...args)
+ }
+}
+
+
+const scheduler = new Scheduler
+
+
+export { scheduler }
\ No newline at end of file
diff --git a/jsm/utils.js b/jsm/utils.js
deleted file mode 100644
index 2185846..0000000
--- a/jsm/utils.js
+++ /dev/null
@@ -1,36 +0,0 @@
-class Scheduler {
- constructor() {
- this.intervalTasks = new Map()
- this.timeoutTasks = new Map()
- }
-
- setInterval(func, delay, ...args) {
- this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
- }
-
- clearInterval(func) {
- if (this.intervalTasks.has(func)) {
- window.clearInterval(this.intervalTasks.get(func))
- this.intervalTasks.delete(func)
- }
- }
-
- setTimeout(func, delay, ...args) {
- this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
- }
-
- clearTimeout(func) {
- if (this.timeoutTasks.has(func)) {
- window.clearTimeout(this.timeoutTasks.get(func))
- this.timeoutTasks.delete(func)
- }
- }
-
- resetTimeout(func, delay, ...args) {
- this.clearTimeout(func)
- this.setTimeout(func, delay, ...args)
- }
-}
-
-
-export { Scheduler }
\ No newline at end of file