Compare commits
173 Commits
44684fe459
...
master
Author | SHA1 | Date | |
---|---|---|---|
051fc223e9 | |||
9feaa3c098 | |||
3175e7b7ad | |||
4281bf1735 | |||
ff37cb7823 | |||
2a8df78d22 | |||
344575bdc4 | |||
2453b2f6b6 | |||
25a98bf42d | |||
022512a5e6 | |||
d56af40362 | |||
f33723a786 | |||
df95139650 | |||
d3b527570c | |||
a1722a700d | |||
6cc8a9e645 | |||
9023252822 | |||
9bf3c0de0c | |||
09f4785ef4 | |||
bf9554d917 | |||
c33d80facb | |||
9282f94956 | |||
fda289dc9c | |||
b6e58b41aa | |||
bd5c7dad3b | |||
7faae294dc | |||
3b59534f90 | |||
ec08747066 | |||
db8a9a3f74 | |||
0ac36444c4 | |||
5d451db8f9 | |||
ab023ec982 | |||
d75696fbc3 | |||
f97b5f0cf9 | |||
ffefb77f3f | |||
21929261e4 | |||
58389623cc | |||
04f6eaf5dc | |||
d56a8a6b06 | |||
e1992f4c1e | |||
27d7a0689d | |||
df62a40c2a | |||
2105296238 | |||
05862b8587 | |||
6c1291833d | |||
095e3f8dbe | |||
82929b6f3b | |||
29661421a5 | |||
d3a6f5d6d6 | |||
8af1801527 | |||
6d95acddd2 | |||
6062f6895b | |||
b726e95bed | |||
228fc56916 | |||
f39d677b62 | |||
fc21a1b12b | |||
072661ff5a | |||
f93f3c71af | |||
ed4d9b82c9 | |||
b17db2ffd4 | |||
9f8c38e6bf | |||
262a26940c | |||
7996e4a7ae | |||
a0af16d0dd | |||
9547e585ca | |||
4d0d51e20d | |||
e7246c0a5d | |||
50502be1d1 | |||
a89a60691c | |||
a04e118ff1 | |||
ef8ddca950 | |||
8773e65885 | |||
ebb9f4810b | |||
baf6e66244 | |||
490566e49f | |||
e3d4520af7 | |||
bf37ca8135 | |||
048d17e041 | |||
365fb17694 | |||
caca970a53 | |||
f42a14d75f | |||
46a0500c66 | |||
227a26880d | |||
ee4945bb27 | |||
c010ccba49 | |||
5e4d729803 | |||
8940c1b79b | |||
a9e3557b56 | |||
4f1904acaa | |||
895635b601 | |||
71a36fdc4d | |||
1b18b00619 | |||
682b0c61a1 | |||
230f20befb | |||
a13d97b550 | |||
f8466cb27f | |||
ea2463157f | |||
c1d6fb87eb | |||
5e600bec56 | |||
31123fc332 | |||
cbf86e0bf5 | |||
875fc21cac | |||
4f4447e889 | |||
b9bb673e7e | |||
fd8b46a212 | |||
36d58eb526 | |||
57639b0c2a | |||
2d687d5fcb | |||
6e089315bf | |||
e6f238629a | |||
d615b6b72b | |||
70d8e6e0aa | |||
ff20114a92 | |||
3318a716f1 | |||
1b82e05bb5 | |||
4cee98ff49 | |||
36cba9b6ef | |||
5bacff51fc | |||
0a491b89be | |||
b4a241b3a9 | |||
ec5ed9425e | |||
fa5c5fb33b | |||
fd09458ee9 | |||
ba313cbbd7 | |||
b358611c28 | |||
af35a5d6b2 | |||
3e13e77d26 | |||
47bbc83788 | |||
dcaf1389eb | |||
bff9225bb0 | |||
e92a6cf5de | |||
13539af376 | |||
7388f13e29 | |||
82b05a902d | |||
e4a162728f | |||
e539a8e24d | |||
06ac4650d5 | |||
f2c11ccbf2 | |||
fdbe8c5a73 | |||
ad0f57db33 | |||
6fc00d50ab | |||
8f96053440 | |||
77d68ece87 | |||
9cf55ed479 | |||
58915a7d69 | |||
a0a7075b6e | |||
889351dfa7 | |||
44276b8d6a | |||
bac13f1eca | |||
d7d32e38d1 | |||
de9c72f760 | |||
c774cc25fc | |||
6520a25cb9 | |||
4587f5a147 | |||
d535bd9c66 | |||
3390df79f2 | |||
34c4ad95e1 | |||
b01f0a7c06 | |||
40a5b59b8a | |||
f9e743f7ae | |||
7b3db0b58d | |||
cc44b69b3b | |||
649fdbc064 | |||
d1a9532842 | |||
5ae2b7c4ed | |||
d6ae8f7475 | |||
cb86833e14 | |||
9baf03fa7e | |||
a720f36de1 | |||
7f07d71f0b | |||
fb34ef72dc | |||
0d5bbc77b6 | |||
063ccba7d7 |
5
README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Quatuor
|
||||
|
||||
Falling blocks
|
||||
|
||||

