refactoring

This commit is contained in:
2023-07-07 08:53:43 +02:00
parent 058fdd8f8b
commit a6c48989d0
8 changed files with 861 additions and 804 deletions

View File

@ -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: `<h1>NIVEAU<br/>${this.level}</h1>` })
}
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: `<h1>NIVEAU<br/>${this.level}</h1>` })
}
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<br/>${comboScore}`
})
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO x${this.combo}<br/>${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<br/>${comboScore}`
})
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `COMBO x${this.combo}<br/>${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<br/>${b2bScore}`
})
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${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<br/>${b2bScore}`
})
} else {
messagesSpan.addNewChild("div", {
className: "zoom-in-animation",
style: "animation-delay: .4s",
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${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++
}
}

View File

@ -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() {

64
jsm/Vortex.js Normal file
View File

@ -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 }

View File

@ -1,28 +0,0 @@
const DELAY = {
LOCK: 500,
FALL: 1000,
}
const T_SPIN = {
NONE: "",
MINI: "PETITE<br/>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 }

493
jsm/gamelogic.js Normal file
View File

@ -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<br/>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 }

39
jsm/scheduler.js Normal file
View File

@ -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 }

View File

@ -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 }