157 Commits

Author SHA1 Message Date
adrien ae883148f8 corrections 2026-01-03 05:00:02 +01:00
adrien 3f665d115a rename styles 2026-01-03 04:52:13 +01:00
adrien d2d4a8e737 classic ghost 2026-01-03 04:42:32 +01:00
adrien fb4755c870 Merge remote-tracking branch 'refs/remotes/malingrey.fr/master' 2026-01-03 03:05:04 +01:00
adrien 26aeb8f9a2 white border 2025-08-28 02:15:48 +02:00
adrien 051fc223e9 Add README 2025-05-21 10:22:42 +02:00
adrien 9feaa3c098 meta 2025-05-20 17:01:49 +02:00
adrien 3175e7b7ad mouse event only on sceneDiv 2025-04-24 20:41:33 +02:00
adrien 4281bf1735 grab 2025-04-21 05:03:50 +02:00
adrien ff37cb7823 jpg background 2025-04-14 08:40:52 +02:00
adrien 2a8df78d22 background 2025-04-14 08:30:40 +02:00
adrien 344575bdc4 center background 2025-04-13 23:28:06 +02:00
adrien 2453b2f6b6 background 2025-04-13 17:19:29 +02:00
adrien 25a98bf42d little tweaks 2025-04-10 00:52:43 +02:00
adrien 022512a5e6 perspective 2025-04-09 08:29:19 +02:00
adrien d56af40362 clear animation 2025-04-09 01:14:39 +02:00
adrien f33723a786 ghost border 2025-04-09 00:57:53 +02:00
adrien df95139650 ghost 2025-04-09 00:51:34 +02:00
adrien d3b527570c radial gradient 2025-04-08 03:42:07 +02:00
adrien a1722a700d floatting text 2025-04-08 03:30:01 +02:00
adrien 6cc8a9e645 fix zoom 2025-04-08 00:46:47 +02:00
adrien 9023252822 remove log 2025-04-08 00:29:00 +02:00
adrien 9bf3c0de0c zoom on wheel 2025-04-07 23:51:13 +02:00
adrien 09f4785ef4 3D 2025-04-07 23:37:24 +02:00
adrien bf9554d917 no need block 2025-04-07 09:36:51 +02:00
adrien c33d80facb add binaural (3D) style 2025-04-07 09:30:07 +02:00
adrien 9282f94956 add binaural (3D) style 2025-04-07 09:01:14 +02:00
adrien fda289dc9c opéra 2024-10-25 00:10:47 +02:00
adrien b6e58b41aa opera border 2024-10-21 09:05:13 +02:00
adrien bd5c7dad3b remove wall sound 2024-10-21 08:57:13 +02:00
adrien 7faae294dc new theme 2024-10-21 01:09:18 +02:00
adrien 3b59534f90 colored ghost 2024-09-28 17:32:07 +02:00
adrien ec08747066 more glowing 2024-08-18 02:49:01 +02:00
adrien db8a9a3f74 pop colors 2024-08-17 13:01:48 +02:00
adrien 0ac36444c4 disabled pop 2024-08-08 01:35:57 +02:00
adrien 5d451db8f9 disable first held piece 2024-08-03 00:07:23 +02:00
adrien ab023ec982 show disabled held piece 2024-08-02 23:24:39 +02:00
adrien d75696fbc3 update pop theme 2024-05-28 21:26:40 +02:00
adrien f97b5f0cf9 fix pop buffer zone 2024-05-18 04:54:47 +02:00
adrien ffefb77f3f serviceWorker in html 2024-05-12 11:31:31 +02:00
adrien 21929261e4 shadow 2024-04-29 17:51:17 +02:00
adrien 58389623cc move service worker 2024-04-29 17:51:04 +02:00
adrien 04f6eaf5dc fix action timout on key up 2023-12-23 03:02:47 +01:00
adrien d56a8a6b06 stripe line clear animation 2023-12-23 03:02:17 +01:00
adrien e1992f4c1e rgba 2023-12-23 03:02:01 +01:00
adrien 27d7a0689d rgba 2023-12-23 03:01:43 +01:00
adrien df62a40c2a update themes 2023-12-19 00:43:27 +01:00
adrien 2105296238 not mino border 2023-12-18 01:32:42 +01:00
adrien 05862b8587 remove double lines 2023-12-18 01:29:01 +01:00
adrien 6c1291833d new Pop theme 2023-12-18 01:24:57 +01:00
adrien 095e3f8dbe modified: css/pop.css 2023-12-18 00:52:15 +01:00
adrien 82929b6f3b fix I color 2023-12-18 00:26:11 +01:00
adrien 29661421a5 fix input onblur 2023-12-14 00:39:15 +01:00
adrien d3a6f5d6d6 check for key duplicates 2023-12-13 23:14:28 +01:00
adrien 8af1801527 sometime wall sound reset 2023-12-11 23:43:56 +01:00
adrien 6d95acddd2 fix autorepeat and volume save 2023-12-11 23:14:26 +01:00
adrien 6062f6895b aliasing 2023-12-08 22:00:18 +01:00
adrien b726e95bed center pieces in newt and hold tables 2023-12-08 21:21:20 +01:00
adrien 228fc56916 fix settings load 2023-12-08 12:12:15 +01:00
adrien f39d677b62 align hold table and stats 2023-12-08 08:52:39 +01:00
adrien fc21a1b12b change stylesheet for chrome & firefox 2023-12-08 02:13:33 +01:00
adrien 072661ff5a ghost 2023-12-08 00:54:46 +01:00
adrien f93f3c71af format 2023-12-08 00:50:56 +01:00
adrien ed4d9b82c9 improve electro style 2023-12-08 00:05:17 +01:00
adrien b17db2ffd4 prevent capslock 2023-12-07 23:36:06 +01:00
adrien 9f8c38e6bf update electro style 2023-12-07 22:41:28 +01:00
adrien 262a26940c this.form.querySelectorAll("[name]") 2023-12-07 22:36:00 +01:00
adrien 7996e4a7ae fix hard drop animation 2023-12-07 21:44:30 +01:00
adrien a0af16d0dd add transparency 2023-12-07 21:44:21 +01:00
adrien 9547e585ca format 2023-12-07 21:44:03 +01:00
adrien 4d0d51e20d move KEY_NAMES to interface.js 2023-12-07 21:43:50 +01:00
adrien e7246c0a5d fix styleSheet change on chrome 2023-12-07 21:29:48 +01:00
adrien 50502be1d1 little changes 2023-12-07 21:29:29 +01:00
adrien a89a60691c no wall nound on hard drop 2023-12-07 20:02:43 +01:00
adrien a04e118ff1 reduce blink frequency 2023-12-07 20:00:33 +01:00
adrien ef8ddca950 wall sound on lock 2023-12-07 00:48:54 +01:00
adrien 8773e65885 play wall sound only if player action succeded previously 2023-12-07 00:32:27 +01:00
adrien ebb9f4810b change tSpin sound 2023-12-07 00:29:53 +01:00
adrien baf6e66244 change tSpin sound 2023-12-06 08:50:27 +01:00
adrien 490566e49f tSpin & lineClear sound together 2023-12-06 02:30:26 +01:00
adrien e3d4520af7 format 2023-12-06 02:07:26 +01:00
adrien bf37ca8135 more drums! 2023-12-06 02:06:50 +01:00
adrien 048d17e041 change note on combo 2023-12-06 01:07:15 +01:00
adrien 365fb17694 small changes 2023-12-05 08:51:08 +01:00
adrien caca970a53 splt js 2023-12-03 22:00:43 +01:00
adrien f42a14d75f split js 2023-12-03 21:57:20 +01:00
adrien 46a0500c66 change sounds 2023-12-03 21:47:03 +01:00
adrien 227a26880d wall sound on soft drop 2023-12-03 06:50:48 +01:00
adrien ee4945bb27 wall sound 2023-12-03 03:15:33 +01:00
adrien c010ccba49 try to reduce audio latency 2023-12-03 02:45:47 +01:00
adrien 5e4d729803 Sounds! 2023-12-01 02:28:01 +01:00
adrien 8940c1b79b blur clear line animation 2023-12-01 01:30:13 +01:00
adrien a9e3557b56 retro stats width 2023-11-30 01:43:02 +01:00
adrien 4f1904acaa brief 2023-11-30 01:27:26 +01:00
adrien 895635b601 improve style 2023-11-29 02:05:55 +01:00
adrien 71a36fdc4d input as button, abbr 2023-11-28 23:42:20 +01:00
adrien 1b18b00619 improve styles 2023-11-28 23:25:15 +01:00
adrien 682b0c61a1 cleared line shadow 2023-11-13 16:05:42 +01:00
adrien 230f20befb less brightness 2023-11-13 09:04:44 +01:00
adrien a13d97b550 minimal cleared line 2023-11-13 08:53:28 +01:00
adrien f8466cb27f cleared-line-animation 2023-11-13 08:33:26 +01:00
adrien ea2463157f faster bllinking 2023-11-13 08:17:38 +01:00
adrien c1d6fb87eb glowing title 2023-11-12 22:43:13 +01:00
adrien 5e600bec56 bout en bout 2023-11-12 22:43:07 +01:00
adrien 31123fc332 rename QUATUOR 2023-11-07 23:53:46 +01:00
adrien cbf86e0bf5 less z-index 2023-11-07 22:10:07 +01:00
adrien 875fc21cac add minimal style 2023-11-07 21:42:37 +01:00
adrien 4f4447e889 small fixes 2023-07-01 22:10:56 +02:00
adrien b9bb673e7e hard-dropped-table-animation 2023-06-24 01:32:43 +02:00
adrien fd8b46a212 quicker blink 2023-06-18 19:01:42 +02:00
adrien 36d58eb526 preload favicons 2023-06-16 02:10:41 +02:00
adrien 57639b0c2a use piece for glint 2023-06-16 02:05:42 +02:00
adrien 2d687d5fcb electro ghost mino 2023-06-16 01:44:56 +02:00
adrien 6e089315bf update moving piece in electro style 2023-06-16 00:51:28 +02:00
adrien e6f238629a update style on moving and locked 2023-06-16 00:23:08 +02:00
adrien d615b6b72b locked animation 2023-06-16 00:06:49 +02:00
adrien 70d8e6e0aa locking class & moving on matrix 2023-06-15 23:56:44 +02:00
adrien ff20114a92 ghost glint on electro style 2023-06-15 23:48:55 +02:00
adrien 3318a716f1 fix locked held piece 2023-06-15 22:52:42 +02:00
adrien 1b82e05bb5 b2b & combo properties 2023-06-15 08:56:46 +02:00
adrien 4cee98ff49 update styles 2023-06-15 01:20:23 +02:00
adrien 36cba9b6ef update electro style 2023-06-14 23:05:40 +02:00
adrien 5bacff51fc change electro bg 2023-06-14 22:56:15 +02:00
adrien 0a491b89be bug fixes 2023-06-14 22:54:32 +02:00
adrien b4a241b3a9 update electro style 2023-06-14 20:34:46 +02:00
adrien ec5ed9425e update electro style 2023-06-14 20:34:21 +02:00
adrien fa5c5fb33b blink locked piece 2023-06-14 19:00:10 +02:00
adrien fd09458ee9 formatting 2023-06-14 18:15:28 +02:00
adrien ba313cbbd7 Backspace translation 2023-06-14 18:14:45 +02:00
adrien b358611c28 onblur = pausesettings after gameOver 2023-05-16 23:14:34 +02:00
adrien af35a5d6b2 remove duplicate pauseSettings() 2023-05-14 22:01:40 +02:00
adrien 3e13e77d26 restart 2023-05-14 21:22:23 +02:00
adrien 47bbc83788 fix T.tSpin 2023-05-10 09:05:24 +02:00
adrien dcaf1389eb refactoring 2023-05-09 15:13:46 +02:00
adrien bff9225bb0 refactoring 2023-05-09 09:21:16 +02:00
adrien e92a6cf5de refactoring 2023-05-09 09:16:18 +02:00
adrien 13539af376 css 2023-05-03 14:39:46 +02:00
adrien 7388f13e29 doctype 2023-05-02 21:12:38 +02:00
adrien 82b05a902d use bootstrap shadow 2023-05-02 20:36:25 +02:00
adrien e4a162728f smaller ghost glow 2023-05-02 20:35:32 +02:00
adrien e539a8e24d new classic ghost 2023-05-01 22:46:22 +02:00
adrien 06ac4650d5 fix S favicon 2023-04-30 02:09:02 +02:00
adrien f2c11ccbf2 mino colors 2023-04-30 02:04:21 +02:00
adrien fdbe8c5a73 S color 2023-04-30 01:52:45 +02:00
adrien ad0f57db33 S color 2023-04-30 01:51:45 +02:00
adrien 6fc00d50ab highscore 2023-04-29 22:14:38 +02:00
adrien 8f96053440 fix restart livel 2023-04-29 20:55:37 +02:00
adrien 77d68ece87 change combo name 2023-04-29 20:43:13 +02:00
adrien 9cf55ed479 drop animation more transparent 2023-04-29 18:23:49 +02:00
adrien 58915a7d69 dynamic favicon! 2023-04-29 16:00:42 +02:00
adrien a0a7075b6e tune common style 2023-04-29 12:30:19 +02:00
adrien 889351dfa7 settingsForm 2023-04-27 02:51:49 +02:00
adrien 44276b8d6a no plural 2023-04-27 02:35:33 +02:00
adrien bac13f1eca oninput 2023-04-26 13:43:04 +02:00
adrien d7d32e38d1 hour 2023-04-26 08:43:36 +02:00
adrien de9c72f760 settings arrangement 2023-04-26 00:06:07 +02:00
adrien c774cc25fc start center 2023-04-25 22:56:03 +02:00
52 changed files with 2258 additions and 1177 deletions
+5
View File
@@ -0,0 +1,5 @@
# Quatuor
Falling blocks
![screenshot](https://git.malingrey.fr/adrien/quatuor/raw/branch/master/thumbnail.png)
-919
View File
@@ -1,919 +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 FACING = {
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.init()
}
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.locked? " locked" : "")) {
piece.minoesPosition[piece.facing]
.translate(piece.center)
.forEach(minoPosition => {
this.drawMino(minoPosition, className)
})
}
redraw() {
for (let y=0; y<this.rows; y++) {
for (let x=0; x<this.columns; x++) {
this.drawMino([x, y], "")
}
}
}
}
MinoesTable.prototype.init_center = [2, 2]
class NextQueue extends MinoesTable {
constructor() {
super("nextTable")
this.init()
}
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))
}
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, 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, clearClassName="") {
let success = this.canMove(translation, rotation)
if (success) {
scheduler.clearTimeout(lockDown)
matrix.drawPiece(this, clearClassName)
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 (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
return true
}
})
}
get ghost() {
return new this.constructor(Array.from(this.center), this.facing, "ghost " + this.className)
}
}
// 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 {}
T.prototype.minoesPosition = [
[[-1, 0], [0, 0], [1, 0], [0, -1]],
[[0, -1], [0, 0], [1, 0], [0, 1]],
[[-1, 0], [0, 0], [1, 0], [0, 1]],
[[0, -1], [0, 0], [0, 1], [-1, 0]],
]
T.prototype.tSlots = [
[[-1, -1], [ 1, -1], [ 1, 1], [-1, 1]],
[[ 1, -1], [ 1, 1], [-1, 1], [-1, -1]],
[[ 1, 1], [-1, 1], [-1, -1], [ 1, -1]],
[[-1, 1], [-1, -1], [ 1, -1], [ 1, 1]],
]
class Z extends Tetromino {}
Z.prototype.minoesPosition = [
[[-1, -1], [0, -1], [0, 0], [ 1, 0]],
[[ 1, -1], [1, 0], [0, 0], [ 0, 1]],
[[-1, 0], [0, 0], [0, 1], [ 1, 1]],
[[ 0, -1], [-1, 0], [0, 0], [-1, 1]]
]
class Settings {
constructor() {
this.form = settingsForm
this.load()
this.modal = new bootstrap.Modal('#settingsModal')
settingsModal.addEventListener('shown.bs.modal', () => {
resumeButton.focus()
})
this.init()
}
load() {
for (let input of this.form.getElementsByTagName("input")) {
if (localStorage[input.name]) input.value = localStorage[input.name]
}
if (localStorage["stylesheet"]) stylesheetSelect.value = localStorage["stylesheet"]
document.selectedStyleSheetSet=stylesheetSelect.value
}
save() {
for (let input of this.form.getElementsByTagName("input")) {
localStorage[input.name] = input.value
}
localStorage["stylesheet"] = stylesheetSelect.value
}
init() {
this.form.onsubmit = newGame
levelInput.name = "startLevel"
levelInput.disabled = false
titleHeader.innerHTML = "QUATRIS"
resumeButton.innerHTML = "Jouer"
}
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.modal = new bootstrap.Modal('#statsModal')
this.load()
this.init()
}
load() {
this.highScore = Number(localStorage["highScore"]) || 0
}
init() {
this.score = 0
this.goal = 0
this.combo = 0
this.b2b = 0
this.startTime = new Date()
this.lockDelay = 0
this.totalClearedLines = 0
this.nbQuatris = 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 time(time) {
this.startTime = new Date() - time
ticktack()
}
get time() {
return new Date() - this.startTime
}
lockDown(nbClearedLines, tSpin) {
this.totalClearedLines += nbClearedLines
if (nbClearedLines == 4) this.nbQuatris++
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 > this.maxCombo) this.maxCombo = 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 > this.maxB2B) this.maxB2B = 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++
}
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)
statsModalNbQuatris.innerText = this.nbQuatris
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", {
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 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.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.facing = FACING.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)
if (action == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod/20)
else 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.facing]
.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.facing]
.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 nbClearedLines = 0
for (let y=0; y<matrix.rows; y++) {
let row = matrix.blocks[y]
if (row.filter(lockedMino => lockedMino).length == matrix.columns) {
nbClearedLines++
matrix.blocks.splice(y, 1)
matrix.blocks.unshift(Array(matrix.columns))
matrix.table.rows[y].classList.add("cleared-line-animation")
}
}
matrix.redraw()
stats.lockDown(nbClearedLines, tSpin)
generate()
} else {
gameOver() // lock out
}
}
function gameOver() {
matrix.piece.locked = false
matrix.drawPiece()
document.onkeydown = null
onblur = null
scheduler.clearInterval(ticktack)
playing = false
stats.show()
}
function restart() {
stats.modal.hide()
holdQueue.init()
stats.init()
matrix.init()
matrix.redraw()
nextQueue.init()
settings.init()
pauseSettings()
}
window.onbeforeunload = function(event) {
stats.save()
settings.save()
if (playing) return false;
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
+276
View 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);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+84 -52
View File
@@ -8,84 +8,116 @@
border: 4px solid; border: 4px solid;
padding: 0; padding: 0;
opacity: 100%; opacity: 100%;
border-radius: 2px; border-radius: 1px;
box-shadow: 2px 2px 4px #000a;
} }
.I.mino { .I.mino {
--background-color: #009FdA; --background-color : #00d6fb;
--frontier-color: #43e7fd; --frontier-color : #43e7fd;
--light-color: #afeff9; --light-color : #afeff9;
border-top-color: #7cf2fd; border-top-color : #7cf2fd;
border-left-color: #2ed5e5; border-left-color : #2ed5e5;
border-right-color: #00b8ca; border-right-color : #01b8ca;
border-bottom-color: #00a4b0; border-bottom-color: #00a4b0;
} }
.J.mino { .J.mino {
--background-color: #0065Bd; --background-color : #2E00FB;
--frontier-color: #7054fb; --frontier-color : #7054fb;
--light-color: #b8b4ff; --light-color : #b8b4ff;
border-top-color: #4985fd; border-top-color : #4985fd;
border-left-color: #2f36ea; border-left-color : #2f36ea;
border-right-color: #0006ca; border-right-color : #0006ca;
border-bottom-color: #00009d; border-bottom-color: #00009d;
} }
.L.mino { .L.mino {
--background-color: #FF7900; --background-color : #FF7900;
--frontier-color: #fe9551; --frontier-color : #fe9551;
--light-color: #fdd0b7; --light-color : #fdd0b7;
border-top-color: #fd9f6b; border-top-color : #fd9f6b;
border-left-color: #e76d28; border-left-color : #e76d28;
border-right-color: #e74f00; border-right-color : #e74f00;
border-bottom-color: #c54800; border-bottom-color: #c54800;
} }
.O.mino { .O.mino {
--background-color: #FeCB00; --background-color : #FeCB00;
--frontier-color: #fce15c; --frontier-color : #fce15c;
--light-color: #ffedac;; --light-color : #ffedac;;
border-top-color: #ffe364; border-top-color : #ffe364;
border-left-color: #e7ba23; border-left-color : #e7ba23;
border-right-color: #e3a707; border-right-color : #e3a707;
border-bottom-color: #ca9501; 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 { .S.mino {
--background-color: #69Be28; --background-color : #67EE12;
--frontier-color: #93f85a; --frontier-color : #93f85a;
--light-color: #93f85a; --light-color : #C8FBA8;
border-top-color: #a4fc6d; border-top-color : #a4fc6d;
border-left-color: #5ee82b; border-left-color : #5ee82b;
border-right-color: #35db00; border-right-color : #35db00;
border-bottom-color: #1cbc02; 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 { .Z.mino {
--background-color: #ed2939; --background-color : #ed2939;
--frontier-color: #fe6483; --frontier-color : #fe6483;
--light-color: #ffb8c5; --light-color : #ffb8c5;
border-top-color: #fd718d; border-top-color : #fd718d;
border-left-color: #e62250; border-left-color : #e62250;
border-right-color: #e20332; border-right-color : #e20332;
border-bottom-color: #ad1936; border-bottom-color: #ad1936;
} }
.locked.mino { .ghost.mino {
filter: saturate(50%) brightness(200%); margin: 1px;
opacity: 20%;
filter: brightness(180%) saturate(60%) blur(1px);
} }
.ghost.mino { .moving.mino {
opacity: 20%; filter: saturate(80%) brightness(150%);
filter: brightness(200%); }
.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;
} }
+62 -24
View File
@@ -1,5 +1,8 @@
:root { :root {
--cell-side: 24px; --cell-side: 24px;
--rX: -15deg;
--rY: 0;
--tZ: 0;
} }
body { body {
@@ -7,18 +10,44 @@ body {
} }
@supports (backdrop-filter: blur()) { @supports (backdrop-filter: blur()) {
.modal {
backdrop-filter: blur(2px);
}
.modal-content { .modal-content {
background-color: rgba(33, 37, 41, 30%); background-color: #2125294d;
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
} }
} }
.card { .card {
background-color: rgb(37, 41, 45); background-color: #25292d;
box-shadow: 5px 5px 20px #202020; }
#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;
}
#timeCell {
min-width: 10ch;
} }
.minoes-table { .minoes-table {
--piece-column: 0;
--piece-row : 0;
table-layout: fixed; table-layout: fixed;
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
@@ -30,18 +59,11 @@ body {
} }
@keyframes hard-dropped-table-animation { @keyframes hard-dropped-table-animation {
from {
transform: translateY(0);
}
25% { 25% {
transform: translateY(5px); transform: translateY(2px);
}
to {
transform: translateY(0);
} }
} }
.hard-dropped-table-animation {
#matrixTable.hard-dropped-table-animation {
animation: hard-dropped-table-animation .2s; animation: hard-dropped-table-animation .2s;
} }
@@ -50,7 +72,10 @@ tr.buffer-zone td:not(.mino) {
} }
tr.matrix 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 { td {
@@ -59,9 +84,9 @@ td {
height: var(--cell-side); height: var(--cell-side);
} }
@keyframes hard-drop-animation { @keyframes trail-animation {
from { from {
background-color: rgb(206, 255, 255, 40%); background-color: #ceffff40;
filter: saturate(50%) brightness(300%); filter: saturate(50%) brightness(300%);
} }
to { to {
@@ -69,13 +94,13 @@ td {
} }
} }
td.hard-drop-animation { td.trail-animation {
animation: hard-drop-animation ease-out .3s; animation: trail-animation ease-out .3s;
} }
@keyframes cleared-line-animation { @keyframes cleared-line-animation {
from { from {
background-color: rgb(206, 255, 255, 40%); background-color: #ceffff66;
filter: saturate(50%) brightness(300%); filter: saturate(50%) brightness(300%);
box-shadow: -200px 0 5px white, 200px 0 5px white; box-shadow: -200px 0 5px white, 200px 0 5px white;
} }
@@ -84,17 +109,30 @@ td.hard-drop-animation {
} }
} }
tr.cleared-line-animation{ tr.cleared-line-animation {
animation: cleared-line-animation ease-out .3s; 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; position: absolute;
top: 20%; top: 5%;
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
color: rgba(255, 255, 255, 0.8); color: #fffc;
text-shadow: 1px 1px rgba(0, 0, 0, 0.8); text-shadow: 1px 1px #000c;
font-size: 3vmin; font-size: 3vmin;
text-align: center; text-align: center;
} }
@@ -202,4 +240,4 @@ span {
animation: game-over-animation; animation: game-over-animation;
animation-timing-function: (0.4, 0, 0.6, 1); animation-timing-function: (0.4, 0, 0.6, 1);
animation-duration: 2s; animation-duration: 2s;
} }
+92 -13
View File
@@ -1,20 +1,99 @@
.mino { body {
background-image: radial-gradient( background-image: url("electro/bg.jpg");
farthest-corner at 4px 6px, background-size: cover;
rgba(204, 238, 247, 0.3) 0%, }
rgba(106, 197, 220, 0.3) 100%
); body[data-bs-theme="dark"] {
border: 1px solid rgba(127, 229, 255, 0.7); --bs-body-bg: #2125296b;
border-radius: 0.3vmin; }
.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 { .ghost.mino {
background: rgba(255, 255, 255, 0.2); background: rgba(242, 255, 255, 10%);
border: 1px solid rgba(255, 255, 255, 0.4); border : 2px solid rgba(255, 255, 255, 0.3);
border-radius: 0.3vmin; 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 { .locked.mino {
background: rgba(242, 255, 255, 0.5); animation: locked-animation;
border-color: rgba(242, 255, 255, 0.7); animation-duration: 0.2s;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

+124
View File
@@ -0,0 +1,124 @@
.card {
background-color: #363941;
}
.minoes-table {
display: flex;
flex-direction: column;
filter:
drop-shadow(-2px 0 0 white)
drop-shadow(2px 0 0 white)
drop-shadow(0 -2px 0 white)
drop-shadow(0 2px 0 white)
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: 5%;
box-shadow: none;
}
.moving.mino {
filter: saturate(80%) brightness(110%);
}
.locking.mino {
--background-color: white;
--box-shadow-color: #DDD;
}
.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;
}
}
@keyframes trail-animation {
from {
background-color: #ceffff10;
filter: saturate(50%) brightness(110%);
}
to {
background-color: transparent;
}
}
+70
View File
@@ -0,0 +1,70 @@
body {
background-image: url("new-wave/bg.png");
background-size: cover;
}
body[data-bs-theme="dark"] {
--bs-body-bg: #2125296b;
}
.btn-dark {
--bs-btn-bg: #2125296b;
}
.card {
background: #25292d66;
}
#matrixCard {
background-image: radial-gradient(#2226, #25292d66);
}
.minoes-table {
background: transparent;
}
.mino {
background: var(--color);
border: 3px solid var(--border);
box-shadow: 0 0 8px var(--border);
}
.I {
--color: #00eaf888;
--border: #00eaf5;
}
.J {
--color: #00a9f788;
--border: #00a9f7;
}
.L {
--color: #f9b60088;
--border: #f9b600;
}
.O {
--color: #e3e04988;
--border: #e3e049;
}
.S {
--color: #7bd59e88;
--border: #7bd59e;
}
.T {
--color: #d136e288;
--border: #d136e2;
}
.Z {
--color: #E67D8688;
--border: #E67D86;
}
.ghost {
--color: #fff4;
--border: #fff5;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

+155
View 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;
}
-45
View File
@@ -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);
}
+49 -4
View File
@@ -7,10 +7,13 @@
src: url("retro/Early GameBoy.ttf"); src: url("retro/Early GameBoy.ttf");
} }
body {
}
#screenRow { #screenRow {
background-image: url("retro/bg.png"); background-image: url("retro/bg.png");
background-size: 10px; background-size: 10px;
padding: 40px; padding: 40px 20px;
border: 3px inset black; border: 3px inset black;
border-radius: 10px; border-radius: 10px;
} }
@@ -28,6 +31,7 @@
border-image-repeat: repeat; border-image-repeat: repeat;
border-image-outset: 12px; border-image-outset: 12px;
box-shadow: unset; box-shadow: unset;
width: 100%;
} }
.card-header { .card-header {
@@ -44,8 +48,11 @@
#statsTable, #statsTable,
.card, .card,
.card-header,
#messagesSpan { #messagesSpan {
font-family: "Early GameBoy", monospace; font-family: "Early GameBoy", monospace;
font-smooth: never;
-webkit-font-smoothing: none;
color: #254806; color: #254806;
text-shadow: -1px -1px 3px rgba(0, 0, 0, 40%), 1px 1px 1px rgba(0, 0, 0, 40%); text-shadow: -1px -1px 3px rgba(0, 0, 0, 40%), 1px 1px 1px rgba(0, 0, 0, 40%);
} }
@@ -55,12 +62,22 @@
letter-spacing: -.1em; letter-spacing: -.1em;
} }
#statsTable tr {
display: flex;
flex-flow: column;
}
#statsTable th, #statsTable th,
#statsTable td { #statsTable td {
border: 0; border: 0;
padding: 0 .2rem; padding: 0 .2rem;
} }
#statsTable td {
width: auto;
text-align: end;
}
#messagesSpan { #messagesSpan {
text-shadow: -2px -2px #808302, -2px 2px #808302, 2px -2px #808302, 2px 2px #808302; text-shadow: -2px -2px #808302, -2px 2px #808302, 2px -2px #808302, 2px 2px #808302;
} }
@@ -102,10 +119,38 @@ td {
background-image: url("retro/Z-mino.png") background-image: url("retro/Z-mino.png")
} }
.locked.mino { @keyframes blinker {
filter: saturate(60%) brightness(200%); 35% {
opacity: 0;
}
} }
.ghost.mino { .locking.mino {
animation: blinker 0.08s step-start infinite;
}
.ghost.mino,
.disabled.mino {
opacity: 50%; 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;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

+123
View File
@@ -0,0 +1,123 @@
body {
background-image: url("new-wave/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%;
}
}

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+105 -119
View File
@@ -1,21 +1,36 @@
<!doctype html>
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>QUATRIS</title> <title>Quatuor</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<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@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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/classic.css"title="Classique"> <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/electro.css" title="Électro">
<link rel="alternate stylesheet" href="css/pop.css" title="Pop"> <link rel="alternate stylesheet" href="css/new-wave.css" title="New Wave">
<link rel="alternate stylesheet" href="css/retro.css" title="Rétro"> <link rel="alternate stylesheet" href="css/synthwave.css" title="Synthwave">
<link rel="apple-touch-icon" sizes="180x180" href="favicons/apple-touch-icon.png"> <link rel="alternate stylesheet" href="css/retro.css" title="Rétro">
<link rel="icon" type="image/png" sizes="32x32" href="favicons/favicon-32x32.png"> <link rel="alternate stylesheet" href="css/opera.css" title="Opéra">
<link rel="icon" type="image/png" sizes="16x16" href="favicons/favicon-16x16.png"> <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"> <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> </head>
<body data-bs-theme="dark"> <body data-bs-theme="dark">
@@ -24,97 +39,55 @@
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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>
<div class="modal-body"> <div class="modal-body">
<form name="settingsForm" class="needs-validation" novalidate> <form name="settingsForm" class="needs-validation" novalidate>
<fieldset id="keyBindFielset" class="row g-2 mb-3 align-items-center text-center"> <fieldset id="keyBindFielset" class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Commandes</legend>
<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>
<label for="moveLeftInput" title="Gauche" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center"> <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>
<i class="bi bi-arrow-left"></i> <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> <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>
<div class="col-sm-4"> <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>
<input name="moveLeft" id="moveLeftInput" type="text" class="form-control text-center" value="" onclick="changeKey(this)" placeholder="Touche ?" required> <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> <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>
<div class="col-sm-4"> <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>
<input name="moveRight" id="moveRightInput" type="text" class="form-control text-center" value="→" onclick="changeKey(this)" placeholder="Touche ?" required> <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> <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>
<label for="moveRightInput" title="Droite" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center"> <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>
<i class="bi bi-arrow-right"></i> <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> <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>
<label for="rotateCounterclockwiseInput" title="Rotation anti-horaire" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center"> <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>
<i class="bi bi-arrow-counterclockwise"></i> <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> <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>
<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> </fieldset>
<fieldset id="autorepearFieldset" class="row g-2 mb-3 align-items-center text-center"> <fieldset id="autorepeatFieldset" class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Répétition automatique</legend>
<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>
<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-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="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>
<div class="input-group"> <label for="dasInput" class="col-2 col-form-label"><abbr title="Delayed AutoShift : délai initial avant répétition">DAS</abbr></label>
<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> </fieldset>
<fieldset class="row g-2 mb-3 align-items-center text-center"> <fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Interface</legend>
<legend class="text-start">Feuille de style</legend> <label for="stylesheetSelect" class="col-2 col-form-label">Thème</label>
<label for="stylesheetSelect" class="col-sm-2 col-form-label text-center">Style</label> <div class="col-4"><select name="stylesheet" id="stylesheetSelect" class="form-select" oninput="selectedStyleSheet.href = this.value">
<div class="col-sm-4"> <option value="css/classic.css" selected>Classique</option>
<select name="stylesheet" id="stylesheetSelect" class="form-select" onclick="document.selectedStyleSheetSet=this.value"> <option value="css/minimal.css">Minimal</option>
<option selected>Classique</option> <option value="css/new-wave.css">New Wave</option>
<option>Électro</option> <option value="css/synthwave.css">Synthwave</option>
<option>Pop</option> <option value="css/electro.css">Électro</option>
<option>Rétro</option> <option value="css/retro.css">Rétro</option>
</select> <option value="css/opera.css">Opéra</option>
</div> <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>
<fieldset class="row g-2 mb-3 align-items-center text-center"> <fieldset class="row g-2 mb-3 align-items-center text-center"><legend class="text-start">Partie</legend>
<legend class="text-start">Partie</legend> <label for="levelInput" class="col-2 col-form-label text-center">Niveau</label>
<label for="levelInput" class="col-sm-2 col-form-label text-center">Niveau</label> <div class="col-4">
<div class="col-sm-4">
<input name="startLevel" id="levelInput" type="number" class="form-control text-center" value="1" min="1" max="15"> <input name="startLevel" id="levelInput" type="number" class="form-control text-center" value="1" min="1" max="15">
</div> </div>
<div class="col-sm-4"> <div class="col-4">
<button id="resumeButton" type="submit" class="btn btn-primary w-100" autofocus>Jouer</button> <button id="resumeButton" type="submit" class="btn btn-primary w-100" autofocus>Jouer</button>
</div> </div>
</fieldset> </fieldset>
@@ -124,14 +97,14 @@
</div> </div>
</div> </div>
<div class="container-fluid d-flex h-100 justify-content-center d-flex align-items-center"> <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 id="screenRow" class="row row-cols-auto align-items-start gap-2">
<div class="col d-flex flex-column align-items-end"> <div class="col d-flex flex-column align-items-end">
<div class="card mb-4"> <div class="card shadow mb-4 w-100">
<div class="card-header text-center"><strong>HOLD</strong></div> <div class="card-header fw-bold text-uppercase text-center">Hold</div>
<div class="card-body p-0"> <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> <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>
@@ -140,21 +113,19 @@
</table> </table>
</div> </div>
</div> </div>
<div class="card"> <div class="card shadow">
<table id="statsTable" class="table mb-0"> <table id="statsTable" class="table mb-0">
<tr><th>Score</th> <td id="scoreCell">0</td> </tr> <tr class="card-header fw-bold text-uppercase"><th>Score</th><td id="scoreCell">0</td> </tr>
<tr><th>Meilleur</th> <td id="highScoreCell"> <tr><th>Meilleur<br/>score</th><td id="highScoreCell"><script>document.write(Number(localStorage["highScore"]) || 0)</script></td></tr>
<script>document.write(Number(localStorage["highScore"]) || 0)</script> <tr><th>Niveau</th> <td id="levelCell">0</td> </tr>
</td></tr> <tr><th>But</th> <td id="goalCell">0</td> </tr>
<tr><th>Niveau</th> <td id="levelCell">0</td> </tr> <tr><th>Temps</th> <td id="timeCell">00:00</td> </tr>
<tr><th>But</th> <td id="goalCell">0</td> </tr>
<tr><th>Temps</th> <td id="timeCell">00:00</td> </tr>
</table> </table>
</div> </div>
</div> </div>
<div class="col position-relative"> <div class="col position-relative">
<div id="matrixCard" class="card"> <div id="matrixCard" class="card shadow">
<table id="matrixTable" class="minoes-table" style="--buffer-zone-rows: 5"> <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>
<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>
@@ -183,12 +154,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> <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> </table>
</div> </div>
<span id="messagesSpan"></span> <span id="messagesSpan">
<div class="show-level-animation">Chargement...</div>
</span>
</div> </div>
<div class="col"> <div class="col">
<div class="card"> <div class="card shadow">
<div class="card-header text-center"><strong>NEXT</strong></div> <div class="card-header fw-bold text-uppercase text-center">Next</div>
<div class="card-body p-0"> <div class="card-body p-0">
<table id="nextTable" class="minoes-table caption-top"> <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> <tr class="buffer-zone"><td></td><td></td><td></td><td></td><td></td><td></td></tr>
@@ -225,11 +198,11 @@
</div> </div>
<div class="modal-body p-0"> <div class="modal-body p-0">
<table class="table mb-0"> <table class="table mb-0">
<tr><th>Score</th> <td id="statsModalScoreCell"></td> <th>Niveau</th> <td id="statsModalLevelCell"></td> </tr> <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>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 minutes</th> <td id="statsModaltotalClearedLinesPM"></td> </tr> <tr><th>Lignes</th> <td id="statsModaltotalClearedLines"></td><th>Lignes par minute</th> <td id="statsModaltotalClearedLinesPM"></td></tr>
<tr><th>Quatris</th> <td id="statsModalNbQuatris"></td> <th>Pirouettes</th> <td id="statsModalNbTSpin"></td> </tr> <tr><th>Quatuors</th> <td id="statsModalNbQuatuors"></td> <th>Plus long combo</th> <td id="statsModalMaxCombo"></td> </tr>
<tr><th>Plus grand enchaînement</th> <td id="statsModalMaxCombo"></td> <th>Plus grand bout à bout</th> <td id="statsModalMaxB2B"></td> </tr> <tr><th>Pirouettes</th> <td id="statsModalNbTSpin"></td> <th>Plus long bout en bout</th><td id="statsModalMaxB2B"></td> </tr>
</table> </table>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -239,14 +212,27 @@
</div> </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="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 src="js/game_logic.js" language="Javascript" type="text/javascript"></script>
<script> <script src="js/interface.js" language="Javascript" type="text/javascript"></script>
<script src="js/app.js" language="Javascript" type="text/javascript"></script>
</script> <script>navigator?.serviceWorker.register('js/service-worker.js')</script>
</body> </body>
</html> </html>
+314
View 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
View 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
View 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()
}
+1 -1
View File
@@ -16,7 +16,7 @@ Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
const OFFLINE_VERSION = 1; const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline"; const CACHE_NAME = "offline";
// Customize this with a different URL if needed. // Customize this with a different URL if needed.
const OFFLINE_URL = "index.html"; const OFFLINE_URL = "../index.html";
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.