split js
This commit is contained in:
parent
46a0500c66
commit
f42a14d75f
772
app.js
772
app.js
@ -1,767 +1,3 @@
|
|||||||
|
|
||||||
/* Constants */
|
|
||||||
const TRANSLATION = {
|
|
||||||
NONE: [ 0, 0],
|
|
||||||
LEFT: [-1, 0],
|
|
||||||
RIGHT: [ 1, 0],
|
|
||||||
DOWN: [ 0, 1],
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROTATION = {
|
|
||||||
CW: 1, // ClockWise
|
|
||||||
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",
|
|
||||||
"QUATUOR",
|
|
||||||
]
|
|
||||||
|
|
||||||
const DELAY = {
|
|
||||||
LOCK: 500,
|
|
||||||
FALL: 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
const FACING = {
|
|
||||||
NORTH: 0,
|
|
||||||
EAST: 1,
|
|
||||||
SOUTH: 2,
|
|
||||||
WEST: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY_NAMES = new Proxy({
|
|
||||||
["ArrowLeft"] : "←",
|
|
||||||
["←"] : "ArrowLeft",
|
|
||||||
["ArrowRight"] : "→",
|
|
||||||
["→"] : "ArrowRight",
|
|
||||||
["ArrowUp"] : "↑",
|
|
||||||
["↑"] : "ArrowUp",
|
|
||||||
["ArrowDown"] : "↓",
|
|
||||||
["↓"] : "ArrowDown",
|
|
||||||
[" "] : "Espace",
|
|
||||||
["Espace"] : " ",
|
|
||||||
["Escape"] : "Échap.",
|
|
||||||
["Échap."] : "Escape",
|
|
||||||
["Backspace"] : "Ret. arrière",
|
|
||||||
["Ret. arrière"]: "Backspace",
|
|
||||||
["Enter"] : "Entrée",
|
|
||||||
["Entrée"] : "Enter",
|
|
||||||
}, {
|
|
||||||
get(obj, keyName) {
|
|
||||||
return keyName in obj? obj[keyName] : keyName
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Customize Array to be use as position */
|
|
||||||
Object.defineProperties(Array.prototype, {
|
|
||||||
"x": {
|
|
||||||
get: function() { return this[0] },
|
|
||||||
set: function(x) { this[0] = x }
|
|
||||||
},
|
|
||||||
"y": {
|
|
||||||
get: function() { return this[1] },
|
|
||||||
set: function(y) { this[1] = y }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
|
|
||||||
Array.prototype.mul = function(k) { return this.map(x => k * x) }
|
|
||||||
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
|
|
||||||
Array.prototype.rotate = function(rotation) { return [-rotation*this.y, rotation*this.x] }
|
|
||||||
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
|
|
||||||
|
|
||||||
|
|
||||||
HTMLElement.prototype.addNewChild = function(tag, properties) {
|
|
||||||
let child = document.createElement(tag)
|
|
||||||
for (key in properties) {
|
|
||||||
child[key] = properties[key]
|
|
||||||
}
|
|
||||||
this.appendChild(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* 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 MinoesTable {
|
|
||||||
constructor(id) {
|
|
||||||
this.table = document.getElementById(id)
|
|
||||||
Array.from(this.table.getElementsByTagName("tr")).forEach((tr, row) => {
|
|
||||||
tr.style.setProperty('--row', row)
|
|
||||||
Array.from(tr.getElementsByTagName("td")).forEach((td, column) => {
|
|
||||||
td.style.setProperty('--column', column)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.rows = this.table.rows.length
|
|
||||||
this.columns = this.table.rows[0].childElementCount
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._piece = null
|
|
||||||
}
|
|
||||||
|
|
||||||
get piece() {
|
|
||||||
return this._piece
|
|
||||||
}
|
|
||||||
set piece(piece) {
|
|
||||||
this._piece = piece
|
|
||||||
this._piece.center = Array.from(this.init_center)
|
|
||||||
this.redraw()
|
|
||||||
this.drawPiece()
|
|
||||||
}
|
|
||||||
|
|
||||||
drawMino(position, className) {
|
|
||||||
this.table.rows[position.y].cells[position.x].className = className
|
|
||||||
}
|
|
||||||
|
|
||||||
drawPiece(piece=this.piece, className=piece.className) {
|
|
||||||
piece.minoesPosition[piece.facing]
|
|
||||||
.translate(piece.center)
|
|
||||||
.forEach(minoPosition => {
|
|
||||||
this.drawMino(minoPosition, className)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
|
||||||
for (let y=0; y<this.rows; y++) {
|
|
||||||
for (let x=0; x<this.columns; x++) {
|
|
||||||
this.drawMino([x, y], "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MinoesTable.prototype.init_center = [2, 2]
|
|
||||||
|
|
||||||
|
|
||||||
class NextQueue extends MinoesTable {
|
|
||||||
constructor() {
|
|
||||||
super("nextTable")
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.pieces = this.init_centers.map(center => {
|
|
||||||
let piece = new Tetromino.pick()
|
|
||||||
piece.center = Array.from(center)
|
|
||||||
return piece
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shift() {
|
|
||||||
let fistPiece = this.pieces.shift()
|
|
||||||
this.pieces.push(new Tetromino.pick())
|
|
||||||
this.pieces.forEach((piece, i) => {
|
|
||||||
piece.center = Array.from(this.init_centers[i])
|
|
||||||
})
|
|
||||||
this.redraw()
|
|
||||||
return fistPiece
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
|
||||||
super.redraw()
|
|
||||||
this.pieces.forEach((piece) => {
|
|
||||||
this.drawPiece(piece)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NextQueue.prototype.init_centers = [[2, 2], [2, 5], [2, 8], [2, 11], [2, 14]]
|
|
||||||
|
|
||||||
|
|
||||||
class Matrix extends MinoesTable {
|
|
||||||
constructor() {
|
|
||||||
super("matrixTable")
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init()
|
|
||||||
this.blocks = Array(this.rows).fill().map(() => Array(this.columns))
|
|
||||||
this.redraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
cellIsEmpty(position) {
|
|
||||||
return 0 <= position.x && position.x < this.columns && 0 <= position.y && position.y < this.rows && !this.blocks[position.y][position.x]
|
|
||||||
}
|
|
||||||
|
|
||||||
get piece() {
|
|
||||||
return this._piece
|
|
||||||
}
|
|
||||||
set piece(piece) {
|
|
||||||
this._piece = piece
|
|
||||||
this._piece.center = Array.from(this.init_center)
|
|
||||||
this.ghost = piece.ghost
|
|
||||||
this.redraw()
|
|
||||||
this.drawPiece()
|
|
||||||
}
|
|
||||||
|
|
||||||
drawPiece(piece=this.piece, className=piece.className) {
|
|
||||||
super.drawPiece(this.ghost, "")
|
|
||||||
this.ghost = piece.ghost
|
|
||||||
super.drawPiece(this.ghost)
|
|
||||||
if (piece.locked) className += " locking"
|
|
||||||
if (piece==this.piece && actionsQueue.length) className += " moving"
|
|
||||||
super.drawPiece(piece, className)
|
|
||||||
matrix.table.style.setProperty('--piece-column', this.piece.center.x)
|
|
||||||
matrix.table.style.setProperty('--piece-row', this.piece.center.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
|
||||||
for (let y=0; y<this.rows; y++) {
|
|
||||||
for (let x=0; x<this.columns; x++) {
|
|
||||||
if (this.table.rows[y].cells[x].classList != "trail-animation")
|
|
||||||
this.drawMino([x, y], this.blocks[y][x] || "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock() {
|
|
||||||
let blocksPosition = this.piece.minoesPosition[this.piece.facing].translate(this.piece.center)
|
|
||||||
if (blocksPosition.some(position => position.y >= 4)) {
|
|
||||||
blocksPosition.forEach(position => {
|
|
||||||
this.blocks[position.y][position.x] = "locked " + this.piece.className
|
|
||||||
this.drawMino(position, this.piece.className)
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLines() {
|
|
||||||
let nbClearedLines = 0
|
|
||||||
for (let y=0; y<this.rows; y++) {
|
|
||||||
let row = this.blocks[y]
|
|
||||||
if (row.filter(lockedMino => lockedMino).length == this.columns) {
|
|
||||||
nbClearedLines++
|
|
||||||
this.blocks.splice(y, 1)
|
|
||||||
this.blocks.unshift(Array(matrix.columns))
|
|
||||||
this.table.rows[y].classList.add("cleared-line-animation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.redraw()
|
|
||||||
return nbClearedLines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Matrix.prototype.init_center = [4, 4]
|
|
||||||
|
|
||||||
|
|
||||||
class Tetromino {
|
|
||||||
static randomBag = []
|
|
||||||
static get pick() {
|
|
||||||
if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
|
|
||||||
return this.randomBag.pick()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(center, facing=0, className=this.constructor.name + " mino") {
|
|
||||||
this.center = center
|
|
||||||
this.className = className
|
|
||||||
this.facing = facing
|
|
||||||
this.lastRotation = false
|
|
||||||
this.rotationPoint4Used = false
|
|
||||||
this.holdEnabled = true
|
|
||||||
this.locked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
canMove(translation, rotation=ROTATION.NONE) {
|
|
||||||
let testCenter = this.center.add(translation)
|
|
||||||
let testFacing = rotation? (this.facing + rotation + 4) % 4: this.facing
|
|
||||||
let testMinoesPosition = this.minoesPosition[testFacing]
|
|
||||||
if (testMinoesPosition
|
|
||||||
.translate(testCenter)
|
|
||||||
.every(minoPosition => matrix.cellIsEmpty(minoPosition)))
|
|
||||||
return {center: testCenter, facing: testFacing}
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
move(translation, rotation=ROTATION.NONE, hardDropped=false) {
|
|
||||||
let success = this.canMove(translation, rotation)
|
|
||||||
if (success) {
|
|
||||||
scheduler.clearTimeout(lockDown)
|
|
||||||
matrix.drawPiece(this, hardDropped? "trail-animation" : "")
|
|
||||||
this.center = success.center
|
|
||||||
if (rotation) this.facing = success.facing
|
|
||||||
this.lastRotation = rotation
|
|
||||||
if (this.canMove(TRANSLATION.DOWN)) {
|
|
||||||
this.locked = false
|
|
||||||
} else {
|
|
||||||
this.locked = true
|
|
||||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
||||||
}
|
|
||||||
matrix.drawPiece()
|
|
||||||
return true
|
|
||||||
} else if (!hardDropped) {
|
|
||||||
// wallSound.play()
|
|
||||||
if (translation == TRANSLATION.DOWN) {
|
|
||||||
this.locked = true
|
|
||||||
if (!scheduler.timeoutTasks.has(lockDown))
|
|
||||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
||||||
matrix.drawPiece()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rotate(rotation) {
|
|
||||||
return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
|
|
||||||
if (this.move(translation, rotation)) {
|
|
||||||
if (rotationPoint == 4) this.rotationPoint4Used = true
|
|
||||||
favicon.href = this.favicon_href
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
get ghost() {
|
|
||||||
let ghost = new this.constructor(Array.from(this.center), this.facing, "ghost " + this.className)
|
|
||||||
while (ghost.canMove(TRANSLATION.DOWN)) ghost.center.y++
|
|
||||||
return ghost
|
|
||||||
}
|
|
||||||
|
|
||||||
get favicon_href() {
|
|
||||||
return `favicons/${this.constructor.name}-${this.facing}.png`
|
|
||||||
}
|
|
||||||
|
|
||||||
get tSpin() {
|
|
||||||
return T_SPIN.NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Super Rotation System
|
|
||||||
// freedom of movement = srs[piece.facing][rotation]
|
|
||||||
Tetromino.prototype.srs = [
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]] },
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]] },
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]] },
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]] },
|
|
||||||
]
|
|
||||||
|
|
||||||
class I extends Tetromino {}
|
|
||||||
I.prototype.minoesPosition = [
|
|
||||||
[[-1, 0], [0, 0], [1, 0], [2, 0]],
|
|
||||||
[[1, -1], [1, 0], [1, 1], [1, 2]],
|
|
||||||
[[-1, 1], [0, 1], [1, 1], [2, 1]],
|
|
||||||
[[0, -1], [0, 0], [0, 1], [0, 2]],
|
|
||||||
]
|
|
||||||
I.prototype.srs = [
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]] },
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]], [ROTATION.CCW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]] },
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]] },
|
|
||||||
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]], [ROTATION.CCW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]] },
|
|
||||||
]
|
|
||||||
|
|
||||||
class J extends Tetromino {}
|
|
||||||
J.prototype.minoesPosition = [
|
|
||||||
[[-1, -1], [-1, 0], [0, 0], [1, 0]],
|
|
||||||
[[ 0, -1], [1, -1], [0, 0], [0, 1]],
|
|
||||||
[[ 1, 1], [-1, 0], [0, 0], [1, 0]],
|
|
||||||
[[ 0, -1], [-1, 1], [0, 0], [0, 1]],
|
|
||||||
]
|
|
||||||
|
|
||||||
class L extends Tetromino {}
|
|
||||||
L.prototype.minoesPosition = [
|
|
||||||
[[-1, 0], [0, 0], [1, 0], [ 1, -1]],
|
|
||||||
[[0, -1], [0, 0], [0, 1], [ 1, 1]],
|
|
||||||
[[-1, 0], [0, 0], [1, 0], [-1, 1]],
|
|
||||||
[[0, -1], [0, 0], [0, 1], [-1, -1]],
|
|
||||||
]
|
|
||||||
|
|
||||||
class O extends Tetromino {}
|
|
||||||
O.prototype.minoesPosition = [
|
|
||||||
[[0, 0], [1, 0], [0, -1], [1, -1]]
|
|
||||||
]
|
|
||||||
O.prototype.srs = [
|
|
||||||
{[ROTATION.CW]: [], [ROTATION.CCW]: []}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class S extends Tetromino {}
|
|
||||||
S.prototype.minoesPosition = [
|
|
||||||
[[-1, 0], [0, 0], [0, -1], [1, -1]],
|
|
||||||
[[ 0, -1], [0, 0], [1, 0], [1, 1]],
|
|
||||||
[[-1, 1], [0, 0], [1, 0], [0, 1]],
|
|
||||||
[[-1, -1], [0, 0], [-1, 0], [0, 1]],
|
|
||||||
]
|
|
||||||
|
|
||||||
class T extends Tetromino {
|
|
||||||
get tSpin() {
|
|
||||||
if (this.lastRotation) {
|
|
||||||
let [a, b, c, d] = this.tSlots[this.facing]
|
|
||||||
.translate(this.center)
|
|
||||||
.map(minoPosition => !matrix.cellIsEmpty(minoPosition))
|
|
||||||
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 = [
|
|
||||||
[[-1, 0], [0, 0], [1, 0], [0, -1]],
|
|
||||||
[[0, -1], [0, 0], [1, 0], [0, 1]],
|
|
||||||
[[-1, 0], [0, 0], [1, 0], [0, 1]],
|
|
||||||
[[0, -1], [0, 0], [0, 1], [-1, 0]],
|
|
||||||
]
|
|
||||||
T.prototype.tSlots = [
|
|
||||||
[[-1, -1], [ 1, -1], [ 1, 1], [-1, 1]],
|
|
||||||
[[ 1, -1], [ 1, 1], [-1, 1], [-1, -1]],
|
|
||||||
[[ 1, 1], [-1, 1], [-1, -1], [ 1, -1]],
|
|
||||||
[[-1, 1], [-1, -1], [ 1, -1], [ 1, 1]],
|
|
||||||
]
|
|
||||||
|
|
||||||
class Z extends Tetromino {}
|
|
||||||
Z.prototype.minoesPosition = [
|
|
||||||
[[-1, -1], [0, -1], [0, 0], [ 1, 0]],
|
|
||||||
[[ 1, -1], [1, 0], [0, 0], [ 0, 1]],
|
|
||||||
[[-1, 0], [0, 0], [0, 1], [ 1, 1]],
|
|
||||||
[[ 0, -1], [-1, 0], [0, 0], [-1, 1]]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Settings {
|
|
||||||
constructor() {
|
|
||||||
this.form = settingsForm
|
|
||||||
this.load()
|
|
||||||
this.modal = new bootstrap.Modal('#settingsModal')
|
|
||||||
settingsModal.addEventListener('shown.bs.modal', () => resumeButton.focus())
|
|
||||||
}
|
|
||||||
|
|
||||||
load() {
|
|
||||||
for (let element of settingsForm.elements) {
|
|
||||||
if (element.name) {
|
|
||||||
if (localStorage[element.name]) element.value = localStorage[element.name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.document.selectedStyleSheetSet = stylesheetSelect.value
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
for (let element of settingsForm.elements) {
|
|
||||||
if (element.name) {
|
|
||||||
localStorage[element.name] = element.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.form.onsubmit = newGame
|
|
||||||
levelInput.name = "startLevel"
|
|
||||||
levelInput.disabled = false
|
|
||||||
titleHeader.innerHTML = "QUATUOR"
|
|
||||||
resumeButton.innerHTML = "Jouer"
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
resumeButton.disabled = false
|
|
||||||
settings.form.classList.remove('was-validated')
|
|
||||||
settings.modal.show()
|
|
||||||
settings.form.reportValidity()
|
|
||||||
}
|
|
||||||
|
|
||||||
getInputs() {
|
|
||||||
for (let input of this.form.querySelectorAll("input[type='text']")) {
|
|
||||||
this[input.name] = KEY_NAMES[input.value]
|
|
||||||
}
|
|
||||||
for (let input of this.form.querySelectorAll("input[type='number'], input[type='range']")) {
|
|
||||||
this[input.name] = input.valueAsNumber
|
|
||||||
}
|
|
||||||
for (let input of this.form.querySelectorAll("input[type='checkbox']")) {
|
|
||||||
this[input.name] = input.checked == true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.keyBind = {}
|
|
||||||
for (let actionName in playerActions) {
|
|
||||||
this.keyBind[settings[actionName]] = playerActions[actionName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeKey(input) {
|
|
||||||
prevValue = input.value
|
|
||||||
input.value = ""
|
|
||||||
input.onkeydown = function (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
input.value = KEY_NAMES[event.key]
|
|
||||||
input.blur()
|
|
||||||
}
|
|
||||||
input.onblur = function (event) {
|
|
||||||
if (input.value == "") input.value = prevValue
|
|
||||||
input.onkeydown = null
|
|
||||||
input.onblur = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Stats {
|
|
||||||
constructor() {
|
|
||||||
this.modal = new bootstrap.Modal('#statsModal')
|
|
||||||
this.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
load() {
|
|
||||||
this.highScore = Number(localStorage["highScore"]) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
levelInput.value = localStorage["startLevel"] || 1
|
|
||||||
this.score = 0
|
|
||||||
this.goal = 0
|
|
||||||
this.combo = 0
|
|
||||||
this.b2b = 0
|
|
||||||
this.startTime = new Date()
|
|
||||||
this.lockDelay = DELAY.LOCK
|
|
||||||
this.totalClearedLines = 0
|
|
||||||
this.nbQuatuors = 0
|
|
||||||
this.nbTSpin = 0
|
|
||||||
this.maxCombo = 0
|
|
||||||
this.maxB2B = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
set score(score) {
|
|
||||||
this._score = score
|
|
||||||
scoreCell.innerText = score.toLocaleString()
|
|
||||||
if (score > this.highScore) {
|
|
||||||
this.highScore = score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get score() {
|
|
||||||
return this._score
|
|
||||||
}
|
|
||||||
|
|
||||||
set highScore(highScore) {
|
|
||||||
this._highScore = highScore
|
|
||||||
highScoreCell.innerText = highScore.toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
get highScore() {
|
|
||||||
return this._highScore
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
levelInput.value = level
|
|
||||||
levelCell.innerText = level
|
|
||||||
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>NIVEAU<br/>${this.level}</h1>` })
|
|
||||||
}
|
|
||||||
|
|
||||||
get level() {
|
|
||||||
return this._level
|
|
||||||
}
|
|
||||||
|
|
||||||
set goal(goal) {
|
|
||||||
this._goal = goal
|
|
||||||
goalCell.innerText = goal
|
|
||||||
}
|
|
||||||
|
|
||||||
get goal() {
|
|
||||||
return this._goal
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
set time(time) {
|
|
||||||
this.startTime = new Date() - time
|
|
||||||
ticktack()
|
|
||||||
}
|
|
||||||
|
|
||||||
get time() {
|
|
||||||
return new Date() - this.startTime
|
|
||||||
}
|
|
||||||
|
|
||||||
lockDown(nbClearedLines, tSpin) {
|
|
||||||
this.totalClearedLines += nbClearedLines
|
|
||||||
if (nbClearedLines == 4) this.nbQuatuors++
|
|
||||||
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
|
|
||||||
|
|
||||||
// Sound
|
|
||||||
if (sfxVolumeRange.value) {
|
|
||||||
if (nbClearedLines == 4 || (nbClearedLines && tSpin)) playSound(quatuorSound)
|
|
||||||
else if (nbClearedLines) playSound(lineClearSound)
|
|
||||||
else if (tSpin) playSound(tSpinSound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 EN BOUT<br/>${b2bScore}`
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
messagesSpan.addNewChild("div", {
|
|
||||||
className: "zoom-in-animation",
|
|
||||||
style: "animation-delay: .4s",
|
|
||||||
innerHTML: `BOUT EN 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 EN BOUT`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.b2b = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
this.goal -= awardedLineClears
|
|
||||||
if (this.goal <= 0) this.level++
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
let time = stats.time
|
|
||||||
statsModalScoreCell.innerText = this.score.toLocaleString()
|
|
||||||
statsModalHighScoreCell.innerText = this.highScore.toLocaleString()
|
|
||||||
statsModalLevelCell.innerText = this.level
|
|
||||||
statsModalTimeCell.innerText = this.timeFormat.format(time)
|
|
||||||
statsModaltotalClearedLines.innerText = this.totalClearedLines
|
|
||||||
statsModaltotalClearedLinesPM.innerText = (stats.totalClearedLines * 60000 / time).toFixed(2)
|
|
||||||
statsModalNbQuatuors.innerText = this.nbQuatuors
|
|
||||||
statsModalNbTSpin.innerText = this.nbTSpin
|
|
||||||
statsModalMaxCombo.innerText = this.maxCombo
|
|
||||||
statsModalMaxB2B.innerText = this.maxB2B
|
|
||||||
this.modal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
localStorage["highScore"] = this.highScore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Stats.prototype.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
timeZone: "UTC"
|
|
||||||
})
|
|
||||||
|
|
||||||
function playSound(sound) {
|
|
||||||
sound.currentTime = 0
|
|
||||||
sound.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Game */
|
|
||||||
onanimationend = function (event) {
|
|
||||||
event.target.classList.remove(event.animationName)
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesSpan.onanimationend = function(event) {
|
|
||||||
event.target.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
let scheduler = new Scheduler()
|
let scheduler = new Scheduler()
|
||||||
let settings = new Settings()
|
let settings = new Settings()
|
||||||
let stats = new Stats()
|
let stats = new Stats()
|
||||||
@ -989,6 +225,14 @@ function lockDown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onanimationend = function (event) {
|
||||||
|
event.target.classList.remove(event.animationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesSpan.onanimationend = function(event) {
|
||||||
|
event.target.remove()
|
||||||
|
}
|
||||||
|
|
||||||
function gameOver() {
|
function gameOver() {
|
||||||
matrix.piece.locked = false
|
matrix.piece.locked = false
|
||||||
matrix.drawPiece()
|
matrix.drawPiece()
|
||||||
|
752
classes.js
Normal file
752
classes.js
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
/* Constants */
|
||||||
|
const TRANSLATION = {
|
||||||
|
NONE: [ 0, 0],
|
||||||
|
LEFT: [-1, 0],
|
||||||
|
RIGHT: [ 1, 0],
|
||||||
|
DOWN: [ 0, 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROTATION = {
|
||||||
|
CW: 1, // ClockWise
|
||||||
|
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",
|
||||||
|
"QUATUOR",
|
||||||
|
]
|
||||||
|
|
||||||
|
const DELAY = {
|
||||||
|
LOCK: 500,
|
||||||
|
FALL: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FACING = {
|
||||||
|
NORTH: 0,
|
||||||
|
EAST: 1,
|
||||||
|
SOUTH: 2,
|
||||||
|
WEST: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY_NAMES = new Proxy({
|
||||||
|
["ArrowLeft"] : "←",
|
||||||
|
["←"] : "ArrowLeft",
|
||||||
|
["ArrowRight"] : "→",
|
||||||
|
["→"] : "ArrowRight",
|
||||||
|
["ArrowUp"] : "↑",
|
||||||
|
["↑"] : "ArrowUp",
|
||||||
|
["ArrowDown"] : "↓",
|
||||||
|
["↓"] : "ArrowDown",
|
||||||
|
[" "] : "Espace",
|
||||||
|
["Espace"] : " ",
|
||||||
|
["Escape"] : "Échap.",
|
||||||
|
["Échap."] : "Escape",
|
||||||
|
["Backspace"] : "Ret. arrière",
|
||||||
|
["Ret. arrière"]: "Backspace",
|
||||||
|
["Enter"] : "Entrée",
|
||||||
|
["Entrée"] : "Enter",
|
||||||
|
}, {
|
||||||
|
get(obj, keyName) {
|
||||||
|
return keyName in obj? obj[keyName] : keyName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Customize Array to be use as position */
|
||||||
|
Object.defineProperties(Array.prototype, {
|
||||||
|
"x": {
|
||||||
|
get: function() { return this[0] },
|
||||||
|
set: function(x) { this[0] = x }
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
get: function() { return this[1] },
|
||||||
|
set: function(y) { this[1] = y }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
|
||||||
|
Array.prototype.mul = function(k) { return this.map(x => k * x) }
|
||||||
|
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
|
||||||
|
Array.prototype.rotate = function(rotation) { return [-rotation*this.y, rotation*this.x] }
|
||||||
|
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
|
||||||
|
|
||||||
|
|
||||||
|
HTMLElement.prototype.addNewChild = function(tag, properties) {
|
||||||
|
let child = document.createElement(tag)
|
||||||
|
for (key in properties) {
|
||||||
|
child[key] = properties[key]
|
||||||
|
}
|
||||||
|
this.appendChild(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 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 MinoesTable {
|
||||||
|
constructor(id) {
|
||||||
|
this.table = document.getElementById(id)
|
||||||
|
Array.from(this.table.getElementsByTagName("tr")).forEach((tr, row) => {
|
||||||
|
tr.style.setProperty('--row', row)
|
||||||
|
Array.from(tr.getElementsByTagName("td")).forEach((td, column) => {
|
||||||
|
td.style.setProperty('--column', column)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.rows = this.table.rows.length
|
||||||
|
this.columns = this.table.rows[0].childElementCount
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._piece = null
|
||||||
|
}
|
||||||
|
|
||||||
|
get piece() {
|
||||||
|
return this._piece
|
||||||
|
}
|
||||||
|
set piece(piece) {
|
||||||
|
this._piece = piece
|
||||||
|
this._piece.center = Array.from(this.init_center)
|
||||||
|
this.redraw()
|
||||||
|
this.drawPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMino(position, className) {
|
||||||
|
this.table.rows[position.y].cells[position.x].className = className
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPiece(piece=this.piece, className=piece.className) {
|
||||||
|
piece.minoesPosition[piece.facing]
|
||||||
|
.translate(piece.center)
|
||||||
|
.forEach(minoPosition => {
|
||||||
|
this.drawMino(minoPosition, className)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
for (let y=0; y<this.rows; y++) {
|
||||||
|
for (let x=0; x<this.columns; x++) {
|
||||||
|
this.drawMino([x, y], "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MinoesTable.prototype.init_center = [2, 2]
|
||||||
|
|
||||||
|
|
||||||
|
class NextQueue extends MinoesTable {
|
||||||
|
constructor() {
|
||||||
|
super("nextTable")
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.pieces = this.init_centers.map(center => {
|
||||||
|
let piece = new Tetromino.pick()
|
||||||
|
piece.center = Array.from(center)
|
||||||
|
return piece
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
shift() {
|
||||||
|
let fistPiece = this.pieces.shift()
|
||||||
|
this.pieces.push(new Tetromino.pick())
|
||||||
|
this.pieces.forEach((piece, i) => {
|
||||||
|
piece.center = Array.from(this.init_centers[i])
|
||||||
|
})
|
||||||
|
this.redraw()
|
||||||
|
return fistPiece
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
super.redraw()
|
||||||
|
this.pieces.forEach((piece) => {
|
||||||
|
this.drawPiece(piece)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NextQueue.prototype.init_centers = [[2, 2], [2, 5], [2, 8], [2, 11], [2, 14]]
|
||||||
|
|
||||||
|
|
||||||
|
class Matrix extends MinoesTable {
|
||||||
|
constructor() {
|
||||||
|
super("matrixTable")
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init()
|
||||||
|
this.blocks = Array(this.rows).fill().map(() => Array(this.columns))
|
||||||
|
this.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
cellIsEmpty(position) {
|
||||||
|
return 0 <= position.x && position.x < this.columns && 0 <= position.y && position.y < this.rows && !this.blocks[position.y][position.x]
|
||||||
|
}
|
||||||
|
|
||||||
|
get piece() {
|
||||||
|
return this._piece
|
||||||
|
}
|
||||||
|
set piece(piece) {
|
||||||
|
this._piece = piece
|
||||||
|
this._piece.center = Array.from(this.init_center)
|
||||||
|
this.ghost = piece.ghost
|
||||||
|
this.redraw()
|
||||||
|
this.drawPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPiece(piece=this.piece, className=piece.className) {
|
||||||
|
super.drawPiece(this.ghost, "")
|
||||||
|
this.ghost = piece.ghost
|
||||||
|
super.drawPiece(this.ghost)
|
||||||
|
if (piece.locked) className += " locking"
|
||||||
|
if (piece==this.piece && actionsQueue.length) className += " moving"
|
||||||
|
super.drawPiece(piece, className)
|
||||||
|
matrix.table.style.setProperty('--piece-column', this.piece.center.x)
|
||||||
|
matrix.table.style.setProperty('--piece-row', this.piece.center.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
for (let y=0; y<this.rows; y++) {
|
||||||
|
for (let x=0; x<this.columns; x++) {
|
||||||
|
if (this.table.rows[y].cells[x].classList != "trail-animation")
|
||||||
|
this.drawMino([x, y], this.blocks[y][x] || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock() {
|
||||||
|
let blocksPosition = this.piece.minoesPosition[this.piece.facing].translate(this.piece.center)
|
||||||
|
if (blocksPosition.some(position => position.y >= 4)) {
|
||||||
|
blocksPosition.forEach(position => {
|
||||||
|
this.blocks[position.y][position.x] = "locked " + this.piece.className
|
||||||
|
this.drawMino(position, this.piece.className)
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLines() {
|
||||||
|
let nbClearedLines = 0
|
||||||
|
for (let y=0; y<this.rows; y++) {
|
||||||
|
let row = this.blocks[y]
|
||||||
|
if (row.filter(lockedMino => lockedMino).length == this.columns) {
|
||||||
|
nbClearedLines++
|
||||||
|
this.blocks.splice(y, 1)
|
||||||
|
this.blocks.unshift(Array(matrix.columns))
|
||||||
|
this.table.rows[y].classList.add("cleared-line-animation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.redraw()
|
||||||
|
return nbClearedLines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Matrix.prototype.init_center = [4, 4]
|
||||||
|
|
||||||
|
|
||||||
|
class Tetromino {
|
||||||
|
static randomBag = []
|
||||||
|
static get pick() {
|
||||||
|
if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
|
||||||
|
return this.randomBag.pick()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(center, facing=0, className=this.constructor.name + " mino") {
|
||||||
|
this.center = center
|
||||||
|
this.className = className
|
||||||
|
this.facing = facing
|
||||||
|
this.lastRotation = false
|
||||||
|
this.rotationPoint4Used = false
|
||||||
|
this.holdEnabled = true
|
||||||
|
this.locked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
canMove(translation, rotation=ROTATION.NONE) {
|
||||||
|
let testCenter = this.center.add(translation)
|
||||||
|
let testFacing = rotation? (this.facing + rotation + 4) % 4: this.facing
|
||||||
|
let testMinoesPosition = this.minoesPosition[testFacing]
|
||||||
|
if (testMinoesPosition
|
||||||
|
.translate(testCenter)
|
||||||
|
.every(minoPosition => matrix.cellIsEmpty(minoPosition)))
|
||||||
|
return {center: testCenter, facing: testFacing}
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
move(translation, rotation=ROTATION.NONE, hardDropped=false) {
|
||||||
|
let success = this.canMove(translation, rotation)
|
||||||
|
if (success) {
|
||||||
|
scheduler.clearTimeout(lockDown)
|
||||||
|
matrix.drawPiece(this, hardDropped? "trail-animation" : "")
|
||||||
|
this.center = success.center
|
||||||
|
if (rotation) this.facing = success.facing
|
||||||
|
this.lastRotation = rotation
|
||||||
|
if (this.canMove(TRANSLATION.DOWN)) {
|
||||||
|
this.locked = false
|
||||||
|
} else {
|
||||||
|
this.locked = true
|
||||||
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||||
|
}
|
||||||
|
matrix.drawPiece()
|
||||||
|
return true
|
||||||
|
} else if (!hardDropped) {
|
||||||
|
// wallSound.play()
|
||||||
|
if (translation == TRANSLATION.DOWN) {
|
||||||
|
this.locked = true
|
||||||
|
if (!scheduler.timeoutTasks.has(lockDown))
|
||||||
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||||
|
matrix.drawPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(rotation) {
|
||||||
|
return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
|
||||||
|
if (this.move(translation, rotation)) {
|
||||||
|
if (rotationPoint == 4) this.rotationPoint4Used = true
|
||||||
|
favicon.href = this.favicon_href
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get ghost() {
|
||||||
|
let ghost = new this.constructor(Array.from(this.center), this.facing, "ghost " + this.className)
|
||||||
|
while (ghost.canMove(TRANSLATION.DOWN)) ghost.center.y++
|
||||||
|
return ghost
|
||||||
|
}
|
||||||
|
|
||||||
|
get favicon_href() {
|
||||||
|
return `favicons/${this.constructor.name}-${this.facing}.png`
|
||||||
|
}
|
||||||
|
|
||||||
|
get tSpin() {
|
||||||
|
return T_SPIN.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Super Rotation System
|
||||||
|
// freedom of movement = srs[piece.facing][rotation]
|
||||||
|
Tetromino.prototype.srs = [
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]] },
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]] },
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]] },
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]] },
|
||||||
|
]
|
||||||
|
|
||||||
|
class I extends Tetromino {}
|
||||||
|
I.prototype.minoesPosition = [
|
||||||
|
[[-1, 0], [0, 0], [1, 0], [2, 0]],
|
||||||
|
[[1, -1], [1, 0], [1, 1], [1, 2]],
|
||||||
|
[[-1, 1], [0, 1], [1, 1], [2, 1]],
|
||||||
|
[[0, -1], [0, 0], [0, 1], [0, 2]],
|
||||||
|
]
|
||||||
|
I.prototype.srs = [
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]] },
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]], [ROTATION.CCW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]] },
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]] },
|
||||||
|
{ [ROTATION.CW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]], [ROTATION.CCW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]] },
|
||||||
|
]
|
||||||
|
|
||||||
|
class J extends Tetromino {}
|
||||||
|
J.prototype.minoesPosition = [
|
||||||
|
[[-1, -1], [-1, 0], [0, 0], [1, 0]],
|
||||||
|
[[ 0, -1], [1, -1], [0, 0], [0, 1]],
|
||||||
|
[[ 1, 1], [-1, 0], [0, 0], [1, 0]],
|
||||||
|
[[ 0, -1], [-1, 1], [0, 0], [0, 1]],
|
||||||
|
]
|
||||||
|
|
||||||
|
class L extends Tetromino {}
|
||||||
|
L.prototype.minoesPosition = [
|
||||||
|
[[-1, 0], [0, 0], [1, 0], [ 1, -1]],
|
||||||
|
[[0, -1], [0, 0], [0, 1], [ 1, 1]],
|
||||||
|
[[-1, 0], [0, 0], [1, 0], [-1, 1]],
|
||||||
|
[[0, -1], [0, 0], [0, 1], [-1, -1]],
|
||||||
|
]
|
||||||
|
|
||||||
|
class O extends Tetromino {}
|
||||||
|
O.prototype.minoesPosition = [
|
||||||
|
[[0, 0], [1, 0], [0, -1], [1, -1]]
|
||||||
|
]
|
||||||
|
O.prototype.srs = [
|
||||||
|
{[ROTATION.CW]: [], [ROTATION.CCW]: []}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class S extends Tetromino {}
|
||||||
|
S.prototype.minoesPosition = [
|
||||||
|
[[-1, 0], [0, 0], [0, -1], [1, -1]],
|
||||||
|
[[ 0, -1], [0, 0], [1, 0], [1, 1]],
|
||||||
|
[[-1, 1], [0, 0], [1, 0], [0, 1]],
|
||||||
|
[[-1, -1], [0, 0], [-1, 0], [0, 1]],
|
||||||
|
]
|
||||||
|
|
||||||
|
class T extends Tetromino {
|
||||||
|
get tSpin() {
|
||||||
|
if (this.lastRotation) {
|
||||||
|
let [a, b, c, d] = this.tSlots[this.facing]
|
||||||
|
.translate(this.center)
|
||||||
|
.map(minoPosition => !matrix.cellIsEmpty(minoPosition))
|
||||||
|
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 = [
|
||||||
|
[[-1, 0], [0, 0], [1, 0], [0, -1]],
|
||||||
|
[[0, -1], [0, 0], [1, 0], [0, 1]],
|
||||||
|
[[-1, 0], [0, 0], [1, 0], [0, 1]],
|
||||||
|
[[0, -1], [0, 0], [0, 1], [-1, 0]],
|
||||||
|
]
|
||||||
|
T.prototype.tSlots = [
|
||||||
|
[[-1, -1], [ 1, -1], [ 1, 1], [-1, 1]],
|
||||||
|
[[ 1, -1], [ 1, 1], [-1, 1], [-1, -1]],
|
||||||
|
[[ 1, 1], [-1, 1], [-1, -1], [ 1, -1]],
|
||||||
|
[[-1, 1], [-1, -1], [ 1, -1], [ 1, 1]],
|
||||||
|
]
|
||||||
|
|
||||||
|
class Z extends Tetromino {}
|
||||||
|
Z.prototype.minoesPosition = [
|
||||||
|
[[-1, -1], [0, -1], [0, 0], [ 1, 0]],
|
||||||
|
[[ 1, -1], [1, 0], [0, 0], [ 0, 1]],
|
||||||
|
[[-1, 0], [0, 0], [0, 1], [ 1, 1]],
|
||||||
|
[[ 0, -1], [-1, 0], [0, 0], [-1, 1]]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
constructor() {
|
||||||
|
this.form = settingsForm
|
||||||
|
this.load()
|
||||||
|
this.modal = new bootstrap.Modal('#settingsModal')
|
||||||
|
settingsModal.addEventListener('shown.bs.modal', () => resumeButton.focus())
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
for (let element of settingsForm.elements) {
|
||||||
|
if (element.name) {
|
||||||
|
if (localStorage[element.name]) element.value = localStorage[element.name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.document.selectedStyleSheetSet = stylesheetSelect.value
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
for (let element of settingsForm.elements) {
|
||||||
|
if (element.name) {
|
||||||
|
localStorage[element.name] = element.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.form.onsubmit = newGame
|
||||||
|
levelInput.name = "startLevel"
|
||||||
|
levelInput.disabled = false
|
||||||
|
titleHeader.innerHTML = "QUATUOR"
|
||||||
|
resumeButton.innerHTML = "Jouer"
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
resumeButton.disabled = false
|
||||||
|
settings.form.classList.remove('was-validated')
|
||||||
|
settings.modal.show()
|
||||||
|
settings.form.reportValidity()
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputs() {
|
||||||
|
for (let input of this.form.querySelectorAll("input[type='text']")) {
|
||||||
|
this[input.name] = KEY_NAMES[input.value]
|
||||||
|
}
|
||||||
|
for (let input of this.form.querySelectorAll("input[type='number'], input[type='range']")) {
|
||||||
|
this[input.name] = input.valueAsNumber
|
||||||
|
}
|
||||||
|
for (let input of this.form.querySelectorAll("input[type='checkbox']")) {
|
||||||
|
this[input.name] = input.checked == true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keyBind = {}
|
||||||
|
for (let actionName in playerActions) {
|
||||||
|
this.keyBind[settings[actionName]] = playerActions[actionName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeKey(input) {
|
||||||
|
prevValue = input.value
|
||||||
|
input.value = ""
|
||||||
|
input.onkeydown = function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
input.value = KEY_NAMES[event.key]
|
||||||
|
input.blur()
|
||||||
|
}
|
||||||
|
input.onblur = function (event) {
|
||||||
|
if (input.value == "") input.value = prevValue
|
||||||
|
input.onkeydown = null
|
||||||
|
input.onblur = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Stats {
|
||||||
|
constructor() {
|
||||||
|
this.modal = new bootstrap.Modal('#statsModal')
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.highScore = Number(localStorage["highScore"]) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
levelInput.value = localStorage["startLevel"] || 1
|
||||||
|
this.score = 0
|
||||||
|
this.goal = 0
|
||||||
|
this.combo = 0
|
||||||
|
this.b2b = 0
|
||||||
|
this.startTime = new Date()
|
||||||
|
this.lockDelay = DELAY.LOCK
|
||||||
|
this.totalClearedLines = 0
|
||||||
|
this.nbQuatuors = 0
|
||||||
|
this.nbTSpin = 0
|
||||||
|
this.maxCombo = 0
|
||||||
|
this.maxB2B = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
set score(score) {
|
||||||
|
this._score = score
|
||||||
|
scoreCell.innerText = score.toLocaleString()
|
||||||
|
if (score > this.highScore) {
|
||||||
|
this.highScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get score() {
|
||||||
|
return this._score
|
||||||
|
}
|
||||||
|
|
||||||
|
set highScore(highScore) {
|
||||||
|
this._highScore = highScore
|
||||||
|
highScoreCell.innerText = highScore.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
get highScore() {
|
||||||
|
return this._highScore
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
levelInput.value = level
|
||||||
|
levelCell.innerText = level
|
||||||
|
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>NIVEAU<br/>${this.level}</h1>` })
|
||||||
|
}
|
||||||
|
|
||||||
|
get level() {
|
||||||
|
return this._level
|
||||||
|
}
|
||||||
|
|
||||||
|
set goal(goal) {
|
||||||
|
this._goal = goal
|
||||||
|
goalCell.innerText = goal
|
||||||
|
}
|
||||||
|
|
||||||
|
get goal() {
|
||||||
|
return this._goal
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
set time(time) {
|
||||||
|
this.startTime = new Date() - time
|
||||||
|
ticktack()
|
||||||
|
}
|
||||||
|
|
||||||
|
get time() {
|
||||||
|
return new Date() - this.startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
lockDown(nbClearedLines, tSpin) {
|
||||||
|
this.totalClearedLines += nbClearedLines
|
||||||
|
if (nbClearedLines == 4) this.nbQuatuors++
|
||||||
|
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
if (sfxVolumeRange.value) {
|
||||||
|
if (nbClearedLines == 4 || (nbClearedLines && tSpin)) playSound(quatuorSound)
|
||||||
|
else if (nbClearedLines) playSound(lineClearSound)
|
||||||
|
else if (tSpin) playSound(tSpinSound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 EN BOUT<br/>${b2bScore}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .4s",
|
||||||
|
innerHTML: `BOUT EN 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 EN BOUT`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.b2b = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goal -= awardedLineClears
|
||||||
|
if (this.goal <= 0) this.level++
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
let time = stats.time
|
||||||
|
statsModalScoreCell.innerText = this.score.toLocaleString()
|
||||||
|
statsModalHighScoreCell.innerText = this.highScore.toLocaleString()
|
||||||
|
statsModalLevelCell.innerText = this.level
|
||||||
|
statsModalTimeCell.innerText = this.timeFormat.format(time)
|
||||||
|
statsModaltotalClearedLines.innerText = this.totalClearedLines
|
||||||
|
statsModaltotalClearedLinesPM.innerText = (stats.totalClearedLines * 60000 / time).toFixed(2)
|
||||||
|
statsModalNbQuatuors.innerText = this.nbQuatuors
|
||||||
|
statsModalNbTSpin.innerText = this.nbTSpin
|
||||||
|
statsModalMaxCombo.innerText = this.maxCombo
|
||||||
|
statsModalMaxB2B.innerText = this.maxB2B
|
||||||
|
this.modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
localStorage["highScore"] = this.highScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stats.prototype.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC"
|
||||||
|
})
|
||||||
|
|
||||||
|
function playSound(sound) {
|
||||||
|
sound.currentTime = 0
|
||||||
|
sound.play()
|
||||||
|
}
|
@ -218,7 +218,8 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
||||||
<script src="app.js" language="Javascript" type="text/javascript"></script>
|
<script src="classes.js" language="Javascript" type="text/javascript"></script>
|
||||||
|
<script src="app.js" language="Javascript" type="text/javascript"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user