diff --git a/app.js b/app.js index 9fe9c76..6f1fb1e 100644 --- a/app.js +++ b/app.js @@ -2,6 +2,10 @@ import * as THREE from 'three' import { OrbitControls } from 'three/addons/controls/OrbitControls.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import * as FPS from 'three/addons/libs/stats.module.js'; +import { T_SPIN } from './jsm/common.js' +import { Settings } from './jsm/settings.js' +import { Stats } from './jsm/stats.js' +import { Scheduler } from './jsm/utils.js' let P = (x, y, z = 0) => new THREE.Vector3(x, y, z) @@ -22,11 +26,6 @@ const ROWS = 24 const SKYLINE = 20 const COLUMNS = 10 -const DELAY = { - LOCK: 500, - FALL: 1000, -} - const COLORS = { I: 0xafeff9, J: 0xb8b4ff, @@ -56,57 +55,6 @@ const ROTATION = { CCW: -1, // CounterClockWise } -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", -] - - -/* Classes */ - -class Scheduler { - constructor() { - this.intervalTasks = new Map() - this.timeoutTasks = new Map() - } - - setInterval(func, delay, ...args) { - this.intervalTasks.set(func, window.setInterval(func, delay, ...args)) - } - - setTimeout(func, delay, ...args) { - this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) - } - - clearInterval(func) { - if (this.intervalTasks.has(func)) - window.clearInterval(this.intervalTasks.get(func)) - this.intervalTasks.delete(func) - } - - clearTimeout(func) { - if (this.timeoutTasks.has(func)) - window.clearTimeout(this.timeoutTasks.get(func)) - this.timeoutTasks.delete(func) - } -} - class Matrix extends THREE.Group { constructor() { @@ -231,23 +179,17 @@ Mino.prototype.geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSe class MinoMaterial extends THREE.MeshBasicMaterial { - constructor(color) { super({ side: THREE.DoubleSide, color: color, envMap: minoRenderTarget.texture, reflectivity: 0.9, - //roughness: 0, - //metalness: 0.85, - }) } - } class GhostMaterial extends THREE.MeshBasicMaterial { - constructor(color) { super({ side: THREE.DoubleSide, @@ -258,7 +200,6 @@ class GhostMaterial extends THREE.MeshBasicMaterial { opacity: 0.25 }) } - } @@ -477,249 +418,6 @@ Ghost.prototype.minoesPosition = [ ] -class Stats { - constructor() { - this.clock = new THREE.Clock(false) - this.clock.timeFormat = new Intl.DateTimeFormat("fr-FR", { - hour: "numeric", - minute: "2-digit", - second: "2-digit", - timeZone: "UTC" - }) - this.elapsedTime = 0 - - 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 - } - - set score(score) { - this._score = score - if (score > this.highScore) { - this.highScore = 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}