|
846
app.js
@ -1,846 +0,0 @@
|
||||
/* 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: "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",
|
||||
"QUATRIS",
|
||||
]
|
||||
|
||||
const DELAY = {
|
||||
LOCK: 500,
|
||||
FALL: 1000,
|
||||
}
|
||||
|
||||
const ORIENTATION = {
|
||||
NORTH: 0,
|
||||
EAST: 1,
|
||||
SOUTH: 2,
|
||||
WEST: 3,
|
||||
}
|
||||
|
||||
const KEY_NAMES = {
|
||||
["ArrowLeft"]: "←",
|
||||
["ArrowRight"]: "→",
|
||||
["ArrowUp"]: "↑",
|
||||
["ArrowDown"]: "↓",
|
||||
[" "]: "Espace",
|
||||
["Escape"]: "Échap",
|
||||
["Enter"]: "Entrée",
|
||||
["←"]: "ArrowLeft",
|
||||
["→"]: "ArrowRight",
|
||||
["↑"]: "ArrowUp",
|
||||
["↓"]: "ArrowDown",
|
||||
["Espace"]: " ",
|
||||
["Échap"]: "Escape",
|
||||
["Entrée"]: "Enter",
|
||||
}
|
||||
|
||||
/* 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)
|
||||
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(position, className) {
|
||||
this.table.rows[position.y].cells[position.x].className = className
|
||||
}
|
||||
|
||||
drawPiece(piece=this.piece, className=piece.className + (piece.locked? " locked" : "")) {
|
||||
piece.minoesPosition[piece.orientation]
|
||||
.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")
|
||||
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")
|
||||
this.blocks = Array(this.rows).fill().map(() => Array(this.columns))
|
||||
}
|
||||
|
||||
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 + (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.blocks[y][x] || "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Matrix.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 testMinoesPosition = this.minoesPosition[testOrientation]
|
||||
if (testMinoesPosition
|
||||
.translate(testCenter)
|
||||
.every(minoPosition => matrix.cellIsEmpty(minoPosition)))
|
||||
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.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 {}
|
||||
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.form.onsubmit = newGame
|
||||
this.modal = new bootstrap.Modal('#settingsModal')
|
||||
document.getElementById('settingsModal').addEventListener('shown.bs.modal', () => {
|
||||
resumeButton.focus()
|
||||
})
|
||||
}
|
||||
|
||||
load() {
|
||||
for (let input of this.form.getElementsByTagName("input")) {
|
||||
if (localStorage[input.name]) input.value = localStorage[input.name]
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
resumeButton.disabled = false
|
||||
settings.form.classList.remove('was-validated')
|
||||
settings.modal.show()
|
||||
settings.form.reportValidity()
|
||||
}
|
||||
|
||||
getInputs() {
|
||||
for (let input of keyBindFielset.getElementsByTagName("input")) {
|
||||
this[input.name] = KEY_NAMES[input.value] || input.value
|
||||
}
|
||||
for (let input of autorepearFieldset.getElementsByTagName("input")) {
|
||||
this[input.name] = input.valueAsNumber
|
||||
}
|
||||
|
||||
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] || event.key
|
||||
input.blur()
|
||||
}
|
||||
input.onblur = function (event) {
|
||||
if (input.value == "") input.value = prevValue
|
||||
input.onkeydown = null
|
||||
input.onblur = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Stats {
|
||||
constructor() {
|
||||
this.highScore = Number(localStorage["highScore"]) || 0
|
||||
this.combo = 0
|
||||
this.b2b = 0
|
||||
this.startTime = new Date()
|
||||
}
|
||||
|
||||
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 time(time) {
|
||||
this.startTime = new Date() - time
|
||||
ticktack()
|
||||
}
|
||||
|
||||
get time() {
|
||||
return new Date() - this.startTime
|
||||
}
|
||||
|
||||
lockDown(nbClearedLines, tSpin) {
|
||||
// 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: `ENCHAINEMENT<br/>${comboScore}`
|
||||
})
|
||||
} else {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `ENCHAINEMENT x${this.combo}<br/>${comboScore}`
|
||||
})
|
||||
}
|
||||
this.score += comboScore
|
||||
}
|
||||
} else {
|
||||
this.combo = -1
|
||||
}
|
||||
|
||||
// Back to back sequence
|
||||
if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
|
||||
this.b2b++
|
||||
if (this.b2b >= 1) {
|
||||
let b2bScore = patternScore / 2
|
||||
if (this.b2b == 1) {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `BOUT À BOUT<br/>${b2bScore}`
|
||||
})
|
||||
} else {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${b2bScore}`
|
||||
})
|
||||
}
|
||||
this.score += b2bScore
|
||||
}
|
||||
} else if (nbClearedLines && !tSpin ) {
|
||||
if (this.b2b >= 1) {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `FIN DU BOUT À BOUT`
|
||||
})
|
||||
}
|
||||
this.b2b = -1
|
||||
}
|
||||
|
||||
this.goal -= awardedLineClears
|
||||
if (this.goal <= 0) this.level++
|
||||
}
|
||||
}
|
||||
Stats.prototype.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC"
|
||||
})
|
||||
|
||||
|
||||
/* Game */
|
||||
onanimationend = function (event) {
|
||||
event.target.classList.remove(event.animationName)
|
||||
}
|
||||
|
||||
messagesSpan.onanimationend = function(event) {
|
||||
event.target.remove()
|
||||
}
|
||||
|
||||
let scheduler = new Scheduler()
|
||||
let settings = new Settings()
|
||||
let stats = new Stats()
|
||||
let holdQueue = new MinoesTable("holdTable")
|
||||
let matrix = new Matrix()
|
||||
let nextQueue = new NextQueue()
|
||||
let playing = false
|
||||
|
||||
function init() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function pauseSettings() {
|
||||
scheduler.clearInterval(fall)
|
||||
scheduler.clearTimeout(lockDown)
|
||||
scheduler.clearTimeout(repeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
scheduler.clearInterval(ticktack)
|
||||
stats.pauseTime = stats.time
|
||||
|
||||
document.onkeydown = null
|
||||
|
||||
settings.show()
|
||||
}
|
||||
onblur = pauseSettings
|
||||
|
||||
pauseSettings()
|
||||
|
||||
function newGame(event) {
|
||||
if (!settings.form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
settings.form.reportValidity()
|
||||
settings.form.classList.add('was-validated')
|
||||
} else {
|
||||
stats.lockDelay = DELAY.LOCK
|
||||
levelInput.name = "level"
|
||||
levelInput.disabled = true
|
||||
titleHeader.innerHTML = "PAUSE"
|
||||
resumeButton.innerHTML = "Reprendre"
|
||||
event.target.onsubmit = resume
|
||||
stats.score = 0
|
||||
stats.goal = 0
|
||||
stats.level = levelInput.valueAsNumber
|
||||
localStorage["startLevel"] = levelInput.value
|
||||
playing = true
|
||||
resume(event)
|
||||
}
|
||||
}
|
||||
|
||||
function resume(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
settings.form.reportValidity()
|
||||
settings.form.classList.add('was-validated')
|
||||
|
||||
if (settings.form.checkValidity()) {
|
||||
settings.modal.hide()
|
||||
settings.getInputs()
|
||||
|
||||
document.onkeydown = onkeydown
|
||||
document.onkeyup = onkeyup
|
||||
|
||||
stats.time = stats.pauseTime
|
||||
scheduler.setInterval(ticktack, 1000)
|
||||
|
||||
if (matrix.piece) scheduler.setInterval(fall, stats.fallPeriod)
|
||||
else generate()
|
||||
}
|
||||
}
|
||||
|
||||
function ticktack() {
|
||||
timeCell.innerText = stats.timeFormat.format(stats.time)
|
||||
}
|
||||
|
||||
function generate(piece) {
|
||||
matrix.piece = piece || nextQueue.shift()
|
||||
|
||||
if (matrix.piece.canMove(TRANSLATION.NONE)) {
|
||||
scheduler.setInterval(fall, stats.fallPeriod)
|
||||
} else {
|
||||
gameOver() // block out
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
let heldPiece = holdQueue.piece
|
||||
holdQueue.piece = matrix.piece
|
||||
generate(heldPiece)
|
||||
}
|
||||
},
|
||||
|
||||
pause: pauseSettings,
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
blocksPosition = matrix.piece.minoesPosition[matrix.piece.orientation]
|
||||
.translate(matrix.piece.center)
|
||||
if (blocksPosition.some(minoPosition => minoPosition.y >= 4)) {
|
||||
blocksPosition.forEach(minoPosition => {
|
||||
matrix.blocks[minoPosition.y][minoPosition.x] = matrix.piece.className
|
||||
matrix.drawMino(minoPosition, 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(minoPosition => !matrix.cellIsEmpty(minoPosition))
|
||||
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(blocksPosition.map(minoPosition => minoPosition.y)))
|
||||
.filter(y => matrix.blocks[y].filter(lockedMino => lockedMino).length == matrix.columns)
|
||||
.sort()
|
||||
for (y of clearedLines) {
|
||||
matrix.blocks.splice(y, 1)
|
||||
matrix.blocks.unshift(Array(matrix.columns))
|
||||
matrix.table.rows[y].classList.add("cleared-line-animation")
|
||||
}
|
||||
matrix.redraw()
|
||||
stats.lockDown(clearedLines.length, tSpin)
|
||||
|
||||
generate()
|
||||
} else {
|
||||
gameOver() // lock out
|
||||
}
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
matrix.piece.locked = false
|
||||
matrix.drawPiece()
|
||||
|
||||
document.onkeydown = null
|
||||
onblur = null
|
||||
scheduler.clearInterval(ticktack)
|
||||
playing = false
|
||||
|
||||
messagesSpan.onanimationend = null
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "game-over-animation",
|
||||
style: "opacity: 100%",
|
||||
innerHTML: "<h1>FIN</h1>"
|
||||
})
|
||||
}
|
||||
|
||||
window.onbeforeunload = function(event) {
|
||||
localStorage["highScore"] = stats.highScore
|
||||
for (let input of settings.form.getElementsByTagName("input")) {
|
||||
localStorage[input.name] = input.value
|
||||
}
|
||||
if (playing) return false;
|
||||
}
|
||||
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('service-worker.js');
|
||||
}
|
276
css/binaural.css
Normal file
@ -0,0 +1,276 @@
|
||||
body {
|
||||
background-image: url(binaural/bg.jpg),
|
||||
radial-gradient(
|
||||
circle at center,
|
||||
#39444f 0%,
|
||||
#2c323b 25%,
|
||||
#293036 28%,
|
||||
#252b32 34%,
|
||||
#242930 38%,
|
||||
#1a1d22 52%,
|
||||
#191c22 53%,
|
||||
#151519 63%,
|
||||
#141418 65%,
|
||||
#0f0f12 74%,
|
||||
#0a0c0d 100%
|
||||
);
|
||||
background-repeat: round;
|
||||
}
|
||||
|
||||
#sceneDiv {
|
||||
perspective: 500px;
|
||||
}
|
||||
|
||||
#sceneDiv * {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
#screenRow {
|
||||
display: block;
|
||||
transform: translateZ(var(--tZ)) rotateX(var(--rX)) rotateY(var(--rY));
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#screenRow:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
#screenRow * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#screenRow .col {
|
||||
display: inline-block !important;
|
||||
width: max-content;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #36394180;
|
||||
}
|
||||
|
||||
#matrixCard {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
#screenRow .card > * {
|
||||
transform: translateZ(var(--cell-side));
|
||||
}
|
||||
|
||||
#screenRow .card-header {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.minoes-table th,
|
||||
.minoes-table td {
|
||||
display: inline-block !important;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.minoes-table tr {
|
||||
width: max-content;
|
||||
height: var(--cell-side);
|
||||
}
|
||||
|
||||
#statsTable tr {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#statsTable th,
|
||||
#statsTable td {
|
||||
display: table-cell;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
tr.matrix td:not(.mino) {
|
||||
border: 0;
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.minoes-table td {
|
||||
width: var(--cell-side) !important;
|
||||
height: var(--cell-side);
|
||||
}
|
||||
|
||||
.minoes-table .mino {
|
||||
background: radial-gradient(circle at -150% -200%, #fffb 0%, var(--background-color) 100%);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.mino::before,
|
||||
.mino::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.mino::before {
|
||||
background: var(--light-color);
|
||||
transform: translateZ(calc(-1 * var(--cell-side))) rotateY(-90deg);
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.right .mino::before {
|
||||
background: var(--dark-color);
|
||||
transform: translateZ(calc(-1 * var(--cell-side))) rotateY(90deg);
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.mino::after {
|
||||
background: var(--light-color);
|
||||
transform: translateZ(calc(-1 * var(--cell-side))) rotateX(90deg);
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
.bottom .mino::after {
|
||||
background: var(--dark-color);
|
||||
transform: translateZ(calc(-1 * var(--cell-side))) rotateX(-90deg);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
--background-color: #42afe1b0;
|
||||
--light-color: #6ceaff80;
|
||||
--dark-color: #00a4b0b0;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
--background-color: #1165b5b0;
|
||||
--light-color: #339bff80;
|
||||
--dark-color: #00009db0;
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
--background-color: #f38927b0;
|
||||
--light-color: #ffba5980;
|
||||
--dark-color: #c54800b0;
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
--background-color: #f6d03cb0;
|
||||
--light-color: #ffff7f80;
|
||||
--dark-color: #ca9501b0;
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
--background-color: #51b84db0;
|
||||
--light-color: #84f88080;
|
||||
--dark-color: #1cbc02b0;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
--background-color: #9739a2b0;
|
||||
--light-color: #d958e980;
|
||||
--dark-color: #6e019ab0;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
--background-color: #eb4f65b0;
|
||||
--light-color: #ff7f7980;
|
||||
--dark-color: #ad1936b0;
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
--background-color: #8886;
|
||||
--light-color: #ccc6;
|
||||
--dark-color: #3336;
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
--background-color: #eeeb;
|
||||
--light-color: #fffb;
|
||||
--dark-color: #dddb;
|
||||
}
|
||||
|
||||
.disabled.mino {
|
||||
--background-color: #888b;
|
||||
--light-color: #cccb;
|
||||
--dark-color: #333b;
|
||||
}
|
||||
|
||||
@keyframes cleared-line-animation {
|
||||
from {
|
||||
background-color: #ceffff66;
|
||||
box-shadow: -200px 0 5px white, 200px 0 5px white;
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-level-animation {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(200%);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0) scaleY(1);
|
||||
line-height: var(--bs-body-line-height);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(-100%) scaleY(0);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-in-animation {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale3d(0.3, 0.3, 0.3);
|
||||
line-height: var(--bs-body-line-height);
|
||||
}
|
||||
30% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
80% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
line-height: var(--bs-body-line-height);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale3d(1.5, 0, 1);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-in-animation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: rotate(200deg);
|
||||
line-height: var(--bs-body-line-height);
|
||||
}
|
||||
30% {
|
||||
transform: translateZ(0);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
80% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
line-height: var(--bs-body-line-height);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale3d(1.5, 0, 1);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes game-over-animation {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(200%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scaleY(1);
|
||||
line-height: var(--bs-body-line-height);
|
||||
}
|
||||
}
|
BIN
css/binaural/bg.jpg
Normal file
After Width: | Height: | Size: 62 KiB |
143
css/classic.css
@ -8,84 +8,123 @@
|
||||
border: 4px solid;
|
||||
padding: 0;
|
||||
opacity: 100%;
|
||||
border-radius: 2px;
|
||||
border-radius: 1px;
|
||||
box-shadow: 2px 2px 4px #000a;
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
--background-color: #009FdA;
|
||||
--frontier-color: #43e7fd;
|
||||
--light-color: #afeff9;
|
||||
border-top-color: #7cf2fd;
|
||||
border-left-color: #2ed5e5;
|
||||
border-right-color: #00b8ca;
|
||||
--background-color : #00d6fb;
|
||||
--frontier-color : #43e7fd;
|
||||
--light-color : #afeff9;
|
||||
border-top-color : #7cf2fd;
|
||||
border-left-color : #2ed5e5;
|
||||
border-right-color : #01b8ca;
|
||||
border-bottom-color: #00a4b0;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
--background-color: #0065Bd;
|
||||
--frontier-color: #7054fb;
|
||||
--light-color: #b8b4ff;
|
||||
border-top-color: #4985fd;
|
||||
border-left-color: #2f36ea;
|
||||
border-right-color: #0006ca;
|
||||
--background-color : #2E00FB;
|
||||
--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: #FF7900;
|
||||
--frontier-color: #fe9551;
|
||||
--light-color: #fdd0b7;
|
||||
border-top-color: #fd9f6b;
|
||||
border-left-color: #e76d28;
|
||||
border-right-color: #e74f00;
|
||||
--background-color : #FF7900;
|
||||
--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: #FeCB00;
|
||||
--frontier-color: #fce15c;
|
||||
--light-color: #ffedac;;
|
||||
border-top-color: #ffe364;
|
||||
border-left-color: #e7ba23;
|
||||
border-right-color: #e3a707;
|
||||
--background-color : #FeCB00;
|
||||
--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: #952d98;
|
||||
--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: #69Be28;
|
||||
--frontier-color: #93f85a;
|
||||
--light-color: #93f85a;
|
||||
border-top-color: #a4fc6d;
|
||||
border-left-color: #5ee82b;
|
||||
border-right-color: #35db00;
|
||||
--background-color : #67EE12;
|
||||
--frontier-color : #93f85a;
|
||||
--light-color : #C8FBA8;
|
||||
border-top-color : #a4fc6d;
|
||||
border-left-color : #5ee82b;
|
||||
border-right-color : #35db00;
|
||||
border-bottom-color: #1cbc02;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
--background-color : #B000FE;
|
||||
--frontier-color : #c541fc;
|
||||
--light-color : #edb2ff;
|
||||
border-top-color : #d380ff;
|
||||
border-left-color : #b42deb;
|
||||
border-right-color : #8000cd;
|
||||
border-bottom-color: #6e019a;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
--background-color: #ed2939;
|
||||
--frontier-color: #fe6483;
|
||||
--light-color: #ffb8c5;
|
||||
border-top-color: #fd718d;
|
||||
border-left-color: #e62250;
|
||||
border-right-color: #e20332;
|
||||
--background-color : #ed2939;
|
||||
--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 {
|
||||
margin: 1px;
|
||||
background: transparent !important;
|
||||
border: 2px solid var(--light-color) !important;
|
||||
box-shadow:
|
||||
-2px -2px 6px #FFF8,
|
||||
-2px 2px 6px #FFF8,
|
||||
2px -2px 6px #FFF8,
|
||||
2px 2px 6px #FFF8;
|
||||
opacity: 65%;
|
||||
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
opacity: 20%;
|
||||
filter: brightness(200%)
|
||||
.moving.mino {
|
||||
filter: saturate(80%) brightness(150%);
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
filter: saturate(50%) brightness(200%);
|
||||
box-shadow:
|
||||
-1px -1px 4px #FFF2,
|
||||
-1px 1px 4px #FFF2,
|
||||
1px -1px 4px #FFF2,
|
||||
1px 1px 4px #FFF2;
|
||||
}
|
||||
|
||||
.disabled.mino {
|
||||
filter: brightness(50%) contrast(80%);
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
@keyframes locked-animation {
|
||||
from {
|
||||
filter: saturate(50%) brightness(400%);
|
||||
box-shadow:
|
||||
-1px -1px 4px #FFF2,
|
||||
-1px 1px 4px #FFF2,
|
||||
1px -1px 4px #FFF2,
|
||||
1px 1px 4px #FFF2;
|
||||
}
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
animation: locked-animation;
|
||||
animation-duration: 0.2s;
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
:root {
|
||||
--cell-side: 24px;
|
||||
--rX: -15deg;
|
||||
--rY: 0;
|
||||
--tZ: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -7,18 +10,40 @@ body {
|
||||
}
|
||||
|
||||
@supports (backdrop-filter: blur()) {
|
||||
.modal {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: rgba(33, 37, 41, 30%);
|
||||
background-color: #2125294d;
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: rgb(37, 41, 45);
|
||||
box-shadow: 5px 5px 20px #202020;
|
||||
background-color: #25292d;
|
||||
}
|
||||
|
||||
#matrixCard {
|
||||
background-image: radial-gradient(#222, #25292d)
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-shadow: 0 0 2px black;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
text-shadow: 0 0 8px var(--bs-light);
|
||||
}
|
||||
|
||||
#statsTable td,
|
||||
#statsModal td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.minoes-table {
|
||||
--piece-column: 0;
|
||||
--piece-row : 0;
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
@ -30,18 +55,11 @@ body {
|
||||
}
|
||||
|
||||
@keyframes hard-dropped-table-animation {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
#matrixTable.hard-dropped-table-animation {
|
||||
.hard-dropped-table-animation {
|
||||
animation: hard-dropped-table-animation .2s;
|
||||
}
|
||||
|
||||
@ -50,7 +68,10 @@ tr.buffer-zone td:not(.mino) {
|
||||
}
|
||||
|
||||
tr.matrix td:not(.mino) {
|
||||
border: 1px solid #333;
|
||||
border-left : 1px solid #333;
|
||||
border-right : 1px solid #333;
|
||||
border-top : 1px solid #303030;
|
||||
border-bottom: 1px solid #303030;
|
||||
}
|
||||
|
||||
td {
|
||||
@ -59,9 +80,9 @@ td {
|
||||
height: var(--cell-side);
|
||||
}
|
||||
|
||||
@keyframes hard-drop-animation {
|
||||
@keyframes trail-animation {
|
||||
from {
|
||||
background-color: rgb(206, 255, 255, 40%);
|
||||
background-color: #ceffff40;
|
||||
filter: saturate(50%) brightness(300%);
|
||||
}
|
||||
to {
|
||||
@ -69,13 +90,13 @@ td {
|
||||
}
|
||||
}
|
||||
|
||||
td.hard-drop-animation {
|
||||
animation: hard-drop-animation ease-out .3s;
|
||||
td.trail-animation {
|
||||
animation: trail-animation ease-out .3s;
|
||||
}
|
||||
|
||||
@keyframes cleared-line-animation {
|
||||
from {
|
||||
background-color: rgb(206, 255, 255, 40%);
|
||||
background-color: #ceffff66;
|
||||
filter: saturate(50%) brightness(300%);
|
||||
box-shadow: -200px 0 5px white, 200px 0 5px white;
|
||||
}
|
||||
@ -84,17 +105,30 @@ td.hard-drop-animation {
|
||||
}
|
||||
}
|
||||
|
||||
tr.cleared-line-animation{
|
||||
tr.cleared-line-animation {
|
||||
animation: cleared-line-animation ease-out .3s;
|
||||
}
|
||||
|
||||
span {
|
||||
#holdTable .J,
|
||||
#holdTable .L,
|
||||
#holdTable .S,
|
||||
#holdTable .T,
|
||||
#holdTable .Z,
|
||||
#nextTable .J,
|
||||
#nextTable .L,
|
||||
#nextTable .S,
|
||||
#nextTable .T,
|
||||
#nextTable .Z {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
#messagesSpan {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
|
||||
color: #fffc;
|
||||
text-shadow: 1px 1px #000c;
|
||||
font-size: 3vmin;
|
||||
text-align: center;
|
||||
}
|
||||
@ -202,4 +236,4 @@ span {
|
||||
animation: game-over-animation;
|
||||
animation-timing-function: (0.4, 0, 0.6, 1);
|
||||
animation-duration: 2s;
|
||||
}
|
||||
}
|
105
css/electro.css
@ -1,20 +1,99 @@
|
||||
.mino {
|
||||
background-image: radial-gradient(
|
||||
farthest-corner at 4px 6px,
|
||||
rgba(204, 238, 247, 0.3) 0%,
|
||||
rgba(106, 197, 220, 0.3) 100%
|
||||
);
|
||||
border: 1px solid rgba(127, 229, 255, 0.7);
|
||||
border-radius: 0.3vmin;
|
||||
body {
|
||||
background-image: url("electro/bg.jpg");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
body[data-bs-theme="dark"] {
|
||||
--bs-body-bg: #2125296b;
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
--bs-btn-bg: #2125296b;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: rgba(37, 41, 45, 40%);
|
||||
}
|
||||
|
||||
tr.matrix td:not(.mino) {
|
||||
border-left : none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mino:not(.ghost):not(.locking):not(.disabled) {
|
||||
padding: 1px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(128, 128, 128, 25%);
|
||||
box-shadow: 0px 0px 8px rgba(128, 128, 128, 75%);
|
||||
}
|
||||
|
||||
.mino:not(.ghost):not(.locking):before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
margin: 1px 1px 0px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
--glint-x: calc(50% + 50% * (var(--piece-column) - var(--column))/10);
|
||||
--glint-y: calc(50% + 50% * (var(--piece-row) - var(--row))/25);
|
||||
background: radial-gradient(
|
||||
at var(--glint-x) var(--glint-y),
|
||||
rgba(204, 238, 247, 0.9) 0%,
|
||||
rgba(10, 159, 218, 0.9) 150%);
|
||||
mask:
|
||||
linear-gradient(#666 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-mode: luminance;
|
||||
mask-composite: intersect;
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 0.3vmin;
|
||||
background: rgba(242, 255, 255, 10%);
|
||||
border : 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 0px 10px rgba(242, 255, 255, 75%);
|
||||
}
|
||||
|
||||
.moving.mino {
|
||||
box-shadow: 0px 0px 5px rgba(128, 128, 128, 75%);
|
||||
}
|
||||
|
||||
.moving.mino:not(.locking) {
|
||||
padding: 2px;
|
||||
background: rgba(186, 211, 255, 30%);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
border-width: 3px;
|
||||
background: rgba(186, 211, 255, 70%);
|
||||
border-color: rgba(242, 255, 255, 0.7);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 0px 10px rgba(242, 255, 255, 100%);
|
||||
}
|
||||
|
||||
.disabled.mino {
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.disabled.mino:before {
|
||||
opacity: 50%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@keyframes locked-animation {
|
||||
from {
|
||||
opacity: 1;
|
||||
background: white;
|
||||
border-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
background: rgba(242, 255, 255, 0.5);
|
||||
border-color: rgba(242, 255, 255, 0.7);
|
||||
animation: locked-animation;
|
||||
animation-duration: 0.2s;
|
||||
}
|
BIN
css/electro/bg.jpg
Normal file
After Width: | Height: | Size: 456 KiB |
108
css/minimal.css
Normal file
@ -0,0 +1,108 @@
|
||||
.card {
|
||||
background-color: #363941;
|
||||
}
|
||||
|
||||
.minoes-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
filter: drop-shadow(5px 8px 0 rgba(9, 9, 9, 22%));
|
||||
}
|
||||
|
||||
.minoes-table tr {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
z-index: calc(100 - var(--row));
|
||||
}
|
||||
|
||||
tr.matrix td:not(.mino) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.minoes-table td {
|
||||
display: inline-block;
|
||||
width: var(--cell-side);
|
||||
height: var(--cell-side);
|
||||
padding: 0 !important;
|
||||
z-index: calc(200 - var(--row));
|
||||
}
|
||||
|
||||
.mino {
|
||||
background: var(--background-color);
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
display: block;
|
||||
box-shadow: 0 -6px 0 var(--box-shadow-color);
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
--background-color: #42AFE1;
|
||||
--box-shadow-color: #6CEAFF;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
--background-color: #1165B5;
|
||||
--box-shadow-color: #339BFF;
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
--background-color: #F38927;
|
||||
--box-shadow-color: #FFBA59;
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
--background-color: #F6D03C;
|
||||
--box-shadow-color: #FFFF7F;
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
--background-color: #51B84D;
|
||||
--box-shadow-color: #84F880;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
--background-color: #9739A2;
|
||||
--box-shadow-color: #D958E9;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
--background-color: #EB4F65;
|
||||
--box-shadow-color: #FF7F79;
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
opacity: 50%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.moving.mino {
|
||||
filter: saturate(80%) brightness(110%);
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
filter: saturate(50%) brightness(130%);
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
animation: locked-animation;
|
||||
animation-duration: 0.2s;
|
||||
}
|
||||
|
||||
.disabled.mino {
|
||||
filter: brightness(50%) contrast(50%);
|
||||
}
|
||||
|
||||
@keyframes locked-animation {
|
||||
from {
|
||||
filter: saturate(50%) brightness(300%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cleared-line-animation {
|
||||
from {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
.mino {
|
||||
background: rgba(255, 255, 255, 10%);
|
||||
border: 5px solid;
|
||||
padding: 0;
|
||||
opacity: 100%;
|
||||
border-radius: 4px;
|
||||
filter: blur(1px)
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
border-color: #9bf6ff;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
border-color: #a0c4ff;
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
border-color: #ffd6a5;
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
border-color: #fdffb6;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
border-color: #bdb2ff;
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
border-color: #caffbf;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
border-color: #ffadad;
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
border-color: white;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
filter: brightness(150%) blur(2px);
|
||||
}
|
155
css/opera.css
Normal file
@ -0,0 +1,155 @@
|
||||
body {
|
||||
--bs-gutter-x: 0;
|
||||
background: black !important;
|
||||
}
|
||||
|
||||
#screenRow {
|
||||
gap: 0 !important;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .1em;
|
||||
}
|
||||
|
||||
#screenRow {
|
||||
--bs-gutter-x: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: black;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0.5em !important;
|
||||
}
|
||||
|
||||
.card-header,
|
||||
.card-header th{
|
||||
background: transparent;
|
||||
font-weight: 400 !important;
|
||||
font-size: 1.3em;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#screenRow .table {
|
||||
--bs-border-width: 0;
|
||||
}
|
||||
|
||||
#holdTable {
|
||||
margin: 0 0 0 auto !important;
|
||||
}
|
||||
|
||||
#holdTable,
|
||||
#nextTable {
|
||||
border-bottom: 2px solid white;
|
||||
}
|
||||
|
||||
#statsTable {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
#statsTable tr {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#statsTable td,
|
||||
#statsTable th {
|
||||
margin: 0;
|
||||
padding: 0 !important;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#statsTable th {
|
||||
display: inline;
|
||||
flex-flow: row;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
width: 200%;
|
||||
}
|
||||
|
||||
#statsTable td {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#matrixCard {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
border-left: 4px solid white;
|
||||
border-right: 4px solid white;
|
||||
border-bottom: 4px solid white;
|
||||
}
|
||||
|
||||
.mino {
|
||||
padding: 0;
|
||||
opacity: 100%;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
background-color: #42AFE1;
|
||||
border-color: #6CEAFF;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
background-color: #1165B5;
|
||||
border-color: #339BFF;
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
background-color: #F38927;
|
||||
border-color: #FFBA59;
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
background-color: #F6D03C;
|
||||
border-color: #FFFF7F;
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
background-color: #32ee3e;
|
||||
border-color: #84F880;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
background-color: #9739A2;
|
||||
border-color: #D958E9;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
background-color: #EB4F65;
|
||||
border-color: #FF7F79;
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
background-color: #fff4;
|
||||
border-color: #fff8;
|
||||
}
|
||||
|
||||
.moving.mino {
|
||||
filter: saturate(80%) brightness(150%);
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
filter: saturate(50%) brightness(200%);
|
||||
}
|
||||
|
||||
.disabled.mino {
|
||||
filter: brightness(50%) contrast(80%);
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
@keyframes locked-animation {
|
||||
from {
|
||||
filter: saturate(50%) brightness(400%);
|
||||
}
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
animation: locked-animation;
|
||||
animation-duration: 0.2s;
|
||||
}
|
123
css/pop.css
Normal file
@ -0,0 +1,123 @@
|
||||
body {
|
||||
background-image: url("pop/bg.png");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
body[data-bs-theme="dark"] {
|
||||
--bs-body-bg: #2125296b;
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
--bs-btn-bg: #2125296b;
|
||||
}
|
||||
|
||||
.card,
|
||||
#matrixCard {
|
||||
background: repeating-linear-gradient(transparent, #111 1px);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(37, 41, 45, 50%);
|
||||
}
|
||||
|
||||
#matrixTable {
|
||||
border-spacing: 1px;
|
||||
}
|
||||
|
||||
tr.matrix td:not(.mino) {
|
||||
border: 1px solid #111;
|
||||
}
|
||||
|
||||
.mino {
|
||||
background: radial-gradient(#fff3 0%, var(--color) 170%);
|
||||
border: 2px solid var(--color);
|
||||
border-radius: 0;
|
||||
outline: 1px solid #0006;
|
||||
opacity: 100%;
|
||||
box-shadow: 0 0 12px var(--color);
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
--color: #00eaf5;
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
--color: #00a9f7;
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
--color: #f9b600;
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
--color: #f7f200;;
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
--color: #d136e2;;
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
--color: #35da3f;
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
--color: #ee3b3a;
|
||||
}
|
||||
|
||||
.ghost.mino {
|
||||
background: transparent;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.moving.mino {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
--color: white;
|
||||
box-shadow: 0 0 10px var(--color);
|
||||
}
|
||||
|
||||
@keyframes locked-animation {
|
||||
from {
|
||||
background: white;
|
||||
--color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
animation: locked-animation;
|
||||
animation-duration: 0.2s;
|
||||
}
|
||||
|
||||
.disabled.mino {
|
||||
outline: 0px;
|
||||
box-shadow: none;
|
||||
filter: contrast(40%) brightness(50%);
|
||||
}
|
||||
|
||||
tr.cleared-line-animation {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
tr.cleared-line-animation::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: var(--cell-side);
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: block;
|
||||
background: repeating-linear-gradient(transparent, #fffb 1px);
|
||||
opacity: 0;
|
||||
animation: cleared-line-animation ease-out .3s;
|
||||
}
|
||||
|
||||
@keyframes cleared-line-animation {
|
||||
25% {
|
||||
width: 200%;
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
BIN
css/pop/bg.png
Normal file
After Width: | Height: | Size: 444 KiB |
156
css/retro.css
Normal file
@ -0,0 +1,156 @@
|
||||
:root {
|
||||
--cell-side: 20px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Early GameBoy";
|
||||
src: url("retro/Early GameBoy.ttf");
|
||||
}
|
||||
|
||||
body {
|
||||
}
|
||||
|
||||
#screenRow {
|
||||
background-image: url("retro/bg.png");
|
||||
background-size: 10px;
|
||||
padding: 40px 20px;
|
||||
border: 3px inset black;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.col {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #8D8E04;
|
||||
border-radius: 0;
|
||||
border-image-source: url("retro/border-sm.png");
|
||||
border-image-slice: 25;
|
||||
border-image-width: 13px;
|
||||
border-image-repeat: repeat;
|
||||
border-image-outset: 12px;
|
||||
box-shadow: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: #8D8E04;
|
||||
}
|
||||
|
||||
#matrixCard {
|
||||
background: #808302;
|
||||
border-image-source: url("retro/border-lg.png");
|
||||
border-image-slice: 30;
|
||||
border-image-width: 15px;
|
||||
border-image-outset: 15px;
|
||||
}
|
||||
|
||||
#statsTable,
|
||||
.card,
|
||||
.card-header,
|
||||
#messagesSpan {
|
||||
font-family: "Early GameBoy", monospace;
|
||||
font-smooth: never;
|
||||
-webkit-font-smoothing: none;
|
||||
color: #254806;
|
||||
text-shadow: -1px -1px 3px rgba(0, 0, 0, 40%), 1px 1px 1px rgba(0, 0, 0, 40%);
|
||||
}
|
||||
|
||||
#statsTable {
|
||||
font-size: .7em;
|
||||
letter-spacing: -.1em;
|
||||
}
|
||||
|
||||
#statsTable tr {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
#statsTable th,
|
||||
#statsTable td {
|
||||
border: 0;
|
||||
padding: 0 .2rem;
|
||||
}
|
||||
|
||||
#statsTable td {
|
||||
width: auto;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
#messagesSpan {
|
||||
text-shadow: -2px -2px #808302, -2px 2px #808302, 2px -2px #808302, 2px 2px #808302;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.mino {
|
||||
box-shadow: -2px -2px 5px rgba(0, 0, 0, 40%), 1px 1px 2px rgba(0, 0, 0, 40%);
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.I.mino {
|
||||
background-image: url("retro/I-mino.png")
|
||||
}
|
||||
|
||||
.J.mino {
|
||||
background-image: url("retro/J-mino.png")
|
||||
}
|
||||
|
||||
.L.mino {
|
||||
background-image: url("retro/L-mino.png")
|
||||
}
|
||||
|
||||
.O.mino {
|
||||
background-image: url("retro/O-mino.png")
|
||||
}
|
||||
|
||||
.S.mino {
|
||||
background-image: url("retro/S-mino.png")
|
||||
}
|
||||
|
||||
.T.mino {
|
||||
background-image: url("retro/T-mino.png")
|
||||
}
|
||||
|
||||
.Z.mino {
|
||||
background-image: url("retro/Z-mino.png")
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.locking.mino {
|
||||
animation: blinker 0.08s step-start infinite;
|
||||
}
|
||||
|
||||
.ghost.mino,
|
||||
.disabled.mino {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.locked.mino {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.hard-dropped-table-animation {
|
||||
animation: hard-dropped-table-animation steps(1) .2s;
|
||||
}
|
||||
|
||||
@keyframes cleared-line-animation {
|
||||
10%, 30%, 50%, 70%, 90% {
|
||||
opacity: 0;
|
||||
}
|
||||
20%, 40%, 60%, 80% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
tr.cleared-line-animation {
|
||||
animation: cleared-line-animation ease-out .4s;
|
||||
}
|
BIN
css/retro/Early GameBoy.ttf
Normal file
BIN
css/retro/I-mino.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
css/retro/J-mino.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
css/retro/L-mino.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
css/retro/O-mino.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
css/retro/S-mino.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
css/retro/T-mino.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
css/retro/Z-mino.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
css/retro/bg.png
Normal file
After Width: | Height: | Size: 870 B |
BIN
css/retro/border-lg.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
css/retro/border-sm.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
css/retro/edo3adc8h2re1.jpeg
Normal file
After Width: | Height: | Size: 298 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
favicons/I-0.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
favicons/I-1.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
favicons/I-2.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
favicons/I-3.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
favicons/J-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/J-1.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/J-2.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/J-3.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
favicons/L-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/L-1.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/L-2.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/L-3.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/O-0.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
favicons/S-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/S-1.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/S-2.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/S-3.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/T-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/T-1.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/T-2.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/T-3.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/Z-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/Z-1.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/Z-2.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
favicons/Z-3.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
246
index.html
@ -1,104 +1,91 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>QUATRIS</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Quatuor</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@5.3.0-alpha3/dist/css/bootstrap.min.css" 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/classic.css" rel="stylesheet" title="Classique">
|
||||
<link href="css/electro.css" rel="alternate stylesheet" title="Électro">
|
||||
<link href="css/new-wave.css" rel="alternate stylesheet" title="New Wave">
|
||||
<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="stylesheet" href="css/common.css">
|
||||
<link rel="stylesheet" href="css/classic.css" title="Thème sélectionné" id="selectedStyleSheet">
|
||||
<link rel="alternate stylesheet" href="css/classic.css" title="Classique">
|
||||
<link rel="alternate stylesheet" href="css/minimal.css" title="Minimal">
|
||||
<link rel="alternate stylesheet" href="css/electro.css" title="Électro">
|
||||
<link rel="alternate stylesheet" href="css/pop.css" title="Pop">
|
||||
<link rel="alternate stylesheet" href="css/retro.css" title="Rétro">
|
||||
<link rel="alternate stylesheet" href="css/opera.css" title="Opéra">
|
||||
<link rel="alternate stylesheet" href="css/binaural.css" title="Binaural">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicons/T-2.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicons/favicon-16x16.png">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta property="og:title" content="Quatuor"/>
|
||||
<meta property="og:type" content="game"/>
|
||||
<meta property="og:url" content="https://adrien.malingrey.fr/jeux/quatuor/"/>
|
||||
<meta property="og:image" content="https://adrien.malingrey.fr/jeux/quatuor/thumbnail.png"/>
|
||||
<meta property="og:image:width" content="288"/>
|
||||
<meta property="og:image:height" content="288"/>
|
||||
<meta property="og:description" content="Un jeu avec un quatuor de blocs qui tombent."/>
|
||||
<meta property="og:locale" content="fr_FR"/>
|
||||
<meta property="og:site_name" content="adrien.malingrey.fr"/>
|
||||
</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 id="titleHeader" class="modal-title w-100 text-center">QUATRIS</h1>
|
||||
<h1 id="titleHeader" class="modal-title w-100 text-center">QUATUOR</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="settingsForm" class="needs-validation" novalidate>
|
||||
<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-2 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="moveLeft" id="moveLeftInput" type="text" class="form-control text-center" value="←" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input name="moveRight" id="moveRightInput" type="text" class="form-control text-center" value="→" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<label for="moveRightInput" title="Droite" class="col-sm-2 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-2 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="rotateCounterclockwise" id="rotateCounterclockwiseInput" type="text" class="form-control text-center" value="w" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input name="rotateClockwise" id="rotateClockwiseInput" type="text" class="form-control text-center" value="↑" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<label for="rotateClockwiseInput" title="Rotation horaire" class="col-sm-2 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-2 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-down-short"></i>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="softDrop" id="softDropInput" type="text" class="form-control text-center" value="↓" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input name="hardDrop" id="hardDropInput" type="text" class="form-control text-center" value="Espace" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<label for="hardDropInput" title="Chute rapide" class="col-sm-2 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-2 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="hold" id="holdInput" type="text" class="form-control text-center" value="c" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input name="pause" id="pauseInput" type="text" class="form-control text-center" value="Échap" onclick="changeKey(this)" placeholder="Touche ?" required>
|
||||
</div>
|
||||
<label for="pauseInput" title="Pause" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-pause"></i>
|
||||
</label>
|
||||
<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-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-left"></i></label>
|
||||
<div class="col-4"><input name="moveLeft" id="moveLeftInput" type="text" class="form-control text-center" value="←" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<div class="col-4"><input name="moveRight" id="moveRightInput" type="text" class="form-control text-center" value="→" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<label for="moveRightInput" title="Droite" class="col-2 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-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-counterclockwise"></i></label>
|
||||
<div class="col-4"><input name="rotateCounterclockwise" id="rotateCounterclockwiseInput" type="text" class="form-control text-center" value="w" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<div class="col-4"><input name="rotateClockwise" id="rotateClockwiseInput" type="text" class="form-control text-center" value="↑" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<label for="rotateClockwiseInput" title="Rotation horaire" class="col-2 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-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-down-short"></i></label>
|
||||
<div class="col-4"><input name="softDrop" id="softDropInput" type="text" class="form-control text-center" value="↓" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<div class="col-4"><input name="hardDrop" id="hardDropInput" type="text" class="form-control text-center" value="Espace" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<label for="hardDropInput" title="Chute rapide" class="col-2 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-2 col-form-label d-flex align-items-center justify-content-center"><i class="bi bi-arrow-left-right"></i></label>
|
||||
<div class="col-4"><input name="hold" id="holdInput" type="text" class="form-control text-center" value="c" onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<div class="col-4"><input name="pause" id="pauseInput" type="text" class="form-control text-center" value="Échap." onfocus="changeKey(this)" placeholder="Touche ?" title="Modifier la touche" required></div>
|
||||
<label for="pauseInput" title="Pause" class="col-2 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-2 col-form-label" title="Automatic Repeat Rate : période de répétition de l'action">ARR</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input name="arr" id="arrInput" type="number" class="form-control text-center" value="50" min="2" max="200" step="1">
|
||||
<div class="input-group-text">ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input name="das" id="dasInput" type="number" class="form-control text-center" value="300" min="100" max="500" step="5">
|
||||
<div class="input-group-text">ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="dasInput" class="col-sm-2 col-form-label" title="Delayed AutoShift : délai initial avant répétition">DAS</label>
|
||||
<fieldset id="autorepeatFieldset" 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-2 col-form-label"><abbr title="Automatic Repeat Rate : période de répétition de l'action">ARR</abbr></label>
|
||||
<div class="col-4"><div class="input-group"><input name="arr" id="arrInput" type="number" class="form-control text-center" value="50" min="2" max="200" step="1"><div class="input-group-text">ms</div></div></div>
|
||||
<div class="col-4"><div class="input-group"><input name="das" id="dasInput" type="number" class="form-control text-center" value="300" min="100" max="500" step="5"><div class="input-group-text">ms</div></div></div>
|
||||
<label for="dasInput" class="col-2 col-form-label"><abbr title="Delayed AutoShift : délai initial avant répétition">DAS</abbr></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-2 col-form-label text-center">Niveau</label>
|
||||
<div class="col-sm-4">
|
||||
<fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Interface</legend>
|
||||
<label for="stylesheetSelect" class="col-2 col-form-label">Thème</label>
|
||||
<div class="col-4"><select name="stylesheet" id="stylesheetSelect" class="form-select" oninput="selectedStyleSheet.href = this.value">
|
||||
<option value="css/classic.css" selected>Classique</option>
|
||||
<option value="css/minimal.css">Minimal</option>
|
||||
<option value="css/pop.css">Pop</option>
|
||||
<option value="css/electro.css">Électro</option>
|
||||
<option value="css/retro.css">Rétro</option>
|
||||
<option value="css/opera.css">Opéra</option>
|
||||
<option value="css/binaural.css">Binaural</option>
|
||||
</select></div>
|
||||
<div class="col-4 d-flex align-items-baseline"><input name="sfxVolumeRange" id="sfxVolumeRange" class="form-range" type="range" min="0" max="1" step="any" value="0.7"></div>
|
||||
<label for="sfxVolumeRange" class="col-2 col-form-label">Volume</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-2 col-form-label text-center">Niveau</label>
|
||||
<div class="col-4">
|
||||
<input name="startLevel" id="levelInput" type="number" class="form-control text-center" value="1" min="1" max="15">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-4">
|
||||
<button id="resumeButton" type="submit" class="btn btn-primary w-100" autofocus>Jouer</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
@ -107,13 +94,15 @@
|
||||
</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 id="sceneDiv" class="container-fluid d-flex vh-100 justify-content-center d-flex align-items-center">
|
||||
|
||||
<div id="screenRow" 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 shadow mb-4 w-100">
|
||||
<div class="card-header fw-bold text-uppercase text-center">Hold</div>
|
||||
<div class="card-body p-0">
|
||||
<table id="holdTable" class="minoes-table">
|
||||
<table id="holdTable" class="minoes-table m-auto">
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
@ -122,20 +111,19 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card w-100">
|
||||
<table id="statsTable" class="table mb-0 align-middle">
|
||||
<tr><td>Score</td><th id="scoreCell" class="text-end">0</th></tr>
|
||||
<tr><td>Meilleur score</td><th id="highScoreCell" class="text-end">
|
||||
<script>document.write(Number(localStorage["highScore"]) || 0)</script>
|
||||
</th></tr>
|
||||
<tr><td>Niveau</td><th id="levelCell" class="text-end">0</th></tr>
|
||||
<tr><td>But</td><th id="goalCell" class="text-end">0</th></tr>
|
||||
<tr><td>Temps</td><th id="timeCell" class="text-end">00:00</th></tr>
|
||||
<div class="card shadow">
|
||||
<table id="statsTable" class="table mb-0">
|
||||
<tr class="card-header fw-bold text-uppercase"><th>Score</th><td id="scoreCell">0</td> </tr>
|
||||
<tr><th>Meilleur<br/>score</th><td id="highScoreCell"><script>document.write(Number(localStorage["highScore"]) || 0)</script></td></tr>
|
||||
<tr><th>Niveau</th> <td id="levelCell">0</td> </tr>
|
||||
<tr><th>But</th> <td id="goalCell">0</td> </tr>
|
||||
<tr><th>Temps</th> <td id="timeCell">00:00</td> </tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
|
||||
<div class="col position-relative">
|
||||
<div id="matrixCard" class="card shadow">
|
||||
<table id="matrixTable" class="minoes-table" style="--buffer-zone-rows: 5">
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
@ -164,11 +152,14 @@
|
||||
<tr class="matrix"><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="messagesSpan"></span>
|
||||
<span id="messagesSpan">
|
||||
<div class="show-level-animation">Chargement...</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-center"><strong>NEXT</strong></div>
|
||||
<div class="card shadow">
|
||||
<div class="card-header fw-bold text-uppercase text-center">Next</div>
|
||||
<div class="card-body p-0">
|
||||
<table id="nextTable" class="minoes-table caption-top">
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
@ -189,16 +180,57 @@
|
||||
<tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="statsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title w-100 text-center">Fin</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table class="table mb-0">
|
||||
<tr><th>Score</th> <td id="statsModalScoreCell"></td> <th>Niveau</th> <td id="statsModalLevelCell"></td> </tr>
|
||||
<tr><th>Meilleur score</th><td id="statsModalHighScoreCell"></td> <th>Temps</th> <td id="statsModalTimeCell"></td> </tr>
|
||||
<tr><th>Lignes</th> <td id="statsModaltotalClearedLines"></td><th>Lignes par minute</th> <td id="statsModaltotalClearedLinesPM"></td></tr>
|
||||
<tr><th>Quatuors</th> <td id="statsModalNbQuatuors"></td> <th>Plus long combo</th> <td id="statsModalMaxCombo"></td> </tr>
|
||||
<tr><th>Pirouettes</th> <td id="statsModalNbTSpin"></td> <th>Plus long bout en bout</th><td id="statsModalMaxB2B"></td> </tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="restart()">Rejouer ?</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span style="display: none;">
|
||||
<img src="favicons/I-0.png"/><img src="favicons/I-1.png"/><img src="favicons/I-2.png"/><img src="favicons/I-3.png"/>
|
||||
<img src="favicons/J-0.png"/><img src="favicons/J-1.png"/><img src="favicons/J-2.png"/><img src="favicons/J-3.png"/>
|
||||
<img src="favicons/L-0.png"/><img src="favicons/L-1.png"/><img src="favicons/L-2.png"/><img src="favicons/L-3.png"/>
|
||||
<img src="favicons/O-0.png"/>
|
||||
<img src="favicons/S-0.png"/><img src="favicons/S-1.png"/><img src="favicons/S-2.png"/><img src="favicons/S-3.png"/>
|
||||
<img src="favicons/T-0.png"/><img src="favicons/T-1.png"/><img src="favicons/T-2.png"/><img src="favicons/T-3.png"/>
|
||||
<img src="favicons/Z-0.png"/><img src="favicons/Z-1.png"/><img src="favicons/Z-2.png"/><img src="favicons/Z-3.png"/>
|
||||
|
||||
<audio id="wallSound" src="sounds/808K_A.wav" preload="auto" type="audio/wav"></audio>
|
||||
<audio id="hardDropSound" src="sounds/909S.wav" preload="auto" type="audio/wav"></audio>
|
||||
<audio id="lineClearSound" src="sounds/808COW.wav" preload="auto" type="audio/wav"></audio>
|
||||
<audio id="tSpinSound" src="sounds/78GUIR.wav" preload="auto" type="audio/wav"></audio>
|
||||
<audio id="quatuorSound" src="sounds/BRRDC1.wav" preload="auto" type="audio/wav"></audio>
|
||||
</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="app.js"></script>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
<script src="js/game_logic.js" language="Javascript" type="text/javascript"></script>
|
||||
<script src="js/interface.js" language="Javascript" type="text/javascript"></script>
|
||||
<script src="js/app.js" language="Javascript" type="text/javascript"></script>
|
||||
<script>navigator?.serviceWorker.register('js/service-worker.js')</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
314
js/app.js
Normal file
@ -0,0 +1,314 @@
|
||||
let scheduler = new Scheduler()
|
||||
let settings = new Settings()
|
||||
let stats = new Stats()
|
||||
let holdQueue = new HoldQueue()
|
||||
let matrix = new Matrix()
|
||||
let nextQueue = new NextQueue()
|
||||
let playing = false
|
||||
//let lastActionSucceded = true
|
||||
let favicon
|
||||
|
||||
window.onload = function(event) {
|
||||
document.selectedStyleSheetSet = selectedStyleSheet.title
|
||||
selectedStyleSheet.href = stylesheetSelect.value
|
||||
favicon = document.querySelector("link[rel~='icon']")
|
||||
|
||||
restart()
|
||||
}
|
||||
|
||||
function restart() {
|
||||
stats.modal.hide()
|
||||
holdQueue.init()
|
||||
holdQueue.redraw()
|
||||
stats.init()
|
||||
matrix.init()
|
||||
nextQueue.init()
|
||||
settings.init()
|
||||
pauseSettings()
|
||||
}
|
||||
|
||||
function pauseSettings() {
|
||||
scheduler.clearInterval(fall)
|
||||
scheduler.clearTimeout(lockDown)
|
||||
scheduler.clearTimeout(repeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
scheduler.clearInterval(ticktack)
|
||||
stats.pauseTime = stats.time
|
||||
|
||||
document.onkeydown = null
|
||||
|
||||
settings.show()
|
||||
}
|
||||
|
||||
function newGame(event) {
|
||||
if (!settings.form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
settings.form.reportValidity()
|
||||
settings.form.classList.add('was-validated')
|
||||
} else {
|
||||
const audioContext = new AudioContext()
|
||||
for(const sound of document.getElementsByTagName("audio")) {
|
||||
sound.preservesPitch = false
|
||||
audioContext.createMediaElementSource(sound).connect(audioContext.destination)
|
||||
}
|
||||
|
||||
levelInput.name = "level"
|
||||
levelInput.disabled = true
|
||||
titleHeader.innerHTML = "PAUSE"
|
||||
resumeButton.innerHTML = "Reprendre"
|
||||
event.target.onsubmit = resume
|
||||
stats.level = levelInput.valueAsNumber
|
||||
localStorage["startLevel"] = levelInput.value
|
||||
playing = true
|
||||
onblur = pauseSettings
|
||||
resume(event)
|
||||
}
|
||||
}
|
||||
|
||||
function resume(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
settings.form.reportValidity()
|
||||
settings.form.classList.add('was-validated')
|
||||
|
||||
if (settings.form.checkValidity()) {
|
||||
for(const sound of document.getElementsByTagName("audio"))
|
||||
sound.volume = sfxVolumeRange.value
|
||||
|
||||
settings.modal.hide()
|
||||
settings.getInputs()
|
||||
|
||||
document.onkeydown = onkeydown
|
||||
document.onkeyup = onkeyup
|
||||
|
||||
stats.time = stats.pauseTime
|
||||
scheduler.setInterval(ticktack, 1000)
|
||||
|
||||
if (matrix.piece) scheduler.setInterval(fall, stats.fallPeriod)
|
||||
else generate()
|
||||
}
|
||||
}
|
||||
|
||||
function ticktack() {
|
||||
timeCell.innerText = stats.timeFormat.format(stats.time)
|
||||
}
|
||||
|
||||
function generate(piece) {
|
||||
matrix.piece = piece || nextQueue.shift()
|
||||
if (!piece && holdQueue.piece) holdQueue.drawPiece()
|
||||
//lastActionSucceded = true
|
||||
favicon.href = matrix.piece.favicon_href
|
||||
|
||||
if (matrix.piece.canMove(TRANSLATION.NONE)) {
|
||||
scheduler.setInterval(fall, stats.fallPeriod)
|
||||
} else {
|
||||
gameOver() // block out
|
||||
}
|
||||
}
|
||||
|
||||
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: () => matrix.piece.move(TRANSLATION.DOWN) && ++stats.score,
|
||||
|
||||
hardDrop: function() {
|
||||
scheduler.clearTimeout(lockDown)
|
||||
playSound(hardDropSound)
|
||||
while (matrix.piece.move(TRANSLATION.DOWN, ROTATION.NONE, true)) stats.score += 2
|
||||
matrixCard.classList.add("hard-dropped-table-animation")
|
||||
lockDown()
|
||||
return true
|
||||
},
|
||||
|
||||
hold: function() {
|
||||
if (matrix.piece.holdEnabled) {
|
||||
scheduler.clearInterval(fall)
|
||||
scheduler.clearTimeout(lockDown)
|
||||
|
||||
let piece = matrix.piece
|
||||
piece.facing = FACING.NORTH
|
||||
piece.locked = false
|
||||
generate(holdQueue.piece)
|
||||
matrix.piece.holdEnabled = false
|
||||
holdQueue.piece = piece
|
||||
}
|
||||
},
|
||||
|
||||
pause: pauseSettings,
|
||||
}
|
||||
|
||||
// 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]
|
||||
/*if (action()) {
|
||||
lastActionSucceded = true
|
||||
} else if (lastActionSucceded || !(action in REPEATABLE_ACTIONS)) {
|
||||
playSound(wallSound)
|
||||
lastActionSucceded = false
|
||||
}*/
|
||||
action()
|
||||
if (REPEATABLE_ACTIONS.includes(action)) {
|
||||
actionsQueue.unshift(action)
|
||||
scheduler.clearTimeout(repeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
if (action == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod/20)
|
||||
else scheduler.setTimeout(repeat, settings.das)
|
||||
}
|
||||
matrix.drawPiece()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function repeat() {
|
||||
if (actionsQueue.length) {
|
||||
actionsQueue[0]()
|
||||
scheduler.setInterval(autorepeat, settings.arr)
|
||||
}
|
||||
}
|
||||
|
||||
function autorepeat() {
|
||||
if (actionsQueue.length) {
|
||||
/*if (actionsQueue[0]()) {
|
||||
lastActionSucceded = true
|
||||
} else if (lastActionSucceded) {
|
||||
wallSound.play()
|
||||
lastActionSucceded = false
|
||||
}*/
|
||||
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)
|
||||
scheduler.clearTimeout(repeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
if (actionsQueue.length) {
|
||||
if (actionsQueue[0] == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod/20)
|
||||
else scheduler.setTimeout(repeat, settings.das)
|
||||
} else {
|
||||
matrix.drawPiece()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fall() {
|
||||
matrix.piece.move(TRANSLATION.DOWN)
|
||||
}
|
||||
|
||||
function lockDown() {
|
||||
scheduler.clearTimeout(lockDown)
|
||||
scheduler.clearInterval(fall)
|
||||
|
||||
if (matrix.lock()) {
|
||||
stats.lockDown(matrix.piece.tSpin, matrix.clearLines())
|
||||
|
||||
generate()
|
||||
} else {
|
||||
gameOver() // lock out
|
||||
}
|
||||
}
|
||||
|
||||
onanimationend = function (event) {
|
||||
event.target.classList.remove(event.animationName)
|
||||
}
|
||||
|
||||
messagesSpan.onanimationend = function(event) {
|
||||
event.target.remove()
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
matrix.piece.locked = true
|
||||
matrix.drawPiece()
|
||||
|
||||
document.onkeydown = null
|
||||
onblur = null
|
||||
scheduler.clearInterval(ticktack)
|
||||
playing = false
|
||||
|
||||
stats.show()
|
||||
}
|
||||
|
||||
window.onbeforeunload = function(event) {
|
||||
stats.save()
|
||||
settings.save()
|
||||
if (playing) return false;
|
||||
}
|
||||
|
||||
// Play with 3D
|
||||
let mousedown = false
|
||||
let rX0 = 0
|
||||
let rY0 = 0
|
||||
let clientX0 = 0
|
||||
let clientY0 = 0
|
||||
|
||||
sceneDiv.onmousedown = function(event) {
|
||||
mousedown = true
|
||||
rX0 = parseInt(getComputedStyle(screenRow).getPropertyValue("--rX"))
|
||||
dy0 = parseInt(getComputedStyle(screenRow).getPropertyValue("--rY"))
|
||||
clientX0 = event.clientX
|
||||
clientY0 = event.clientY
|
||||
}
|
||||
|
||||
sceneDiv.onmousemove = function(event) {
|
||||
if (mousedown) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
rX = (rX0 - event.clientY + clientY0 + 360) % 360
|
||||
screenRow.style.setProperty("--rX", rX + "deg")
|
||||
if (rX <= 180) {
|
||||
screenRow.classList.remove("top")
|
||||
screenRow.classList.add("bottom")
|
||||
} else {
|
||||
screenRow.classList.add("top")
|
||||
screenRow.classList.remove("bottom")
|
||||
}
|
||||
rY = (rY0 + event.clientX - clientX0 + 360) % 360
|
||||
screenRow.style.setProperty("--rY", rY + "deg")
|
||||
if (rY >= 180) {
|
||||
screenRow.classList.remove("left")
|
||||
screenRow.classList.add("right")
|
||||
} else {
|
||||
screenRow.classList.add("left")
|
||||
screenRow.classList.remove("right")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sceneDiv.onmouseup = document.onmouseleave = function(event) {
|
||||
mousedown = false
|
||||
}
|
||||
|
||||
sceneDiv.onwheel = function(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
let tZ = parseInt(getComputedStyle(screenRow).getPropertyValue("--tZ"))
|
||||
tZ += event.deltaY
|
||||
screenRow.style.setProperty("--tZ", tZ + "px")
|
||||
}
|
464
js/game_logic.js
Normal file
@ -0,0 +1,464 @@
|
||||
/* 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,
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
if (this.intervalTasks.has(func)) {
|
||||
console.warn(`$func already in intervalTasks`)
|
||||
return false
|
||||
} else {
|
||||
this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(func, delay, ...args) {
|
||||
if (this.timeoutTasks.has(func)) {
|
||||
console.warn(`$func already in timeoutTasks`)
|
||||
return false
|
||||
} else {
|
||||
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
clearInterval(func) {
|
||||
if (this.intervalTasks.has(func)) {
|
||||
window.clearInterval(this.intervalTasks.get(func))
|
||||
this.intervalTasks.delete(func)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(func) {
|
||||
if (this.timeoutTasks.has(func)) {
|
||||
window.clearTimeout(this.timeoutTasks.get(func))
|
||||
this.timeoutTasks.delete(func)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 HoldQueue extends MinoesTable {
|
||||
constructor() {
|
||||
super("holdTable")
|
||||
}
|
||||
|
||||
drawPiece(piece=this.piece, className=piece.className) {
|
||||
if (!matrix.piece.holdEnabled) {
|
||||
className += " disabled"
|
||||
}
|
||||
super.drawPiece(piece, className)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 && 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]]
|
||||
]
|
334
js/interface.js
Normal file
@ -0,0 +1,334 @@
|
||||
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(target, key) {
|
||||
return key in target? target[key] : key
|
||||
}
|
||||
})
|
||||
|
||||
class Settings {
|
||||
constructor() {
|
||||
this.form = settingsForm
|
||||
this.load()
|
||||
this.modal = new bootstrap.Modal('#settingsModal')
|
||||
settingsModal.addEventListener('shown.bs.modal', () => resumeButton.focus())
|
||||
}
|
||||
|
||||
load() {
|
||||
this.form.querySelectorAll("[name]").forEach(element => {
|
||||
if (element.name in localStorage)
|
||||
element.value = localStorage[element.name]
|
||||
})
|
||||
window.document.selectedStyleSheetSet = stylesheetSelect.value
|
||||
}
|
||||
|
||||
save() {
|
||||
this.form.querySelectorAll("[name]").forEach(element => 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 = new Proxy({}, {
|
||||
get: (target, key) => target[key.toLowerCase()],
|
||||
set: (target, key, value) => target[key.toLowerCase()] = value,
|
||||
has: (target, key) => key.toLowerCase() in target
|
||||
|
||||
})
|
||||
for (let actionName in playerActions) {
|
||||
this.keyBind[settings[actionName]] = playerActions[actionName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeKey(input) {
|
||||
prevValue = input.value
|
||||
input.value = ""
|
||||
keyInputs = Array.from(input.form.querySelectorAll("input[type='text']"))
|
||||
input.onkeydown = function (event) {
|
||||
event.preventDefault()
|
||||
input.value = KEY_NAMES[event.key]
|
||||
keyInputs.forEach(input => {
|
||||
input.setCustomValidity("")
|
||||
input.classList.remove("is-invalid")
|
||||
})
|
||||
keyInputs.sort((input1, input2) => {
|
||||
if(input1.value == input2.value) {
|
||||
input1.setCustomValidity("Touche déjà utilisée")
|
||||
input1.classList.add("is-invalid")
|
||||
input2.setCustomValidity("Touche déjà utilisée")
|
||||
input2.classList.add("is-invalid")
|
||||
}
|
||||
return input1.value > input2.value
|
||||
})
|
||||
if (input.checkValidity()) {
|
||||
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(tSpin, nbClearedLines) {
|
||||
this.totalClearedLines += nbClearedLines
|
||||
if (nbClearedLines == 4) this.nbQuatuors++
|
||||
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
|
||||
|
||||
// Cleared lines & T-Spin
|
||||
let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
|
||||
let patternScore = 100 * this.level * awardedLineClears
|
||||
if (tSpin) messagesSpan.addNewChild("div", {
|
||||
className: "rotate-in-animation",
|
||||
innerHTML: tSpin
|
||||
})
|
||||
if (nbClearedLines) messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
|
||||
})
|
||||
if (patternScore) {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .2s",
|
||||
innerHTML: patternScore
|
||||
})
|
||||
this.score += patternScore
|
||||
}
|
||||
|
||||
// Combo
|
||||
if (nbClearedLines) {
|
||||
this.combo++
|
||||
if (this.combo >= 1) {
|
||||
let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
|
||||
if (this.combo == 1) {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `COMBO<br/>${comboScore}`
|
||||
})
|
||||
} else {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `COMBO x${this.combo}<br/>${comboScore}`
|
||||
})
|
||||
}
|
||||
this.score += comboScore
|
||||
}
|
||||
} else {
|
||||
this.combo = -1
|
||||
}
|
||||
|
||||
// Back to back sequence
|
||||
if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
|
||||
this.b2b++
|
||||
if (this.b2b >= 1) {
|
||||
let b2bScore = patternScore / 2
|
||||
if (this.b2b == 1) {
|
||||
messagesSpan.addNewChild("div", {
|
||||
className: "zoom-in-animation",
|
||||
style: "animation-delay: .4s",
|
||||
innerHTML: `BOUT 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
|
||||
}
|
||||
|
||||
// Sound
|
||||
if (sfxVolumeRange.value) {
|
||||
if (nbClearedLines == 4) playSound(quatuorSound, this.combo)
|
||||
else if (nbClearedLines) playSound(lineClearSound, this.combo)
|
||||
if (tSpin) playSound(tSpinSound, this.combo)
|
||||
}
|
||||
|
||||
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, note=0) {
|
||||
sound.currentTime = 0
|
||||
sound.playbackRate = Math.pow(5/4, note)
|
||||
sound.play()
|
||||
}
|
@ -16,7 +16,7 @@ Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
|
||||
const OFFLINE_VERSION = 1;
|
||||
const CACHE_NAME = "offline";
|
||||
// Customize this with a different URL if needed.
|
||||
const OFFLINE_URL = "index.html";
|
||||
const OFFLINE_URL = "../index.html";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|