almost done
699
app.js
Normal file
@ -0,0 +1,699 @@
|
||||
/* Contants */
|
||||
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: "MINI T-SPIN",
|
||||
T_SPIN: "T-SPIN"
|
||||
}
|
||||
|
||||
// score = SCORES[tSpin][clearedLines]
|
||||
const SCORES = {
|
||||
[T_SPIN.NONE]: [0, 100, 300, 500, 800],
|
||||
[T_SPIN.MINI]: [100, 200],
|
||||
[T_SPIN.T_SPIN]: [400, 800, 1200, 1600]
|
||||
}
|
||||
|
||||
const CLEARED_LINES_NAMES = [
|
||||
"",
|
||||
"SINGLE",
|
||||
"DOUBLE",
|
||||
"TRIPLE",
|
||||
"<strong>QUATRIS</strong>",
|
||||
]
|
||||
|
||||
const DELAY = {
|
||||
LOCK: 500,
|
||||
FALL: 1000,
|
||||
}
|
||||
|
||||
const ORIENTATION = {
|
||||
NORTH: 0,
|
||||
EAST: 1,
|
||||
SOUTH: 2,
|
||||
WEST: 3,
|
||||
}
|
||||
|
||||
const KEY_NAMES = {
|
||||
["ArrowLeft"]: "←",
|
||||
["ArrowRight"]: "→",
|
||||
["ArrowUp"]: "↑",
|
||||
["ArrowDown"]: "↓",
|
||||
[" "]: "Space",
|
||||
["←"]: "ArrowLeft",
|
||||
["→"]: "ArrowRight",
|
||||
["↑"]: "ArrowUp",
|
||||
["↓"]: "ArrowDown",
|
||||
["Space"]: " ",
|
||||
}
|
||||
|
||||
/* Customize Array to be use as coord */
|
||||
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] }
|
||||
|
||||
|
||||
/* 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)
|
||||
this.rows = this.table.rows.length
|
||||
this.columns = this.table.rows[0].childElementCount
|
||||
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(coord, className) {
|
||||
this.table.rows[coord.y].cells[coord.x].className = className
|
||||
}
|
||||
|
||||
drawPiece(piece=this.piece, className=piece.className + (piece.locked? " locked" : "")) {
|
||||
piece.minoesCoord[piece.orientation]
|
||||
.translate(piece.center)
|
||||
.forEach(minoCoord => {
|
||||
this.drawMino(minoCoord, 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(id) {
|
||||
super(id)
|
||||
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 PlayfieldMatrix extends MinoesTable {
|
||||
constructor(id, piece_init_position) {
|
||||
super(id, piece_init_position)
|
||||
this.lockedMinoes = Array(this.rows).fill().map(() => Array(this.columns))
|
||||
}
|
||||
|
||||
cellIsEmpty(coord) {
|
||||
return 0 <= coord.x && coord.x < this.columns && 0 <= coord.y && coord.y < this.rows && !this.lockedMinoes[coord.y][coord.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 + (piece.locked? " locked" : "")) {
|
||||
super.drawPiece(this.ghost, "")
|
||||
this.ghost = piece.ghost
|
||||
while (this.ghost.canMove(TRANSLATION.DOWN)) this.ghost.center.y++
|
||||
super.drawPiece(this.ghost)
|
||||
super.drawPiece(piece, className)
|
||||
}
|
||||
|
||||
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 != "hard-drop-animation")
|
||||
this.drawMino([x, y], this.lockedMinoes[y][x] || "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PlayfieldMatrix.prototype.init_center = [5, 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, orientation=0, className=this.constructor.name + " mino") {
|
||||
this.center = center
|
||||
this.className = className
|
||||
this.orientation = orientation
|
||||
this.lastRotation = false
|
||||
this.rotationPoint4Used = false
|
||||
this.holdEnabled = true
|
||||
this.locked = false
|
||||
}
|
||||
|
||||
canMove(translation, rotation=ROTATION.NONE) {
|
||||
let testCenter = this.center.add(translation)
|
||||
let testOrientation = rotation? (this.orientation + rotation + 4) % 4: this.orientation
|
||||
let testMinoesCoord = this.minoesCoord[testOrientation]
|
||||
if (testMinoesCoord
|
||||
.translate(testCenter)
|
||||
.every(minoCoord => 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 = `<h1>LEVEL<br/>${this.level}</h1>`
|
||||
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 += `<div class="rotate-in-animation">${tSpin}</div>\n`
|
||||
if (nbClearedLines) messagesSpan.innerHTML += `<div class="zoom-in-animation">${CLEARED_LINES_NAMES[nbClearedLines]}</div>\n`
|
||||
messagesSpan.innerHTML += `<div class="zoom-in-animation">${SCORES[tSpin][nbClearedLines]}</div>\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 = "<h1>GAME<br/>OVER</h1>"
|
||||
levelSpan.classList.add("show-level-animation")
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('service-worker.js');
|
||||
}
|
91
css/99.css
Normal file
@ -0,0 +1,91 @@
|
||||
.mino {
|
||||
background: radial-gradient(
|
||||
ellipse 140% 66% at 122% 88%,
|
||||
var(--background-color) 100%,
|
||||
var(--frontier-color) 105%,
|
||||
var(--light-color) 130%
|
||||
);
|
||||
border: 4px solid;
|
||||
padding: 0;
|
||||
opacity: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
--background-color: #00d6fb;
|
||||
--frontier-color: #43e7fd;
|
||||
--light-color: #afeff9;
|
||||
border-top-color: #7cf2fd;
|
||||
border-left-color: #2ed5e5;
|
||||
border-right-color: #00b8ca;
|
||||
border-bottom-color: #00a4b0;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
--background-color: #2d00fa;
|
||||
--frontier-color: #7054fb;
|
||||
--light-color: #b8b4ff;
|
||||
border-top-color: #4985fd;
|
||||
border-left-color: #2f36ea;
|
||||
border-right-color: #0006ca;
|
||||
border-bottom-color: #00009d;
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
--background-color: #ff6c13;
|
||||
--frontier-color: #fe9551;
|
||||
--light-color: #fdd0b7;
|
||||
border-top-color: #fd9f6b;
|
||||
border-left-color: #e76d28;
|
||||
border-right-color: #e74f00;
|
||||
border-bottom-color: #c54800;
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
--background-color: #ffce12;
|
||||
--frontier-color: #fce15c;
|
||||
--light-color: #ffedac;;
|
||||
border-top-color: #ffe364;
|
||||
border-left-color: #e7ba23;
|
||||
border-right-color: #e3a707;
|
||||
border-bottom-color: #ca9501;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
--background-color: #ad00fa;
|
||||
--frontier-color: #c541fc;
|
||||
--light-color: #edb2ff;
|
||||
border-top-color: #d380ff;
|
||||
border-left-color: #b42deb;
|
||||
border-right-color: #8000cd;
|
||||
border-bottom-color: #6e019a;
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
--background-color: #6e1;
|
||||
--frontier-color: #93f85a;
|
||||
--light-color: #93f85a;
|
||||
border-top-color: #a4fc6d;
|
||||
border-left-color: #5ee82b;
|
||||
border-right-color: #35db00;
|
||||
border-bottom-color: #1cbc02;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
--background-color: #ff1945;
|
||||
--frontier-color: #fe6483;
|
||||
--light-color: #ffb8c5;
|
||||
border-top-color: #fd718d;
|
||||
border-left-color: #e62250;
|
||||
border-right-color: #e20332;
|
||||
border-bottom-color: #ad1936;
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
filter: saturate(50%) brightness(200%)
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
opacity: 20%;
|
||||
filter: brightness(200%)
|
||||
}
|
196
css/common.css
Normal file
@ -0,0 +1,196 @@
|
||||
:root {
|
||||
--cell-side: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: rgba(33, 37, 41, 30%);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: rgb(37, 41, 45);
|
||||
}
|
||||
|
||||
.minoes-table {
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
height: calc(var(--rows) * var(--cell-side));
|
||||
width: calc(var(--columns) * var(--cell-side));
|
||||
}
|
||||
|
||||
#matrixTable {
|
||||
margin-top: calc(-1 * var(--no-bordered-rows) * var(--cell-side));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@keyframes hard-dropped-table-animation {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#matrixTable.hard-dropped-table-animation {
|
||||
animation: hard-dropped-table-animation .2s;
|
||||
}
|
||||
|
||||
tr.no-border td:not(.mino) {
|
||||
border-width: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
tr.border td:not(.mino) {
|
||||
border: 1px solid #333;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
td {
|
||||
overflow: hidden;
|
||||
width: calc(var(--cell-side) + 4px);
|
||||
height: calc(var(--cell-side) + 4px);
|
||||
}
|
||||
|
||||
@keyframes hard-drop-animation {
|
||||
from {
|
||||
background-color: rgb(206, 255, 255, 40%);
|
||||
filter: saturate(50%) brightness(300%);
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
td.hard-drop-animation {
|
||||
animation: hard-drop-animation ease-out .3s;
|
||||
}
|
||||
|
||||
@keyframes line-cleared-animation {
|
||||
from {
|
||||
background-color: rgb(206, 255, 255, 40%);
|
||||
filter: saturate(50%) brightness(300%);
|
||||
box-shadow: -60px 0 5px white, 60px 0 5px white;
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
tr.line-cleared-animation{
|
||||
animation: line-cleared-animation ease-out .3s;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
|
||||
font-size: 3vmin;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#levelSpan {
|
||||
font-size: 4vmin;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes show-level-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: 70%;
|
||||
}
|
||||
50% {
|
||||
opacity: 100%;
|
||||
top: 40%
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
top: 20%
|
||||
}
|
||||
}
|
||||
|
||||
#levelSpan.show-level-animation {
|
||||
animation: cubic-bezier(0.4, 0, 0.6, 1) 2s show-level-animation;
|
||||
}
|
||||
|
||||
@keyframes zoom-in-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale3d(0.3, 0.3, 0.3);
|
||||
}
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale3d(1.5, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-in-animation {
|
||||
0% {
|
||||
opacity:0;
|
||||
transform:rotate(200deg);
|
||||
}
|
||||
30% {
|
||||
opacity:1;
|
||||
transform:translateZ(0);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale3d(1.5, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#messagesSpan div {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#messagesSpan div.rotate-in-animation {
|
||||
animation-name: rotate-in-animation;
|
||||
animation-timing-function: cubic-bezier(.25,.46,.45,.94);
|
||||
animation-duration: 1s;
|
||||
}
|
||||
|
||||
#messagesSpan div.zoom-in-animation {
|
||||
animation-name: zoom-in-animation;
|
||||
animation-timing-function: cubic-bezier(.25,.46,.45,.94);
|
||||
transform-origin:center;
|
||||
}
|
||||
#messagesSpan div.zoom-in-animation:first-child {
|
||||
animation-duration: 1s;
|
||||
}
|
||||
#messagesSpan div.zoom-in-animation:nth-child(2) {
|
||||
animation-delay: .2s;
|
||||
animation-duration: .9s;
|
||||
}
|
||||
#messagesSpan div.zoom-in-animation:nth-child(3) {
|
||||
animation-delay: .4s;
|
||||
animation-duration: .8s;
|
||||
}
|
||||
#messagesSpan div.zoom-in-animation:nth-child(4) {
|
||||
animation-delay: .6s;
|
||||
animation-duration: .7s;
|
||||
}
|
||||
#messagesSpan div.zoom-in-animation:nth-child(5) {
|
||||
animation-delay: .8s;
|
||||
animation-duration: .6s;
|
||||
}
|
16
css/effect.css
Normal file
@ -0,0 +1,16 @@
|
||||
.mino {
|
||||
background: rgba(127, 229, 255, 0.3);
|
||||
border: 1px solid rgba(127, 229, 255, 0.7);
|
||||
border-radius: 0.3vmin;
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 0.3vmin;
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
background: rgba(242, 255, 255, 0.5);
|
||||
border-color: rgba(242, 255, 255, 0.7);
|
||||
}
|
BIN
favicons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
favicons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
favicons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 571 B |
BIN
favicons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
favicons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
197
index.html
Normal file
@ -0,0 +1,197 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>QUATRIS</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
|
||||
<link href="css/common.css" rel="stylesheet">
|
||||
<link href="css/99.css" rel="stylesheet" title="99">
|
||||
<link href="css/effect.css" rel="alternate stylesheet" title="Effect">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicons/favicon-16x16.png">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body data-bs-theme="dark">
|
||||
<div class="modal fade" id="settingsModal" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title w-100 text-center">QUATRIS</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="settingsForm">
|
||||
<fieldset id="keyBindFielset" class="row g-2 mb-3 align-items-center text-center">
|
||||
<legend class="text-start">Commandes</legend>
|
||||
<label for="moveLeftInput" title="Gauche" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<input name="moveLeft" id="moveLeftInput" type="button" class="form-control text-center" value="←" onclick="changeKey(this)">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input name="moveRight" id="moveRightInput" type="button" class="form-control text-center" value="→" onclick="changeKey(this)">
|
||||
</div>
|
||||
<label for="moveRightInput" title="Droite" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</label>
|
||||
<label for="rotateCounterclockwiseInput" title="Rotation anti-horaire" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<input name="rotateCounterclockwise" id="rotateCounterclockwiseInput" type="button" class="form-control text-center" value="w" onclick="changeKey(this)">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input name="rotateClockwise" id="rotateClockwiseInput" type="button" class="form-control text-center" value="↑" onclick="changeKey(this)">
|
||||
</div>
|
||||
<label for="rotateClockwiseInput" title="Rotation horaire" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</label>
|
||||
<label for="softDropInput" title="Chute lente" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-down-short"></i>
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<input name="softDrop" id="softDropInput" type="button" class="form-control text-center" value="↓" onclick="changeKey(this)">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input name="hardDrop" id="hardDropInput" type="button" class="form-control text-center" value="Space" onclick="changeKey(this)">
|
||||
</div>
|
||||
<label for="hardDropInput" title="Chute rapide" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-download"></i>
|
||||
</label>
|
||||
<label for="holdInput" title="Échanger la pièce" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<input name="hold" id="holdInput" type="button" class="form-control text-center" value="c" onclick="changeKey(this)">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input name="pause" id="pauseInput" type="button" class="form-control text-center" value="Escape" onclick="changeKey(this)">
|
||||
</div>
|
||||
<label for="pauseInput" title="Pause" class="col-sm-3 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-pause"></i>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset id="autorepearFieldset" class="row g-2 mb-3 align-items-center text-center">
|
||||
<legend class="text-start">Répétition automatique</legend>
|
||||
<label for="arrInput" class="col-sm-3 col-form-label" title="Automatic Repeat Rate : période de répétition de l'action">ARR</label>
|
||||
<div class="col-sm-3">
|
||||
<output id="arrOutput" class="form-text form-text-sm w-100 m-auto text-center" for="arrInput">33 ms</output>
|
||||
<input name="arr" id="arrInput" type="range" class="form-range h-100" value="33" min="2" max="200" step="1" oninput="arrOutput.value = value + ' ms'">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<output id="dasOutput" class="form-text form-text-sm w-100 m-auto text-center" for="dasInput">180 ms</output>
|
||||
<input name="das" id="dasInput" type="range" class="form-range h-100" value="180" min="100" max="500" step="10" oninput="dasOutput.value = value + ' ms'">
|
||||
</div>
|
||||
<label for="dasInput" class="col-sm-3 col-form-label" title="Delayed AutoShift : délai initial avant répétition">DAS</label>
|
||||
</fieldset>
|
||||
<fieldset class="row g-2 mb-3 align-items-center text-center">
|
||||
<legend class="text-start">Partie</legend>
|
||||
<label for="levelInput" class="col-sm-3 col-form-label text-center">Niveau</label>
|
||||
<div class="col-sm-3">
|
||||
<input name="startLevel" id="levelInput" type="number" class="form-control" value="1" min="1" max="15">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<button id="resumeButton" type="submit" data-bs-dismiss="modal" class="btn btn-primary form-control" autofocus>Jouer</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid d-flex h-100 justify-content-center d-flex align-items-center">
|
||||
<div class="row row-cols-auto align-items-start gap-2">
|
||||
<div class="col d-flex flex-column align-items-end">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header text-center"><strong>HOLD</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<table id="holdTable" class="minoes-table" style="--columns: 6; --rows: 5;">
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card w-100">
|
||||
<table id="statsTable" class="table mb-0 align-middle">
|
||||
<tr><td>Score</td><th id="scoreTd" class="text-end">0</th></tr>
|
||||
<tr><td>Meilleur score</td><th id="highScoreTd" class="text-end">0</th></tr>
|
||||
<tr><td>Niveau</td><th id="levelTd" class="text-end">0</th></tr>
|
||||
<tr><td>But</td><th id="goalTd" class="text-end">0</th></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<table id="matrixTable" class="minoes-table" style="--columns: 10; --rows: 25; --no-bordered-rows: 6">
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="border"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<span id="levelSpan"></span>
|
||||
<span id="messagesSpan"></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-center"><strong>NEXT</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<table id="nextTable" class="minoes-table caption-top" style="--columns: 6; --rows: 16;">
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="no-border"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"></script>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
43
manifest.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"short_name": "TETRIS",
|
||||
"name": "Tetris",
|
||||
"description": "Falling blocks",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicons/favicon-16x16.png",
|
||||
"type": "image/png",
|
||||
"sizes": "16x16"
|
||||
},
|
||||
{
|
||||
"src": "favicons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "index.html",
|
||||
"background_color": "#212529",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"theme_color": "#212529",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "TETRIS",
|
||||
"short_name": "Tetris",
|
||||
"description": "Falling blocks",
|
||||
"url": "index.html",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "16x16"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
86
service-worker.js
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Incrementing OFFLINE_VERSION will kick off the install event and force
|
||||
// previously cached resources to be updated from the network.
|
||||
const OFFLINE_VERSION = 1;
|
||||
const CACHE_NAME = "offline";
|
||||
// Customize this with a different URL if needed.
|
||||
const OFFLINE_URL = "index.html";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
// Setting {cache: 'reload'} in the new request will ensure that the
|
||||
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
||||
// the network.
|
||||
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
|
||||
})()
|
||||
);
|
||||
// Force the waiting service worker to become the active service worker.
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
// Enable navigation preload if it's supported.
|
||||
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
||||
if ("navigationPreload" in self.registration) {
|
||||
await self.registration.navigationPreload.enable();
|
||||
}
|
||||
})()
|
||||
);
|
||||
|
||||
// Tell the active service worker to take control of the page immediately.
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// We only want to call event.respondWith() if this is a navigation request
|
||||
// for an HTML page.
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
// First, try to use the navigation preload response if it's supported.
|
||||
const preloadResponse = await event.preloadResponse;
|
||||
if (preloadResponse) {
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
// Always try the network first.
|
||||
const networkResponse = await fetch(event.request);
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
// catch is only triggered if an exception is thrown, which is likely
|
||||
// due to a network error.
|
||||
// If fetch() returns a valid HTTP response with a response code in
|
||||
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||
console.log("Fetch failed; returning offline page instead.", error);
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||
return cachedResponse;
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
// If our if() condition is false, then this fetch handler won't intercept the
|
||||
// request. If there are any other fetch handlers registered, they will get a
|
||||
// chance to call event.respondWith(). If no fetch handlers call
|
||||
// event.respondWith(), the request will be handled by the browser as if there
|
||||
// were no service worker involvement.
|
||||
});
|
BIN
thumbnail.png
Normal file
After Width: | Height: | Size: 24 KiB |