` }) - } - - get level() { - return this._level - } - - set combo(combo) { - this._combo = combo - if (combo > this.maxCombo) this.maxCombo = combo - } - - get combo() { - return this._combo - } - - set b2b(b2b) { - this._b2b = b2b - if (b2b > this.maxB2B) this.maxB2B = b2b - } - - get b2b() { - return this._b2b - } - - get time() { - return this.clock.timeFormat.format(this.clock.elapsedTime * 1000) - } - - 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 - } - - // 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 - } - - this.goal -= awardedLineClears - if (this.goal <= 0) this.level++ - } -} - - - -let jsKeyRenamer = new Proxy({ - ["←"] : "ArrowLeft", - ["→"] : "ArrowRight", - ["↑"] : "ArrowUp", - ["↓"] : "ArrowDown", - ["Espace"] : " ", - ["Échap."] : "Escape", - ["Ret. arrière"]: "Backspace", - ["Entrée"] : "Enter", -}, { - get(obj, keyName) { - return keyName in obj? obj[keyName] : keyName - } -}) -let friendyKeyRenamer = new Proxy({ - ["ArrowLeft"] : "←", - ["ArrowRight"] : "→", - ["ArrowUp"] : "↑", - ["ArrowDown"] : "↓", - [" "] : "Espace", - ["Escape"] : "Échap.", - ["Backspace"] : "Ret. arrière", - ["Enter"] : "Entrée", -}, { - get(obj, keyName) { - return keyName in obj? obj[keyName] : keyName - } -}) - -class Settings { - constructor() { - this.startLevel = 1 - - let keyMaps = { - key: {}, - action: {} - } - - this.key = new Proxy(keyMaps, { - set(km, action, key) { - km.action[key] = action - return km.key[action] = jsKeyRenamer[key] - }, - has(km, action) { - return action in km.key - }, - get(km, action) { - return friendyKeyRenamer[km.key[action]] - } - }) - this.action = new Proxy(keyMaps, { - set(km, key, action) { - km.key[action] = key - return km.action[key] = action - }, - has(km, key) { - return key in km.action - }, - get(km, key) { - return km.action[key] - } - }) - - this.key.moveLeft = "ArrowLeft" - this.key.moveRight = "ArrowRight" - this.key.rotateCCW = "w" - this.key.rotateCW = "ArrowUp" - this.key.softDrop = "ArrowDown" - this.key.hardDrop = " " - this.key.hold = "c" - this.key.pause = "Escape" - - this.arrDelay = 50 - this.dasDelay = 300 - - this.musicVolume = 50 - this.sfxVolume = 50 - } -} - - class TetraGUI extends GUI { constructor(game, settings, stats, debug=false) { super({title: "teTra"}) @@ -1288,5 +986,5 @@ window.onbeforeunload = function (event) { if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('service-worker.js'); + navigator.serviceWorker.register('jsm/service-worker.js'); } \ No newline at end of file diff --git a/gui.html b/gui.html deleted file mode 100644 index 77eb617..0000000 --- a/gui.html +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - diff --git a/jsm/common.js b/jsm/common.js new file mode 100644 index 0000000..121918d --- /dev/null +++ b/jsm/common.js @@ -0,0 +1,28 @@ +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/service-worker.js b/jsm/service-worker.js similarity index 100% rename from service-worker.js rename to jsm/service-worker.js diff --git a/jsm/settings.js b/jsm/settings.js new file mode 100644 index 0000000..45ca26b --- /dev/null +++ b/jsm/settings.js @@ -0,0 +1,82 @@ +let jsKeyRenamer = new Proxy({ + ["←"] : "ArrowLeft", + ["→"] : "ArrowRight", + ["↑"] : "ArrowUp", + ["↓"] : "ArrowDown", + ["Espace"] : " ", + ["Échap."] : "Escape", + ["Ret. arrière"]: "Backspace", + ["Entrée"] : "Enter", +}, { + get(obj, keyName) { + return keyName in obj? obj[keyName] : keyName + } +}) +let friendyKeyRenamer = new Proxy({ + ["ArrowLeft"] : "←", + ["ArrowRight"] : "→", + ["ArrowUp"] : "↑", + ["ArrowDown"] : "↓", + [" "] : "Espace", + ["Escape"] : "Échap.", + ["Backspace"] : "Ret. arrière", + ["Enter"] : "Entrée", +}, { + get(obj, keyName) { + return keyName in obj? obj[keyName] : keyName + } +}) + +class Settings { + constructor() { + this.startLevel = 1 + + let keyMaps = { + key: {}, + action: {} + } + + this.key = new Proxy(keyMaps, { + set(km, action, key) { + km.action[key] = action + return km.key[action] = jsKeyRenamer[key] + }, + has(km, action) { + return action in km.key + }, + get(km, action) { + return friendyKeyRenamer[km.key[action]] + } + }) + this.action = new Proxy(keyMaps, { + set(km, key, action) { + km.key[action] = key + return km.action[key] = action + }, + has(km, key) { + return key in km.action + }, + get(km, key) { + return km.action[key] + } + }) + + this.key.moveLeft = "ArrowLeft" + this.key.moveRight = "ArrowRight" + this.key.rotateCCW = "w" + this.key.rotateCW = "ArrowUp" + this.key.softDrop = "ArrowDown" + this.key.hardDrop = " " + this.key.hold = "c" + this.key.pause = "Escape" + + this.arrDelay = 50 + this.dasDelay = 300 + + this.musicVolume = 50 + this.sfxVolume = 50 + } +} + + +export { Settings } \ No newline at end of file diff --git a/jsm/stats.js b/jsm/stats.js new file mode 100644 index 0000000..d9dc60b --- /dev/null +++ b/jsm/stats.js @@ -0,0 +1,166 @@ +import { Clock } from 'three' +import { DELAY, T_SPIN, AWARDED_LINE_CLEARS, CLEARED_LINES_NAMES } from './common.js' + + +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 + + 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 + } + + set score(score) { + this._score = score + if (score > this.highScore) { + this.highScore = 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}

` }) + } + + get level() { + return this._level + } + + set combo(combo) { + this._combo = combo + if (combo > this.maxCombo) this.maxCombo = combo + } + + get combo() { + return this._combo + } + + set b2b(b2b) { + this._b2b = b2b + if (b2b > this.maxB2B) this.maxB2B = b2b + } + + get b2b() { + return this._b2b + } + + get time() { + return this.clock.timeFormat.format(this.clock.elapsedTime * 1000) + } + + 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 + } + + // 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 + } + + this.goal -= awardedLineClears + if (this.goal <= 0) this.level++ + } +} + + +export { Stats } \ No newline at end of file diff --git a/jsm/utils.js b/jsm/utils.js new file mode 100644 index 0000000..8486faa --- /dev/null +++ b/jsm/utils.js @@ -0,0 +1,29 @@ +class Scheduler { + constructor() { + this.intervalTasks = new Map() + this.timeoutTasks = new Map() + } + + setInterval(func, delay, ...args) { + this.intervalTasks.set(func, window.setInterval(func, delay, ...args)) + } + + setTimeout(func, delay, ...args) { + this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) + } + + clearInterval(func) { + if (this.intervalTasks.has(func)) + window.clearInterval(this.intervalTasks.get(func)) + this.intervalTasks.delete(func) + } + + clearTimeout(func) { + if (this.timeoutTasks.has(func)) + window.clearTimeout(this.timeoutTasks.get(func)) + this.timeoutTasks.delete(func) + } +} + + +export { Scheduler } \ No newline at end of file