refactoring
This commit is contained in:
		
							
								
								
									
										312
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										312
									
								
								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<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",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* 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: `<h1>NIVEAU<br/>${this.level}</h1>` })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<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
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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');
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										159
									
								
								gui.html
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								gui.html
									
									
									
									
									
								
							@ -1,159 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8" />
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
 | 
			
		||||
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
 | 
			
		||||
  <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
 | 
			
		||||
  <script type="importmap">
 | 
			
		||||
    {
 | 
			
		||||
      "imports": {
 | 
			
		||||
        "three": "https://unpkg.com/three@0.152.2/build/three.module.js?module",
 | 
			
		||||
        "three/addons/": "https://unpkg.com/three@0.152.2/examples/jsm/"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  </script>
 | 
			
		||||
  <style>
 | 
			
		||||
    body {
 | 
			
		||||
      background: url(https://adrien.malingrey.fr/jeux/.assets/themes/clouds/background.jpg);
 | 
			
		||||
    }
 | 
			
		||||
    .lil-gui {
 | 
			
		||||
      --background-color: rgba(33, 37, 41, 30%);
 | 
			
		||||
      backdrop-filter: blur(15px);
 | 
			
		||||
    }
 | 
			
		||||
    .lil-gui.autoPlace {
 | 
			
		||||
      left: 15px;
 | 
			
		||||
    }
 | 
			
		||||
    .lil-gui .controller.disabled {
 | 
			
		||||
      opacity: .8;
 | 
			
		||||
    }
 | 
			
		||||
    i {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <script type="module">
 | 
			
		||||
    import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 | 
			
		||||
 | 
			
		||||
    var game = {
 | 
			
		||||
      startLevel: 1,
 | 
			
		||||
      start: () => {
 | 
			
		||||
        gui.gameFolder.hide()
 | 
			
		||||
        gui.statsFolder.show()
 | 
			
		||||
        gui.statsFolder.open()
 | 
			
		||||
        gui.settingsFolder.close()
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var stats = {
 | 
			
		||||
      level: 1,
 | 
			
		||||
      goal: 0,
 | 
			
		||||
      score: 0,
 | 
			
		||||
      highScore:0,
 | 
			
		||||
      time: "00:00:00",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var settings = {
 | 
			
		||||
      moveLeftKey : "ArrowLeft",
 | 
			
		||||
      moveRightKey: "ArrowRight",
 | 
			
		||||
      rotateCCWKey: "w",
 | 
			
		||||
      rotateCWKey : "ArrowUp",
 | 
			
		||||
      softDropKey : "ArrowDown",
 | 
			
		||||
      hardDropKey : " ",
 | 
			
		||||
      holdKey     : "c",
 | 
			
		||||
      pauseKey    : "Escape",
 | 
			
		||||
 | 
			
		||||
      arrDelay: 50,
 | 
			
		||||
      dasDelay: 300,
 | 
			
		||||
 | 
			
		||||
      musicVolume: 50,
 | 
			
		||||
      sfxVolume  : 50,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const KEY_NAMES = {
 | 
			
		||||
      ["ArrowLeft"]   : "←",
 | 
			
		||||
      ["ArrowRight"]  : "→",
 | 
			
		||||
      ["ArrowUp"]     : "↑",
 | 
			
		||||
      ["ArrowDown"]   : "↓",
 | 
			
		||||
      [" "]           : "Espace",
 | 
			
		||||
      ["Escape"]      : "Échap.",
 | 
			
		||||
      ["Backspace"]   : "Ret. arrière",
 | 
			
		||||
      ["Enter"]       : "Entrée",
 | 
			
		||||
      ["←"]           : "ArrowLeft",
 | 
			
		||||
      ["→"]           : "ArrowRight",
 | 
			
		||||
      ["↑"]           : "ArrowUp",
 | 
			
		||||
      ["↓"]           : "ArrowDown",
 | 
			
		||||
      ["Espace"]      : " ",
 | 
			
		||||
      ["Échap."]      : "Escape",
 | 
			
		||||
      ["Ret. arrière"]: "Backspace",
 | 
			
		||||
      ["Entrée"]      : "Enter",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function changeKey(event) {
 | 
			
		||||
      const input = event.target
 | 
			
		||||
      let prevValue = input.value
 | 
			
		||||
      input.value = ""
 | 
			
		||||
      input.onkeydown = function (event) {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
        input.value = KEY_NAMES[event.key] || event.key
 | 
			
		||||
        input.blur()
 | 
			
		||||
      }
 | 
			
		||||
      input.onblur = function (event) {
 | 
			
		||||
        if (input.value == "") input.value = prevValue
 | 
			
		||||
        input.onkeydown = null
 | 
			
		||||
        input.onblur = null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Gui extends GUI {
 | 
			
		||||
      constructor() {
 | 
			
		||||
        super({title: "teTra"});
 | 
			
		||||
 | 
			
		||||
        this.gameFolder = this.addFolder("Partie")
 | 
			
		||||
        this.gameFolder.add(game, "startLevel").name("Niveau").min(1).max(15).step(1)
 | 
			
		||||
        this.gameFolder.add(game, "start").name("Commencer")
 | 
			
		||||
        this.gameFolder.open()
 | 
			
		||||
 | 
			
		||||
        this.statsFolder = this.addFolder("Stats")
 | 
			
		||||
        this.statsFolder.add(stats,"level").name("Niveau").disable()
 | 
			
		||||
        this.statsFolder.add(stats,"goal").name("Objectif").disable()
 | 
			
		||||
        this.statsFolder.add(stats,"score").name("Score").disable()
 | 
			
		||||
        this.statsFolder.add(stats,"highScore").name("Meilleur score").disable()
 | 
			
		||||
        this.statsFolder.add(stats,"time").name("Temps").disable()
 | 
			
		||||
        this.statsFolder.hide()
 | 
			
		||||
 | 
			
		||||
        this.settingsFolder = this.addFolder("Options");
 | 
			
		||||
        this.settingsFolder.close()
 | 
			
		||||
 | 
			
		||||
        this.settingsFolder.keyMapping = this.settingsFolder.addFolder("Commandes")
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"moveLeftKey").name('<i class="bi bi-arrow-left"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"moveRightKey").name('<i class="bi bi-arrow-right"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"rotateCCWKey").name('<i class="bi bi-arrow-counterclockwise"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"rotateCWKey").name('<i class="bi bi-arrow-clockwise"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"softDropKey").name('<i class="bi bi-arrow-down-short"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"hardDropKey").name('<i class="bi bi-download"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"holdKey").name('<i class="bi bi-arrow-left-right"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.add(settings,"pauseKey").name('<i class="bi bi-pause"></i>').domElement.onclick = changeKey
 | 
			
		||||
        this.settingsFolder.keyMapping.open()
 | 
			
		||||
 | 
			
		||||
        this.settingsFolder.delayFolder = this.settingsFolder.addFolder("Répétition automatique")
 | 
			
		||||
        this.settingsFolder.delayFolder.add(settings,"arrDelay").name("ARR (ms)").min(2).max(200);
 | 
			
		||||
        this.settingsFolder.delayFolder.add(settings,"dasDelay").name("DAS (ms)").min(100).max(500).step(5);
 | 
			
		||||
        this.settingsFolder.delayFolder.open()
 | 
			
		||||
 | 
			
		||||
        this.settingsFolder.volumeFolder = this.settingsFolder.addFolder("Volume")
 | 
			
		||||
        this.settingsFolder.volumeFolder.add(settings,"musicVolume").name("Musique").min(0).max(100);
 | 
			
		||||
        this.settingsFolder.volumeFolder.add(settings,"sfxVolume").name("SFX").min(0).max(100)
 | 
			
		||||
        this.settingsFolder.volumeFolder.open()
 | 
			
		||||
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var gui = new Gui();
 | 
			
		||||
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										28
									
								
								jsm/common.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								jsm/common.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
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 }
 | 
			
		||||
							
								
								
									
										82
									
								
								jsm/settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								jsm/settings.js
									
									
									
									
									
										Normal file
									
								
							@ -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 }
 | 
			
		||||
							
								
								
									
										166
									
								
								jsm/stats.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								jsm/stats.js
									
									
									
									
									
										Normal file
									
								
							@ -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: `<h1>NIVEAU<br/>${this.level}</h1>` })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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<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
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.goal -= awardedLineClears
 | 
			
		||||
      if (this.goal <= 0) this.level++
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export { Stats }
 | 
			
		||||
							
								
								
									
										29
									
								
								jsm/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								jsm/utils.js
									
									
									
									
									
										Normal file
									
								
							@ -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 }
 | 
			
		||||
		Reference in New Issue
	
	Block a user