matrix.cellIsEmpty(minoCoord)))
return {center: testCenter, orientation: testOrientation}
else
return false
}
move(translation, rotation=ROTATION.NONE, clearClassName="") {
let success = this.canMove(translation, rotation)
if (success) {
scheduler.clearTimeout(lockDown)
matrix.drawPiece(this, clearClassName)
this.center = success.center
if (rotation) this.orientation = success.orientation
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 (translation == TRANSLATION.DOWN) {
this.locked = true
if (!scheduler.timeoutTasks.has(lockDown))
scheduler.setTimeout(lockDown, stats.lockDelay)
matrix.drawPiece()
}
}
rotate(rotation) {
return this.srs[this.orientation][rotation].some((translation, rotationPoint) => {
if (this.move(translation, rotation)) {
if (rotationPoint == 4) this.rotationPoint4Used = true
return true
}
})
}
get ghost() {
return new this.constructor(Array.from(this.center), this.orientation, "ghost " + this.className)
}
}
// Super Rotation System
// freedom of movement = srs[piece.orientation][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.minoesCoord = [
[[-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.minoesCoord = [
[[-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.minoesCoord = [
[[-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.minoesCoord = [
[[0, 0], [1, 0], [0, -1], [1, -1]]
]
O.prototype.srs = [
{[ROTATION.CW]: [], [ROTATION.CCW]: []}
]
class S extends Tetromino {}
S.prototype.minoesCoord = [
[[-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 {}
T.prototype.minoesCoord = [
[[-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.minoesCoord = [
[[-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() {
for (let input of settingsForm.getElementsByTagName("input")) {
if (localStorage[input.name]) input.value = localStorage[input.name]
}
arrOutput.value = arrInput.value + " ms"
dasOutput.value = dasInput.value + " ms"
settingsForm.onsubmit = newGame
this.modal = new bootstrap.Modal('#settingsModal')
document.getElementById('settingsModal').addEventListener('shown.bs.modal', () => {
resumeButton.focus()
})
}
load() {
for (let input of keyBindFielset.getElementsByTagName("input")) {
this[input.name] = KEY_NAMES[input.value] || input.value
localStorage[input.name] = input.value
}
for (let input of autorepearFieldset.getElementsByTagName("input")) {
this[input.name] = input.valueAsNumber
localStorage[input.name] = input.value
}
this.keyBind = {}
for (let actionName in playerActions) {
this.keyBind[settings[actionName]] = playerActions[actionName]
}
}
}
function changeKey(input) {
prevValue = input.value
input.value = "Touche ?"
input.onkeydown = function (event) {
event.preventDefault()
input.value = KEY_NAMES[event.key] || event.key
}
input.onblur = function (event) {
if (input.value == "Touche ?") input.value = prevValue
input.onkeydown = null
input.onblur = null
}
}
class Stats {
constructor() {
this.highScore = Number(localStorage["highScore"]) || 0
}
set score(score) {
this._score = score
scoreTd.innerText = score.toLocaleString()
if (score > this.highScore) {
this.highScore = score
}
}
get score() {
return this._score
}
set highScore(highScore) {
this._highScore = highScore
highScoreTd.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
levelTd.innerText = level
levelSpan.innerHTML = `LEVEL
${this.level}
`
levelSpan.classList.add("show-level-animation")
}
get level() {
return this._level
}
set goal(goal) {
this._goal = goal
goalTd.innerText = goal
}
get goal() {
return this._goal
}
}
/* Game */
onanimationend = function (event) {
event.target.classList.remove(event.animationName)
}
let scheduler = new Scheduler()
let settings = new Settings()
let stats = new Stats()
let holdQueue = new MinoesTable("holdTable")
let matrix = new PlayfieldMatrix("matrixTable")
let nextQueue = new NextQueue("nextTable")
function pause() {
document.onkeydown = null
document.onkeyup = null
scheduler.clearInterval(fall)
scheduler.clearTimeout(lockDown)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
resumeButton.disabled = false
settings.modal.show()
}
//window.onblur = pause()
pause()
function newGame(event) {
stats.lockDelay = DELAY.LOCK
resume(event)
levelInput.name = "level"
levelInput.disabled = true
resumeButton.innerHTML = "Reprendre"
event.target.onsubmit = resume
settingsModal["data-bs-backdrop"] = ""
stats.score = 0
stats.goal = 0
stats.level = levelInput.valueAsNumber
localStorage["startLevel"] = levelInput.value
generate()
}
function resume(event) {
event.preventDefault()
settings.load()
document.onkeydown = onkeydown
document.onkeyup = onkeyup
if (stats.fallPeriod) scheduler.setInterval(fall, stats.fallPeriod)
}
function generate(piece=nextQueue.shift()) {
matrix.piece = piece
if (matrix.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(fall, stats.fallPeriod)
} else {
gameOver()
}
}
let playerActions = {
moveLeft: () => matrix.piece.move(TRANSLATION.LEFT),
moveRight: () => matrix.piece.move(TRANSLATION.RIGHT),
rotateClockwise: () => matrix.piece.rotate(ROTATION.CW),
rotateCounterclockwise: () => matrix.piece.rotate(ROTATION.CCW),
softDrop: function() {
if (matrix.piece.move(TRANSLATION.DOWN)) stats.score++
},
hardDrop: function() {
scheduler.clearTimeout(lockDown)
//matrix.table.classList.add("hard-dropped-table-animation")
while (matrix.piece.move(TRANSLATION.DOWN, ROTATION.NONE, "hard-drop-animation")) stats.score +=2
lockDown()
},
hold: function() {
if (matrix.piece.holdEnabled) {
scheduler.clearInterval(fall)
scheduler.clearTimeout(lockDown)
matrix.piece.holdEnabled = false
matrix.piece.locked = false
matrix.piece.orientation = ORIENTATION.NORTH
if (holdQueue.piece) {
let piece = holdQueue.piece
holdQueue.piece = matrix.piece
generate(piece)
} else {
holdQueue.piece = matrix.piece
generate()
}
}
},
pause: pause,
}
// Handle player inputs
const REPEATABLE_ACTIONS = [
playerActions.moveLeft,
playerActions.moveRight,
playerActions.softDrop
]
pressedKeys = new Set()
actionsQueue = []
function onkeydown(event) {
if (event.key in settings.keyBind) {
event.preventDefault()
if (!pressedKeys.has(event.key)) {
pressedKeys.add(event.key)
action = settings.keyBind[event.key]
action()
if (REPEATABLE_ACTIONS.includes(action)) {
actionsQueue.unshift(action)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
scheduler.setTimeout(repeat, settings.das)
}
}
}
}
function repeat() {
if (actionsQueue.length) {
actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arr)
}
}
function autorepeat() {
if (actionsQueue.length) {
actionsQueue[0]()
} else {
scheduler.clearInterval(autorepeat)
}
}
function onkeyup(event) {
if (event.key in settings.keyBind) {
event.preventDefault()
pressedKeys.delete(event.key)
action = settings.keyBind[event.key]
if (actionsQueue.includes(action)) {
actionsQueue.splice(actionsQueue.indexOf(action), 1)
if (!actionsQueue.length) {
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
}
}
}
}
function fall() {
matrix.piece.move(TRANSLATION.DOWN)
}
function lockDown() {
scheduler.clearTimeout(lockDown)
scheduler.clearInterval(fall)
lockedMinoesCoord = matrix.piece.minoesCoord[matrix.piece.orientation]
.translate(matrix.piece.center)
if (lockedMinoesCoord.every(minoCoord => minoCoord.y < 4)) {
gameOver()
} else {
lockedMinoesCoord.forEach(minoCoord => {
matrix.lockedMinoes[minoCoord.y][minoCoord.x] = matrix.piece.className
matrix.drawMino(minoCoord, matrix.piece.className)
})
// T-spin
let tSpin = T_SPIN.NONE
if (matrix.piece.lastRotation && matrix.piece.constructor == T) {
let [a, b, c, d] = matrix.piece.tSlots[matrix.piece.orientation]
.translate(matrix.piece.center)
.map(minoCoord => !matrix.cellIsEmpty(minoCoord))
if (a && b && (c || d))
tSpin = T_SPIN.T_SPIN
else if (c && d && (a || b))
tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI
}
// Cleared lines
let clearedLines = Array.from(new Set(lockedMinoesCoord.map(minoCoord => minoCoord.y)))
.filter(y => matrix.lockedMinoes[y].filter(lockedMino => lockedMino).length == matrix.columns)
for (y of clearedLines) {
matrix.lockedMinoes.splice(y, 1)
matrix.lockedMinoes.unshift(Array(matrix.columns))
matrix.table.rows[y].classList.add("line-cleared-animation")
}
let nbClearedLines = clearedLines.length
if (nbClearedLines || tSpin) {
matrix.redraw()
stats.goal -= nbClearedLines
stats.score += SCORES[tSpin][nbClearedLines]
messagesSpan.innerHTML = ""
if (tSpin) messagesSpan.innerHTML += `${tSpin}
\n`
if (nbClearedLines) messagesSpan.innerHTML += `${CLEARED_LINES_NAMES[nbClearedLines]}
\n`
messagesSpan.innerHTML += `${SCORES[tSpin][nbClearedLines]}
\n`
}
if (stats.goal <= 0) {
stats.level++
}
generate()
}
}
function gameOver() {
console.log("GAME OVER")
matrix.piece.locked = false
matrix.drawPiece()
document.onkeydown = null
document.onkeyup = null
localStorage["highScore"] = stats.highScore
levelSpan.innerHTML = "GAME
OVER
"
levelSpan.classList.add("show-level-animation")
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}