898 lines
27 KiB
JavaScript
898 lines
27 KiB
JavaScript
// Constants
|
|
const NEXT_PIECES = 6
|
|
const MATRIX_INVISIBLE_ROWS = 4
|
|
const START_POSITION = {
|
|
HOLD: [2, 3],
|
|
MATRIX: [4, 3],
|
|
NEXT: Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+3])
|
|
}
|
|
|
|
const CLASSNAME = {
|
|
EMPTY_CELL: "",
|
|
MINO: "mino",
|
|
LOCKED: "locked",
|
|
TRAIL: "mino trail",
|
|
GHOST: "ghost",
|
|
CLEARED_LINE: "cleared-line",
|
|
MESSAGE_SPAN_FADE_OUT: "messageSpan-fade-out"
|
|
}
|
|
const DELAY = {
|
|
LOCK: 500,
|
|
FALL: 1000,
|
|
ANIMATION: 200,
|
|
MESSAGE: 700
|
|
}
|
|
const MOVEMENT = {
|
|
LEFT: [-1, 0],
|
|
RIGHT: [ 1, 0],
|
|
DOWN: [ 0, 1]
|
|
}
|
|
const SPIN = {
|
|
CW: 1, // ClockWise
|
|
CCW: -1 // Counterstats.ClockWise
|
|
}
|
|
const T_SPIN = {
|
|
NONE: "",
|
|
MINI: "MINI T-SPIN",
|
|
T_SPIN: "T-SPIN"
|
|
}
|
|
const T_SLOT = {
|
|
A: 0,
|
|
B: 1,
|
|
C: 3,
|
|
D: 2
|
|
}
|
|
const T_SLOT_POS = [
|
|
[-1, -1],
|
|
[1, -1],
|
|
[1, 1],
|
|
[-1, 1]
|
|
]
|
|
const SCORES = [
|
|
{linesClearedName: "", "": 000, "MINI T-SPIN": 100, "T-SPIN": 400},
|
|
{linesClearedName: "SINGLE", "": 100, "MINI T-SPIN": 200, "T-SPIN": 800},
|
|
{linesClearedName: "DOUBLE", "": 300, "T-SPIN": 1200},
|
|
{linesClearedName: "TRIPLE", "": 500, "T-SPIN": 1600},
|
|
{linesClearedName: "TETRIS", "": 800},
|
|
]
|
|
const STATE = {
|
|
WAITING: "WAITING",
|
|
PLAYING: "PLAYING",
|
|
PAUSED: "PAUSE",
|
|
GAME_OVER: "GAME OVER",
|
|
}
|
|
const RETRIES = 3
|
|
|
|
|
|
// Customize Array to be use as coordinates
|
|
Object.defineProperty(Array.prototype, "x", {
|
|
get: function () { return this[0] },
|
|
set: function (x) { this[0] = x}
|
|
})
|
|
Object.defineProperty(Array.prototype, "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(spin) { return [-spin*this.y, spin*this.x] }
|
|
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
|
|
|
|
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
randomBag = []
|
|
class Tetromino {
|
|
constructor(position=null, shape=null) {
|
|
this.pos = position
|
|
this.orientation = 0
|
|
this.rotatedLast = false
|
|
this.rotationPoint5Used = false
|
|
this.holdEnabled = true
|
|
this.locked = false
|
|
this.srs = {} // Super Rotation System
|
|
this.srs[SPIN.CW] = [
|
|
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]],
|
|
[[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]],
|
|
[[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]],
|
|
[[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]],
|
|
]
|
|
this.srs[SPIN.CCW] = [
|
|
[[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]],
|
|
[[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]],
|
|
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]],
|
|
[[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]],
|
|
]
|
|
if (shape)
|
|
this.shape = shape
|
|
else {
|
|
if (!randomBag.length)
|
|
randomBag = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
|
|
this.shape = randomBag.pick()
|
|
}
|
|
switch(this.shape) {
|
|
case 'I':
|
|
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]]
|
|
this.srs[SPIN.CW] = [
|
|
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
|
|
[[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]],
|
|
[[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]],
|
|
[[ 0, 1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]],
|
|
]
|
|
this.srs[SPIN.CCW] = [
|
|
[[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]],
|
|
[[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]],
|
|
[[ 0, -1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]],
|
|
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
|
|
]
|
|
break
|
|
case 'J':
|
|
this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]]
|
|
break
|
|
case 'L':
|
|
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]]
|
|
break
|
|
case 'O':
|
|
this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]]
|
|
this.srs[SPIN.CW] = [[]]
|
|
this.srs[SPIN.CCW] = [[]]
|
|
break
|
|
case 'S':
|
|
this.minoesPos = [[-1, 0], [0, 0], [0, -1], [1, -1]]
|
|
break
|
|
case 'T':
|
|
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]]
|
|
break
|
|
case 'Z':
|
|
this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]]
|
|
break
|
|
}
|
|
this.className = CLASSNAME.MINO + " " + this.shape
|
|
}
|
|
|
|
get minoesAbsPos() {
|
|
return this.minoesPos.translate(this.pos)
|
|
}
|
|
|
|
get ghost() {
|
|
var ghost = new Tetromino(Array.from(this.pos), this.shape)
|
|
ghost.minoesPos = Array.from(this.minoesPos)
|
|
ghost.className = CLASSNAME.GHOST
|
|
return ghost
|
|
}
|
|
}
|
|
|
|
|
|
class MinoesTable {
|
|
constructor(id) {
|
|
this.table = document.getElementById(id)
|
|
this.rows = this.table.rows.length
|
|
this.columns = this.table.rows[0].childElementCount
|
|
}
|
|
|
|
drawMino(x, y, className) {
|
|
this.table.rows[y].cells[x].className = className
|
|
}
|
|
|
|
drawPiece(piece=this.piece, className=piece.locked ? CLASSNAME.LOCKED + " "+ piece.className: piece.className) {
|
|
piece.minoesAbsPos.forEach(pos => this.drawMino(...pos, className))
|
|
}
|
|
|
|
clearTable() {
|
|
for(var y = 0; y < this.rows; y++) {
|
|
for (var x = 0; x < this.columns; x++) {
|
|
this.drawMino(x, y, CLASSNAME.EMPTY_CELL)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class HoldQueue extends MinoesTable {
|
|
constructor() {
|
|
super("holdTable")
|
|
}
|
|
|
|
newGame() {
|
|
this.piece = null
|
|
}
|
|
|
|
draw() {
|
|
this.clearTable()
|
|
if (this.piece)
|
|
this.drawPiece(this.piece)
|
|
}
|
|
}
|
|
|
|
|
|
class Matrix extends MinoesTable {
|
|
constructor() {
|
|
super("matrixTable")
|
|
}
|
|
|
|
newGame() {
|
|
this.lockedMinoes = Array.from(Array(this.rows), row => Array(this.columns))
|
|
this.piece = null
|
|
this.clearedLines = []
|
|
this.trail = {
|
|
minoesPos: [],
|
|
height: 0
|
|
}
|
|
}
|
|
|
|
cellIsOccupied(x, y) {
|
|
return 0 <= x && x < this.columns && y < this.rows ? Boolean(this.lockedMinoes[y][x]) : true
|
|
}
|
|
|
|
spaceToMove(minoesAbsPos) {
|
|
return !minoesAbsPos.some(pos => this.cellIsOccupied(...pos))
|
|
}
|
|
|
|
draw() {
|
|
this.clearTable()
|
|
|
|
// ghost
|
|
if (showGhostCheckbox.value && !this.piece.locked && state != STATE.GAME_OVER) {
|
|
for (var ghost = this.piece.ghost; this.spaceToMove(ghost.minoesAbsPos); ghost.pos.y++) {}
|
|
ghost.pos.y--
|
|
this.drawPiece(ghost)
|
|
}
|
|
|
|
// trail
|
|
if (this.trail.height) {
|
|
this.trail.minoesPos.forEach(pos => {
|
|
for (var y = pos.y; y < pos.y + this.trail.height; y++)
|
|
this.drawMino(pos.x, y, CLASSNAME.TRAIL)
|
|
})
|
|
}
|
|
|
|
this.drawPiece(this.piece)
|
|
|
|
// locked minoes
|
|
for (var y = 0; y < this.rows; y++) {
|
|
for (var x = 0; x < this.columns; x++) {
|
|
if (this.clearedLines.includes(y))
|
|
this.drawMino(x, y, CLASSNAME.CLEARED_LINE)
|
|
else if (this.lockedMinoes[y][x])
|
|
this.drawMino(x, y, this.lockedMinoes[y][x])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class NextQueue extends MinoesTable {
|
|
constructor() {
|
|
super("nextTable")
|
|
}
|
|
|
|
newGame() {
|
|
this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(START_POSITION.NEXT[k]))
|
|
}
|
|
|
|
draw() {
|
|
this.clearTable()
|
|
this.pieces.forEach(piece => this.drawPiece(piece))
|
|
}
|
|
}
|
|
|
|
|
|
class Stats {
|
|
constructor() {
|
|
this.scoreCell = document.getElementById("score")
|
|
this.highScoreCell = document.getElementById("highScore")
|
|
this.timeCell = document.getElementById("time")
|
|
this.levelCell = document.getElementById("level")
|
|
this.goalCell = document.getElementById("goal")
|
|
this.clearedLinesCell = document.getElementById("clearedLines")
|
|
this.highScore = Number(localStorage.getItem('highScore'))
|
|
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
|
this.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
timeZone: "UTC"
|
|
})
|
|
}
|
|
|
|
newGame() {
|
|
this.score = 0
|
|
this.goal = 0
|
|
this.goalCell.innerText = this.goal
|
|
this.clearedLines = 0
|
|
this.clearedLinesCell.innerText = this.clearedLines
|
|
this.time = 0
|
|
this.timeCell.innerText = this.timeFormat.format(0)
|
|
this.combo = -1
|
|
this.lockDelay = DELAY.LOCK
|
|
this.fallPeriod = DELAY.FALL
|
|
this.b2bSequence = false
|
|
}
|
|
|
|
get score() {
|
|
return this._score
|
|
}
|
|
|
|
set score(score) {
|
|
this._score = score
|
|
this.scoreCell.innerText = this._score.toLocaleString()
|
|
if (score > this.highScore) {
|
|
this.highScore = score
|
|
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
|
}
|
|
document.title = `Webtris - Score : ${score}`
|
|
}
|
|
|
|
newLevel(level=null) {
|
|
this.level = level || this.level + 1
|
|
location.hash = this.level
|
|
this.levelCell.innerText = this.level
|
|
printTempTexts(`NIVEAU<br/>${this.level}`)
|
|
this.goal += 5 * this.level
|
|
this.goalCell.innerText = this.goal
|
|
if (this.level <= 20)
|
|
this.fallPeriod = 1000 * Math.pow(0.8 - ((this.level - 1) * 0.007), this.level - 1)
|
|
if (this.level > 15)
|
|
this.lockDelay = 500 * Math.pow(0.9, this.level - 15)
|
|
}
|
|
|
|
lockDown(tSpin, clearedLines) {
|
|
var patternName = []
|
|
var patternScore = 0
|
|
var b2bScore = 0
|
|
var comboScore = 0
|
|
|
|
if (tSpin)
|
|
patternName.push(tSpin)
|
|
if (clearedLines) {
|
|
patternName.push(SCORES[clearedLines].linesClearedName)
|
|
this.combo++
|
|
} else
|
|
this.combo = -1
|
|
|
|
if (clearedLines || tSpin) {
|
|
this.clearedLines += clearedLines
|
|
this.clearedLinesCell.innerText = this.clearedLines
|
|
patternScore = SCORES[clearedLines][tSpin] * this.level
|
|
this.goal -= clearedLines
|
|
this.goalCell.innerText = this.goal
|
|
patternName = patternName.join("\n")
|
|
}
|
|
|
|
if (this.b2bSequence) {
|
|
if ((clearedLines == 4) || (tSpin && clearedLines)) {
|
|
b2bScore = patternScore / 2
|
|
} else if ((0 < clearedLines) && (clearedLines < 4) && !tSpin) {
|
|
this.b2bSequence = false
|
|
}
|
|
} else if ((clearedLines == 4) || (tSpin && clearedLines)) {
|
|
this.b2bSequence = true
|
|
}
|
|
|
|
if (this.combo >= 1)
|
|
comboScore = (clearedLines == 1 ? 20 : 50) * this.combo * this.level
|
|
|
|
if (patternScore) {
|
|
var messages = [patternName, patternScore]
|
|
if (b2bScore)
|
|
messages.push(`BACK TO BACK BONUS`, b2bScore)
|
|
if (comboScore)
|
|
messages.push(`COMBO x${this.combo}`, comboScore)
|
|
printTempTexts(messages.join("<br/>"))
|
|
}
|
|
|
|
this.score += patternScore + comboScore + b2bScore
|
|
}
|
|
}
|
|
|
|
|
|
class Settings {
|
|
constructor() {
|
|
this.keyBind = {}
|
|
for (let button of settingsSection.getElementsByTagName("button")) {
|
|
let keyName = localStorage.getItem(button.id)
|
|
if (keyName) {
|
|
button.innerHTML = keyName
|
|
this.keyBind[keyName == "Space"? " ": keyName] = playerAction[button.id]
|
|
}
|
|
}
|
|
|
|
let autorepeatDelay = localStorage.getItem("autorepeatDelay")
|
|
if (autorepeatDelay) {
|
|
autorepeatDelayRange.value = autorepeatDelay
|
|
autorepeatDelayRange.oninput()
|
|
}
|
|
let autorepeatPeriod = localStorage.getItem("autorepeatPeriod")
|
|
if (autorepeatPeriod) {
|
|
autorepeatPeriodRange.value = autorepeatPeriod
|
|
autorepeatPeriodRange.oninput()
|
|
}
|
|
|
|
let themeName = localStorage.getItem("themeName")
|
|
if (themeName) themeSelect.value = themeName
|
|
let showGhost = localStorage.getItem("showGhost")
|
|
if (showGhost) showGhostCheckbox.checked = showGhost == "true"
|
|
|
|
let startLevel = localStorage.getItem("startLevel")
|
|
if (startLevel) startLevelInput.value = startLevel
|
|
}
|
|
|
|
applyTheme = () => new Promise((resolve, reject) => {
|
|
var link = document.createElement('link')
|
|
link.id = 'theme'
|
|
link.rel = 'stylesheet'
|
|
link.type = 'text/css'
|
|
link.href = `themes/${themeSelect.value}/style.css`
|
|
link.media = 'all'
|
|
link.onload = resolve
|
|
document.head.appendChild(link)
|
|
})
|
|
|
|
save() {
|
|
for (let button of settingsSection.getElementsByTagName("button")) {
|
|
localStorage.setItem(button.id, button.innerHTML)
|
|
}
|
|
localStorage.setItem("autorepeatDelay", autorepeatDelayRange.value)
|
|
localStorage.setItem("autorepeatPeriod", autorepeatPeriodRange.value)
|
|
localStorage.setItem("themeName", themeSelect.value)
|
|
localStorage.setItem("showGhost", showGhostCheckbox.checked)
|
|
localStorage.setItem("startLevel", startLevelInput.value)
|
|
}
|
|
|
|
waitKey(button) {
|
|
document.onkeydown = null
|
|
document.onkeyup = null
|
|
button.previousKey = button.innerHTML
|
|
button.innerHTML = "Touche ?"
|
|
button.onkeyup = function(event) {
|
|
event.preventDefault()
|
|
button.innerHTML = (event.key == " ") ? "Space" : event.key
|
|
settings.keyBind[event.key] = playerAction[button.id]
|
|
button.onkeyup = null
|
|
button.onblur = null
|
|
document.onkeydown = onkeydown
|
|
document.onkeyup = onkeyup
|
|
}
|
|
button.onblur = function(event) {
|
|
button.innerHTML = button.previousKey
|
|
button.onkeyup = null
|
|
button.onblur = null
|
|
document.onkeydown = onkeydown
|
|
document.onkeyup = onkeyup
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Functions
|
|
|
|
// Game logic
|
|
state = STATE.WAITING
|
|
|
|
function newGame(startLevel) {
|
|
startButton.blur()
|
|
|
|
settings.save()
|
|
|
|
holdQueue.newGame()
|
|
matrix.newGame()
|
|
nextQueue.newGame()
|
|
stats.newGame()
|
|
|
|
startSection.style.display = "none"
|
|
gameSection.style.display = "block"
|
|
settingsSection.style.display = "none"
|
|
footer.style.display = "none"
|
|
|
|
state = STATE.PLAYING
|
|
pressedKeys = new Set()
|
|
actionsToRepeat = []
|
|
scheduler.setInterval(stats.clock, 1000)
|
|
document.onkeydown = onkeydown
|
|
document.onkeyup = onkeyup
|
|
newLevel(startLevel)
|
|
}
|
|
|
|
function newLevel(level) {
|
|
stats.newLevel(level)
|
|
generationPhase()
|
|
}
|
|
|
|
function generationPhase(held_piece=null) {
|
|
if (!held_piece) {
|
|
matrix.piece = nextQueue.pieces.shift()
|
|
nextQueue.pieces.push(new Tetromino())
|
|
nextQueue.pieces.forEach((piece, i) => piece.pos = START_POSITION.NEXT[i])
|
|
}
|
|
nextQueue.draw()
|
|
matrix.piece.pos = START_POSITION.MATRIX
|
|
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos))){
|
|
scheduler.clearInterval(lockPhase)
|
|
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
|
fallingPhase()
|
|
} else
|
|
gameOver()
|
|
}
|
|
|
|
function lockPhase() {
|
|
move(MOVEMENT.DOWN)
|
|
}
|
|
|
|
function fallingPhase() {
|
|
scheduler.clearTimeout(lockDown)
|
|
matrix.piece.locked = false
|
|
matrix.draw()
|
|
}
|
|
|
|
function lockDown(){
|
|
scheduler.clearInterval(lockPhase)
|
|
if (matrix.piece.minoesAbsPos.every(pos => pos.y < MATRIX_INVISIBLE_ROWS)) {
|
|
matrix.piece.locked = false
|
|
matrix.draw()
|
|
gameOver()
|
|
} else {
|
|
matrix.piece.minoesAbsPos.forEach(pos => matrix.lockedMinoes[pos.y][pos.x] = matrix.piece.className)
|
|
|
|
// T-Spin detection
|
|
var tSpin = T_SPIN.NONE
|
|
if (matrix.piece.rotatedLast && matrix.piece.shape == "T") {
|
|
const tSlots = T_SLOT_POS.translate(matrix.piece.pos).map(pos => matrix.cellIsOccupied(...pos)),
|
|
a = tSlots[(matrix.piece.orientation+T_SLOT.A)%4],
|
|
b = tSlots[(matrix.piece.orientation+T_SLOT.B)%4],
|
|
c = tSlots[(matrix.piece.orientation+T_SLOT.C)%4],
|
|
d = tSlots[(matrix.piece.orientation+T_SLOT.D)%4]
|
|
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
|
|
}
|
|
|
|
// Complete lines
|
|
matrix.clearedLines = []
|
|
matrix.lockedMinoes.forEach((row, y) => {
|
|
if (row.filter(lockedMino => lockedMino.length).length == matrix.columns) {
|
|
matrix.lockedMinoes.splice(y, 1)
|
|
matrix.lockedMinoes.unshift(Array(matrix.columns))
|
|
matrix.clearedLines.push(y)
|
|
}
|
|
})
|
|
|
|
stats.lockDown(tSpin, matrix.clearedLines.length)
|
|
matrix.draw()
|
|
scheduler.setTimeout(clearLinesCleared, DELAY.ANIMATION)
|
|
|
|
if (stats.goal <= 0)
|
|
newLevel()
|
|
else
|
|
generationPhase()
|
|
}
|
|
}
|
|
|
|
function clearLinesCleared() {
|
|
matrix.clearedLines = []
|
|
matrix.draw()
|
|
}
|
|
|
|
function gameOver() {
|
|
state = STATE.GAME_OVER
|
|
document.onkeydown = null
|
|
document.onkeyup = null
|
|
messageSpan.innerHTML = "GAME<br/>OVER"
|
|
scheduler.clearInterval(lockPhase)
|
|
scheduler.clearTimeout(lockDown)
|
|
scheduler.clearInterval(stats.clock)
|
|
|
|
var info = `GAME OVER\nScore : ${stats.score.toLocaleString()}`
|
|
if (stats.score == stats.highScore) {
|
|
localStorage.setItem('highScore', stats.highScore)
|
|
info += "\nBravo ! Vous avez battu votre précédent record."
|
|
}
|
|
|
|
var retry = 0
|
|
var fd = new FormData()
|
|
fd.append("score", stats.score)
|
|
var request = new XMLHttpRequest()
|
|
request.onload = function(event) {
|
|
if (event.target.responseText == "true") {
|
|
var player = prompt(info + "\nBravo ! Vous êtes dans le Top 20.\nEntrez votre nom pour publier votre score :" , localStorage.getItem("player") || "")
|
|
if (player && player.length) {
|
|
localStorage.setItem("player", player)
|
|
fd.append("player", player)
|
|
request = new XMLHttpRequest()
|
|
request.onload = function(event) {
|
|
open("leaderboard.php")
|
|
}
|
|
request.onerror = function(event) {
|
|
if (confirm('Erreur de connexion.\nRéessayer ?')) {
|
|
request.open('POST', 'publish.php')
|
|
request.send(fd)
|
|
}
|
|
}
|
|
request.open('POST', 'publish.php')
|
|
request.send(fd)
|
|
} else
|
|
alert(info)
|
|
}
|
|
}
|
|
request.onerror = function(event) {
|
|
retry++
|
|
if (retry < RETRIES) {
|
|
request.open('POST', 'inleaderboard.php')
|
|
request.send(fd)
|
|
} else
|
|
alert(info)
|
|
}
|
|
request.open('POST', 'inleaderboard.php')
|
|
request.send(fd)
|
|
|
|
location.hash = "game-over"
|
|
|
|
startSection.style.display = "block"
|
|
footer.style.display = "block"
|
|
}
|
|
|
|
function move(movement, testMinoesPos=matrix.piece.minoesPos, hardDrop=false) {
|
|
const testPos = matrix.piece.pos.add(movement)
|
|
if (matrix.spaceToMove(testMinoesPos.translate(testPos))) {
|
|
matrix.piece.pos = testPos
|
|
matrix.piece.minoesPos = testMinoesPos
|
|
matrix.piece.rotatedLast = false
|
|
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN))))
|
|
fallingPhase()
|
|
else if (!hardDrop) {
|
|
matrix.piece.locked = true
|
|
scheduler.clearTimeout(lockDown)
|
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
}
|
|
if (!hardDrop)
|
|
matrix.draw()
|
|
return true
|
|
} else {
|
|
if (movement == MOVEMENT.DOWN) {
|
|
matrix.piece.locked = true
|
|
if (!scheduler.timeoutTasks.has(lockDown))
|
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
matrix.draw()
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
function rotate(spin) {
|
|
const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin))
|
|
let rotationPoint = 1
|
|
for (const movement of matrix.piece.srs[spin][matrix.piece.orientation]) {
|
|
if (move(movement, test_minoes_pos)) {
|
|
matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4
|
|
matrix.piece.rotatedLast = true
|
|
if (rotationPoint == 5)
|
|
matrix.piece.rotationPoint5Used = true
|
|
return true
|
|
}
|
|
rotationPoint++
|
|
}
|
|
return false
|
|
}
|
|
|
|
function pause() {
|
|
state = STATE.PAUSED
|
|
location.hash = "pause"
|
|
stats.startTime = performance.now() - stats.startTime
|
|
actionsToRepeat = []
|
|
scheduler.clearInterval(lockPhase)
|
|
scheduler.clearTimeout(lockDown)
|
|
scheduler.clearTimeout(autorepeat)
|
|
scheduler.clearInterval(stats.clock)
|
|
scheduler.clearInterval(delTempTexts)
|
|
holdQueue.draw()
|
|
matrix.draw()
|
|
nextQueue.draw()
|
|
header.style.display = "block"
|
|
gameSection.style.display = "none"
|
|
settingsSection.style.display = "block"
|
|
}
|
|
|
|
function resume() {
|
|
settings.save()
|
|
settingsSection.style.display = "none"
|
|
gameSection.style.display = "block"
|
|
location.hash = stats.level
|
|
state = STATE.PLAYING
|
|
stats.startTime = performance.now() - stats.startTime
|
|
messageSpan.innerHTML = ""
|
|
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
|
if (matrix.piece.locked)
|
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
scheduler.setInterval(stats.clock, 1000)
|
|
holdQueue.draw()
|
|
matrix.draw()
|
|
nextQueue.draw()
|
|
if (tempTexts.length)
|
|
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
|
}
|
|
|
|
playerAction = {
|
|
moveLeft: function () {
|
|
move(MOVEMENT.LEFT)
|
|
},
|
|
|
|
moveRight: function () {
|
|
move(MOVEMENT.RIGHT)
|
|
},
|
|
|
|
softDrop: function () {
|
|
if (move(MOVEMENT.DOWN))
|
|
stats.score++
|
|
},
|
|
|
|
hardDrop: function () {
|
|
scheduler.clearInterval(lockPhase)
|
|
scheduler.clearTimeout(lockDown)
|
|
matrix.trail.minoesPos = Array.from(matrix.piece.minoesAbsPos)
|
|
for (matrix.trail.height = 0; move(MOVEMENT.DOWN, matrix.piece.minoesPos, true); matrix.trail.height++) {}
|
|
stats.score += 2 * matrix.trail.height
|
|
matrix.draw()
|
|
lockDown()
|
|
scheduler.setTimeout(() => {matrix.trail.height = 0; matrix.draw()}, DELAY.ANIMATION)
|
|
},
|
|
|
|
rotateCW: function () {
|
|
rotate(SPIN.CW)
|
|
},
|
|
|
|
rotateCCW: function () {
|
|
rotate(SPIN.CCW)
|
|
},
|
|
|
|
hold: function () {
|
|
if (matrix.piece.holdEnabled) {
|
|
scheduler.clearInterval(move)
|
|
scheduler.clearInterval(lockDown)
|
|
var shape = matrix.piece.shape
|
|
matrix.piece = holdQueue.piece
|
|
holdQueue.piece = new Tetromino(START_POSITION.HOLD, shape)
|
|
holdQueue.piece.holdEnabled = false
|
|
holdQueue.draw()
|
|
generationPhase(matrix.piece)
|
|
matrix.piece.holdEnabled = false
|
|
}
|
|
},
|
|
|
|
pauseResume: function() {
|
|
if (state == STATE.PLAYING) {
|
|
pause()
|
|
} else {
|
|
resume()
|
|
}
|
|
}
|
|
}
|
|
|
|
function autorepeat() {
|
|
if (actionsToRepeat.length) {
|
|
actionsToRepeat[0]()
|
|
if (scheduler.timeoutTasks.has(autorepeat)) {
|
|
scheduler.clearTimeout(autorepeat)
|
|
scheduler.setInterval(autorepeat, autorepeatPeriodRange.value)
|
|
}
|
|
} else {
|
|
scheduler.clearTimeout(autorepeat)
|
|
scheduler.clearInterval(autorepeat)
|
|
}
|
|
}
|
|
|
|
// Handle player inputs
|
|
const REPEATABLE_ACTION = [playerAction.moveLeft, playerAction.moveRight, playerAction.softDrop]
|
|
pressedKeys = new Set()
|
|
actionsToRepeat = []
|
|
|
|
function onkeydown(event) {
|
|
if (event.key in settings.keyBind)
|
|
event.preventDefault()
|
|
if (!pressedKeys.has(event.key)) {
|
|
pressedKeys.add(event.key)
|
|
if (event.key in settings.keyBind) {
|
|
action = settings.keyBind[event.key]
|
|
action()
|
|
if (REPEATABLE_ACTION.includes(action)) {
|
|
actionsToRepeat.unshift(action)
|
|
scheduler.clearTimeout(autorepeat)
|
|
scheduler.clearInterval(autorepeat)
|
|
if (action == softDrop)
|
|
scheduler.setInterval(autorepeat, stats.fallPeriod / 20)
|
|
else
|
|
scheduler.setTimeout(autorepeat, autorepeatDelayRange.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onkeyup(event) {
|
|
pressedKeys.delete(event.key)
|
|
if (event.key in settings.keyBind) {
|
|
action = settings.keyBind[event.key]
|
|
if (actionsToRepeat.includes(action)) {
|
|
actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1)
|
|
if (!actionsToRepeat.length) {
|
|
scheduler.clearTimeout(autorepeat)
|
|
scheduler.clearInterval(autorepeat)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Text display
|
|
tempTexts = []
|
|
function printTempTexts(text) {
|
|
tempTexts.push(text)
|
|
messageSpan.innerHTML = tempTexts[0]
|
|
if (!scheduler.intervalTasks.has(delTempTexts))
|
|
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
|
messageSpan.classList.add(CLASSNAME.MESSAGE_SPAN_FADE_OUT)
|
|
}
|
|
|
|
function delTempTexts(self) {
|
|
messageSpan.classList.remove(CLASSNAME.MESSAGE_SPAN_FADE_OUT)
|
|
if (tempTexts.length)
|
|
tempTexts.shift()
|
|
if (tempTexts.length)
|
|
messageSpan.innerHTML = tempTexts[0]
|
|
else {
|
|
scheduler.clearInterval(delTempTexts)
|
|
messageSpan.innerHTML = ""
|
|
}
|
|
}
|
|
|
|
function clock() {
|
|
timeCell.innerText = stats.timeFormat.format(1000 * ++stats.time)
|
|
}
|
|
|
|
|
|
// Initialization
|
|
window.onload = async function() {
|
|
scheduler = new Scheduler()
|
|
holdQueue = new HoldQueue()
|
|
stats = new Stats()
|
|
matrix = new Matrix()
|
|
nextQueue = new NextQueue()
|
|
settings = new Settings()
|
|
await settings.applyTheme()
|
|
let startLevel = parseInt(location.hash.slice(1))
|
|
body.style.display = "block"
|
|
if (1 <= startLevel && startLevel <= 15) {
|
|
newGame(startLevel)
|
|
} else {
|
|
location.hash = ""
|
|
startButton.focus()
|
|
}
|
|
}
|
|
|
|
window.onblur = pause |