css animation
This commit is contained in:
parent
958ba01f6b
commit
c3a3e45c18
680
app.js
680
app.js
@ -1,47 +1,25 @@
|
|||||||
// Customize Array to be use as coordinates
|
|
||||||
Object.defineProperty(Array.prototype, "x", {
|
|
||||||
get: function () { return this[0] },
|
|
||||||
set: function (x) { this[0] = x}
|
|
||||||
})
|
|
||||||
Object.defineProperty(Array.prototype, "y", {
|
|
||||||
get: function () { return this[1] },
|
|
||||||
set: function (y) { this[1] = y}
|
|
||||||
})
|
|
||||||
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
|
|
||||||
Array.prototype.mul = function(k) { return this.map(x => k * x) }
|
|
||||||
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
|
|
||||||
Array.prototype.rotate = function(spin) { return [-spin*this.y, spin*this.x] }
|
|
||||||
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const NEXT_PIECES = 6
|
const NEXT_PIECES = 6
|
||||||
const HOLD = {
|
const MATRIX_INVISIBLE_ROWS = 4
|
||||||
PIECE_POSITION: [2, 3]
|
const START_POSITION = {
|
||||||
}
|
HOLD: [2, 3],
|
||||||
const MATRIX = {
|
MATRIX: [4, 3],
|
||||||
INVISIBLE_ROWS: 4,
|
NEXT: Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+3])
|
||||||
PIECE_POSITION: [4, 3]
|
|
||||||
}
|
|
||||||
const NEXT= {
|
|
||||||
PIECE_POSITION: Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+3])
|
|
||||||
}
|
|
||||||
const THEME = {
|
|
||||||
PIECE_POSITION: [1, 1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLASSNAME = {
|
const CLASSNAME = {
|
||||||
EMPTY_CELL: "",
|
EMPTY_CELL: "",
|
||||||
MINO: "mino",
|
MINO: "mino",
|
||||||
LOCKED: "locked",
|
LOCKED: "locked",
|
||||||
TRAIL: "mino trail",
|
TRAIL: "mino trail",
|
||||||
GHOST: "ghost",
|
GHOST: "ghost",
|
||||||
CLEARED_LINE: "cleared-line"
|
CLEARED_LINE: "cleared-line",
|
||||||
|
MESSAGE_SPAN_FADE_OUT: "messageSpan-fade-out"
|
||||||
}
|
}
|
||||||
const DELAY = {
|
const DELAY = {
|
||||||
LOCK: 500,
|
LOCK: 500,
|
||||||
FALL: 1000,
|
FALL: 1000,
|
||||||
AUTOREPEAT: 300,
|
ANIMATION: 200,
|
||||||
AUTOREPEAT_PERIOD: 10,
|
|
||||||
ANIMATION: 100,
|
|
||||||
MESSAGE: 700
|
MESSAGE: 700
|
||||||
}
|
}
|
||||||
const MOVEMENT = {
|
const MOVEMENT = {
|
||||||
@ -51,7 +29,7 @@ const MOVEMENT = {
|
|||||||
}
|
}
|
||||||
const SPIN = {
|
const SPIN = {
|
||||||
CW: 1, // ClockWise
|
CW: 1, // ClockWise
|
||||||
CCW: -1 // CounterClockWise
|
CCW: -1 // Counterstats.ClockWise
|
||||||
}
|
}
|
||||||
const T_SPIN = {
|
const T_SPIN = {
|
||||||
NONE: "",
|
NONE: "",
|
||||||
@ -77,27 +55,30 @@ const SCORES = [
|
|||||||
{linesClearedName: "TRIPLE", "": 500, "T-SPIN": 1600},
|
{linesClearedName: "TRIPLE", "": 500, "T-SPIN": 1600},
|
||||||
{linesClearedName: "TETRIS", "": 800},
|
{linesClearedName: "TETRIS", "": 800},
|
||||||
]
|
]
|
||||||
const REPEATABLE_ACTIONS = [moveLeft, moveRight, softDrop]
|
|
||||||
const STATE = {
|
const STATE = {
|
||||||
WAITING: "WAITING",
|
WAITING: "WAITING",
|
||||||
PLAYING: "PLAYING",
|
PLAYING: "PLAYING",
|
||||||
PAUSED: "PAUSE",
|
PAUSED: "PAUSE",
|
||||||
GAME_OVER: "GAME OVER",
|
GAME_OVER: "GAME OVER",
|
||||||
}
|
}
|
||||||
const actionsDefaultKeys = {
|
|
||||||
moveLeft: "ArrowLeft",
|
|
||||||
moveRight: "ArrowRight",
|
|
||||||
softDrop: "ArrowDown",
|
|
||||||
hardDrop: " ",
|
|
||||||
rotateCW: "ArrowUp",
|
|
||||||
rotateCCW: "z",
|
|
||||||
hold: "c",
|
|
||||||
pause: "Escape",
|
|
||||||
}
|
|
||||||
const RETRIES = 3
|
const RETRIES = 3
|
||||||
const DEFAULT_THEME = "default"
|
|
||||||
|
|
||||||
var theme = null
|
|
||||||
|
// Customize Array to be use as coordinates
|
||||||
|
Object.defineProperty(Array.prototype, "x", {
|
||||||
|
get: function () { return this[0] },
|
||||||
|
set: function (x) { this[0] = x}
|
||||||
|
})
|
||||||
|
Object.defineProperty(Array.prototype, "y", {
|
||||||
|
get: function () { return this[1] },
|
||||||
|
set: function (y) { this[1] = y}
|
||||||
|
})
|
||||||
|
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
|
||||||
|
Array.prototype.mul = function(k) { return this.map(x => k * x) }
|
||||||
|
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
|
||||||
|
Array.prototype.rotate = function(spin) { return [-spin*this.y, spin*this.x] }
|
||||||
|
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
@ -278,16 +259,7 @@ class Matrix extends MinoesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
// grid
|
this.clearTable()
|
||||||
for (var y = 0; y < this.rows; y++) {
|
|
||||||
for (var x = 0; x < this.columns; x++) {
|
|
||||||
if (this.clearedLines.includes(y))
|
|
||||||
var className = CLASSNAME.CLEARED_LINE
|
|
||||||
else
|
|
||||||
var className = this.lockedMinoes[y][x] || CLASSNAME.EMPTY_CELL
|
|
||||||
this.drawMino(x, y, className)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ghost
|
// ghost
|
||||||
if (showGhostCheckbox.value && !this.piece.locked && state != STATE.GAME_OVER) {
|
if (showGhostCheckbox.value && !this.piece.locked && state != STATE.GAME_OVER) {
|
||||||
@ -299,12 +271,22 @@ class Matrix extends MinoesTable {
|
|||||||
// trail
|
// trail
|
||||||
if (this.trail.height) {
|
if (this.trail.height) {
|
||||||
this.trail.minoesPos.forEach(pos => {
|
this.trail.minoesPos.forEach(pos => {
|
||||||
for (var y = pos.y; y < pos.y + this.trail.height - 1; y++)
|
for (var y = pos.y; y < pos.y + this.trail.height; y++)
|
||||||
this.drawMino(pos.x, y, CLASSNAME.TRAIL)
|
this.drawMino(pos.x, y, CLASSNAME.TRAIL)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawPiece(this.piece)
|
this.drawPiece(this.piece)
|
||||||
|
|
||||||
|
// locked minoes
|
||||||
|
for (var y = 0; y < this.rows; y++) {
|
||||||
|
for (var x = 0; x < this.columns; x++) {
|
||||||
|
if (this.clearedLines.includes(y))
|
||||||
|
this.drawMino(x, y, CLASSNAME.CLEARED_LINE)
|
||||||
|
else if (this.lockedMinoes[y][x])
|
||||||
|
this.drawMino(x, y, this.lockedMinoes[y][x])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,7 +297,7 @@ class NextQueue extends MinoesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newGame() {
|
newGame() {
|
||||||
this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(NEXT.PIECE_POSITION[k]))
|
this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(START_POSITION.NEXT[k]))
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
@ -326,7 +308,7 @@ class NextQueue extends MinoesTable {
|
|||||||
|
|
||||||
|
|
||||||
class Stats {
|
class Stats {
|
||||||
constructor () {
|
constructor() {
|
||||||
this.scoreCell = document.getElementById("score")
|
this.scoreCell = document.getElementById("score")
|
||||||
this.highScoreCell = document.getElementById("highScore")
|
this.highScoreCell = document.getElementById("highScore")
|
||||||
this.timeCell = document.getElementById("time")
|
this.timeCell = document.getElementById("time")
|
||||||
@ -335,6 +317,11 @@ class Stats {
|
|||||||
this.clearedLinesCell = document.getElementById("clearedLines")
|
this.clearedLinesCell = document.getElementById("clearedLines")
|
||||||
this.highScore = Number(localStorage.getItem('highScore'))
|
this.highScore = Number(localStorage.getItem('highScore'))
|
||||||
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
||||||
|
this.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
newGame() {
|
newGame() {
|
||||||
@ -344,7 +331,7 @@ class Stats {
|
|||||||
this.clearedLines = 0
|
this.clearedLines = 0
|
||||||
this.clearedLinesCell.innerText = this.clearedLines
|
this.clearedLinesCell.innerText = this.clearedLines
|
||||||
this.time = 0
|
this.time = 0
|
||||||
this.timeCell.innerText = timeFormat(0)
|
this.timeCell.innerText = this.timeFormat.format(0)
|
||||||
this.combo = -1
|
this.combo = -1
|
||||||
this.lockDelay = DELAY.LOCK
|
this.lockDelay = DELAY.LOCK
|
||||||
this.fallPeriod = DELAY.FALL
|
this.fallPeriod = DELAY.FALL
|
||||||
@ -362,11 +349,12 @@ class Stats {
|
|||||||
this.highScore = score
|
this.highScore = score
|
||||||
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
||||||
}
|
}
|
||||||
|
document.title = `Webtris - Score : ${score}`
|
||||||
}
|
}
|
||||||
|
|
||||||
newLevel(level=null) {
|
newLevel(level=null) {
|
||||||
this.level = level || this.level + 1
|
this.level = level || this.level + 1
|
||||||
location.hash = "#level" + this.level
|
location.hash = this.level
|
||||||
this.levelCell.innerText = this.level
|
this.levelCell.innerText = this.level
|
||||||
printTempTexts(`NIVEAU<br/>${this.level}`)
|
printTempTexts(`NIVEAU<br/>${this.level}`)
|
||||||
this.goal += 5 * this.level
|
this.goal += 5 * this.level
|
||||||
@ -427,17 +415,99 @@ class Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
constructor() {
|
||||||
|
this.keyBind = {}
|
||||||
|
for (let button of settingsSection.getElementsByTagName("button")) {
|
||||||
|
let keyName = localStorage.getItem(button.id)
|
||||||
|
if (keyName) {
|
||||||
|
button.innerHTML = keyName
|
||||||
|
this.keyBind[keyName == "Space"? " ": keyName] = playerAction[button.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let autorepeatDelay = localStorage.getItem("autorepeatDelay")
|
||||||
|
if (autorepeatDelay) {
|
||||||
|
autorepeatDelayRange.value = autorepeatDelay
|
||||||
|
autorepeatDelayRange.oninput()
|
||||||
|
}
|
||||||
|
let autorepeatPeriod = localStorage.getItem("autorepeatPeriod")
|
||||||
|
if (autorepeatPeriod) {
|
||||||
|
autorepeatPeriodRange.value = autorepeatPeriod
|
||||||
|
autorepeatPeriodRange.oninput()
|
||||||
|
}
|
||||||
|
|
||||||
|
let themeName = localStorage.getItem("themeName")
|
||||||
|
if (themeName) themeSelect.value = themeName
|
||||||
|
let showGhost = localStorage.getItem("showGhost")
|
||||||
|
if (showGhost) showGhostCheckbox.checked = showGhost == "true"
|
||||||
|
|
||||||
|
let startLevel = localStorage.getItem("startLevel")
|
||||||
|
if (startLevel) startLevelInput.value = startLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme = () => new Promise((resolve, reject) => {
|
||||||
|
var link = document.createElement('link')
|
||||||
|
link.id = 'theme'
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.type = 'text/css'
|
||||||
|
link.href = `themes/${themeSelect.value}/style.css`
|
||||||
|
link.media = 'all'
|
||||||
|
link.onload = resolve
|
||||||
|
document.head.appendChild(link)
|
||||||
|
})
|
||||||
|
|
||||||
|
save() {
|
||||||
|
for (let button of settingsSection.getElementsByTagName("button")) {
|
||||||
|
localStorage.setItem(button.id, button.innerHTML)
|
||||||
|
}
|
||||||
|
localStorage.setItem("autorepeatDelay", autorepeatDelayRange.value)
|
||||||
|
localStorage.setItem("autorepeatPeriod", autorepeatPeriodRange.value)
|
||||||
|
localStorage.setItem("themeName", themeSelect.value)
|
||||||
|
localStorage.setItem("showGhost", showGhostCheckbox.checked)
|
||||||
|
localStorage.setItem("startLevel", startLevelInput.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitKey(button) {
|
||||||
|
document.onkeydown = null
|
||||||
|
document.onkeyup = null
|
||||||
|
button.previousKey = button.innerHTML
|
||||||
|
button.innerHTML = "Touche ?"
|
||||||
|
button.onkeyup = function(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
button.innerHTML = (event.key == " ") ? "Space" : event.key
|
||||||
|
settings.keyBind[event.key] = playerAction[button.id]
|
||||||
|
button.onkeyup = null
|
||||||
|
button.onblur = null
|
||||||
|
document.onkeydown = onkeydown
|
||||||
|
document.onkeyup = onkeyup
|
||||||
|
}
|
||||||
|
button.onblur = function(event) {
|
||||||
|
button.innerHTML = button.previousKey
|
||||||
|
button.onkeyup = null
|
||||||
|
button.onblur = null
|
||||||
|
document.onkeydown = onkeydown
|
||||||
|
document.onkeyup = onkeyup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
|
|
||||||
|
// Game logic
|
||||||
|
state = STATE.WAITING
|
||||||
|
|
||||||
function newGame(startLevel) {
|
function newGame(startLevel) {
|
||||||
startButton.blur()
|
startButton.blur()
|
||||||
|
|
||||||
|
settings.save()
|
||||||
|
|
||||||
holdQueue.newGame()
|
holdQueue.newGame()
|
||||||
matrix.newGame()
|
matrix.newGame()
|
||||||
nextQueue.newGame()
|
nextQueue.newGame()
|
||||||
stats.newGame()
|
stats.newGame()
|
||||||
|
|
||||||
localStorage.setItem("startLevel", startLevel)
|
|
||||||
|
|
||||||
startSection.style.display = "none"
|
startSection.style.display = "none"
|
||||||
gameSection.style.display = "block"
|
gameSection.style.display = "block"
|
||||||
settingsSection.style.display = "none"
|
settingsSection.style.display = "none"
|
||||||
@ -446,9 +516,9 @@ function newGame(startLevel) {
|
|||||||
state = STATE.PLAYING
|
state = STATE.PLAYING
|
||||||
pressedKeys = new Set()
|
pressedKeys = new Set()
|
||||||
actionsToRepeat = []
|
actionsToRepeat = []
|
||||||
addEventListener("keydown", keyDownHandler, false)
|
scheduler.setInterval(stats.clock, 1000)
|
||||||
addEventListener("keyup", keyUpHandler, false)
|
document.onkeydown = onkeydown
|
||||||
scheduler.setInterval(clock, 1000)
|
document.onkeyup = onkeyup
|
||||||
newLevel(startLevel)
|
newLevel(startLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,10 +531,10 @@ function generationPhase(held_piece=null) {
|
|||||||
if (!held_piece) {
|
if (!held_piece) {
|
||||||
matrix.piece = nextQueue.pieces.shift()
|
matrix.piece = nextQueue.pieces.shift()
|
||||||
nextQueue.pieces.push(new Tetromino())
|
nextQueue.pieces.push(new Tetromino())
|
||||||
nextQueue.pieces.forEach((piece, i) => piece.pos = NEXT.PIECE_POSITION[i])
|
nextQueue.pieces.forEach((piece, i) => piece.pos = START_POSITION.NEXT[i])
|
||||||
}
|
}
|
||||||
nextQueue.draw()
|
nextQueue.draw()
|
||||||
matrix.piece.pos = MATRIX.PIECE_POSITION
|
matrix.piece.pos = START_POSITION.MATRIX
|
||||||
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos))){
|
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos))){
|
||||||
scheduler.clearInterval(lockPhase)
|
scheduler.clearInterval(lockPhase)
|
||||||
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
||||||
@ -473,62 +543,19 @@ function generationPhase(held_piece=null) {
|
|||||||
gameOver()
|
gameOver()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lockPhase() {
|
||||||
|
move(MOVEMENT.DOWN)
|
||||||
|
}
|
||||||
|
|
||||||
function fallingPhase() {
|
function fallingPhase() {
|
||||||
scheduler.clearTimeout(lockDown)
|
scheduler.clearTimeout(lockDown)
|
||||||
matrix.piece.locked = false
|
matrix.piece.locked = false
|
||||||
matrix.draw()
|
matrix.draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockPhase() {
|
|
||||||
move(MOVEMENT.DOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
function move(movement, testMinoesPos=matrix.piece.minoesPos, hardDrop=false) {
|
|
||||||
const testPos = matrix.piece.pos.add(movement)
|
|
||||||
if (matrix.spaceToMove(testMinoesPos.translate(testPos))) {
|
|
||||||
matrix.piece.pos = testPos
|
|
||||||
matrix.piece.minoesPos = testMinoesPos
|
|
||||||
matrix.piece.rotatedLast = false
|
|
||||||
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN))))
|
|
||||||
fallingPhase()
|
|
||||||
else if (!hardDrop) {
|
|
||||||
matrix.piece.locked = true
|
|
||||||
scheduler.clearTimeout(lockDown)
|
|
||||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
||||||
}
|
|
||||||
if (!hardDrop)
|
|
||||||
matrix.draw()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
if (movement == MOVEMENT.DOWN) {
|
|
||||||
matrix.piece.locked = true
|
|
||||||
if (!scheduler.timeoutTasks.has(lockDown))
|
|
||||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
||||||
matrix.draw()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotate(spin) {
|
|
||||||
const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin))
|
|
||||||
rotationPoint = 1
|
|
||||||
for (const movement of matrix.piece.srs[spin][matrix.piece.orientation]) {
|
|
||||||
if (move(movement, test_minoes_pos)) {
|
|
||||||
matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4
|
|
||||||
matrix.piece.rotatedLast = true
|
|
||||||
if (rotationPoint == 5)
|
|
||||||
matrix.piece.rotationPoint5Used = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
rotationPoint++
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function lockDown(){
|
function lockDown(){
|
||||||
scheduler.clearInterval(lockPhase)
|
scheduler.clearInterval(lockPhase)
|
||||||
if (matrix.piece.minoesAbsPos.every(pos => pos.y < MATRIX.INVISIBLE_ROWS)) {
|
if (matrix.piece.minoesAbsPos.every(pos => pos.y < MATRIX_INVISIBLE_ROWS)) {
|
||||||
matrix.piece.locked = false
|
matrix.piece.locked = false
|
||||||
matrix.draw()
|
matrix.draw()
|
||||||
gameOver()
|
gameOver()
|
||||||
@ -577,12 +604,12 @@ function clearLinesCleared() {
|
|||||||
|
|
||||||
function gameOver() {
|
function gameOver() {
|
||||||
state = STATE.GAME_OVER
|
state = STATE.GAME_OVER
|
||||||
|
document.onkeydown = null
|
||||||
|
document.onkeyup = null
|
||||||
messageSpan.innerHTML = "GAME<br/>OVER"
|
messageSpan.innerHTML = "GAME<br/>OVER"
|
||||||
scheduler.clearInterval(lockPhase)
|
scheduler.clearInterval(lockPhase)
|
||||||
scheduler.clearTimeout(lockDown)
|
scheduler.clearTimeout(lockDown)
|
||||||
scheduler.clearInterval(clock)
|
scheduler.clearInterval(stats.clock)
|
||||||
removeEventListener("keydown", keyDownHandler, false)
|
|
||||||
removeEventListener("keyup", keyUpHandler, false)
|
|
||||||
|
|
||||||
var info = `GAME OVER\nScore : ${stats.score.toLocaleString()}`
|
var info = `GAME OVER\nScore : ${stats.score.toLocaleString()}`
|
||||||
if (stats.score == stats.highScore) {
|
if (stats.score == stats.highScore) {
|
||||||
@ -627,20 +654,154 @@ function gameOver() {
|
|||||||
request.open('POST', 'inleaderboard.php')
|
request.open('POST', 'inleaderboard.php')
|
||||||
request.send(fd)
|
request.send(fd)
|
||||||
|
|
||||||
location.hash = "#game-over"
|
location.hash = "game-over"
|
||||||
|
|
||||||
startSection.style.display = "block"
|
startSection.style.display = "block"
|
||||||
gameSection.style.display = "block"
|
|
||||||
settingsSection.style.display = "none"
|
|
||||||
footer.style.display = "block"
|
footer.style.display = "block"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function move(movement, testMinoesPos=matrix.piece.minoesPos, hardDrop=false) {
|
||||||
|
const testPos = matrix.piece.pos.add(movement)
|
||||||
|
if (matrix.spaceToMove(testMinoesPos.translate(testPos))) {
|
||||||
|
matrix.piece.pos = testPos
|
||||||
|
matrix.piece.minoesPos = testMinoesPos
|
||||||
|
matrix.piece.rotatedLast = false
|
||||||
|
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN))))
|
||||||
|
fallingPhase()
|
||||||
|
else if (!hardDrop) {
|
||||||
|
matrix.piece.locked = true
|
||||||
|
scheduler.clearTimeout(lockDown)
|
||||||
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||||
|
}
|
||||||
|
if (!hardDrop)
|
||||||
|
matrix.draw()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
if (movement == MOVEMENT.DOWN) {
|
||||||
|
matrix.piece.locked = true
|
||||||
|
if (!scheduler.timeoutTasks.has(lockDown))
|
||||||
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||||
|
matrix.draw()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(spin) {
|
||||||
|
const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin))
|
||||||
|
let rotationPoint = 1
|
||||||
|
for (const movement of matrix.piece.srs[spin][matrix.piece.orientation]) {
|
||||||
|
if (move(movement, test_minoes_pos)) {
|
||||||
|
matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4
|
||||||
|
matrix.piece.rotatedLast = true
|
||||||
|
if (rotationPoint == 5)
|
||||||
|
matrix.piece.rotationPoint5Used = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
rotationPoint++
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause() {
|
||||||
|
state = STATE.PAUSED
|
||||||
|
location.hash = "pause"
|
||||||
|
stats.startTime = performance.now() - stats.startTime
|
||||||
|
actionsToRepeat = []
|
||||||
|
scheduler.clearInterval(lockPhase)
|
||||||
|
scheduler.clearTimeout(lockDown)
|
||||||
|
scheduler.clearTimeout(autorepeat)
|
||||||
|
scheduler.clearInterval(stats.clock)
|
||||||
|
scheduler.clearInterval(delTempTexts)
|
||||||
|
holdQueue.draw()
|
||||||
|
matrix.draw()
|
||||||
|
nextQueue.draw()
|
||||||
|
header.style.display = "block"
|
||||||
|
gameSection.style.display = "none"
|
||||||
|
settingsSection.style.display = "block"
|
||||||
|
}
|
||||||
|
|
||||||
|
function resume() {
|
||||||
|
settings.save()
|
||||||
|
settingsSection.style.display = "none"
|
||||||
|
gameSection.style.display = "block"
|
||||||
|
location.hash = stats.level
|
||||||
|
state = STATE.PLAYING
|
||||||
|
stats.startTime = performance.now() - stats.startTime
|
||||||
|
messageSpan.innerHTML = ""
|
||||||
|
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
||||||
|
if (matrix.piece.locked)
|
||||||
|
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||||
|
scheduler.setInterval(stats.clock, 1000)
|
||||||
|
holdQueue.draw()
|
||||||
|
matrix.draw()
|
||||||
|
nextQueue.draw()
|
||||||
|
if (tempTexts.length)
|
||||||
|
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
playerAction = {
|
||||||
|
moveLeft: function () {
|
||||||
|
move(MOVEMENT.LEFT)
|
||||||
|
},
|
||||||
|
|
||||||
|
moveRight: function () {
|
||||||
|
move(MOVEMENT.RIGHT)
|
||||||
|
},
|
||||||
|
|
||||||
|
softDrop: function () {
|
||||||
|
if (move(MOVEMENT.DOWN))
|
||||||
|
stats.score++
|
||||||
|
},
|
||||||
|
|
||||||
|
hardDrop: function () {
|
||||||
|
scheduler.clearInterval(lockPhase)
|
||||||
|
scheduler.clearTimeout(lockDown)
|
||||||
|
matrix.trail.minoesPos = Array.from(matrix.piece.minoesAbsPos)
|
||||||
|
for (matrix.trail.height = 0; move(MOVEMENT.DOWN, matrix.piece.minoesPos, true); matrix.trail.height++) {}
|
||||||
|
stats.score += 2 * matrix.trail.height
|
||||||
|
matrix.draw()
|
||||||
|
lockDown()
|
||||||
|
scheduler.setTimeout(() => {matrix.trail.height = 0; matrix.draw()}, DELAY.ANIMATION)
|
||||||
|
},
|
||||||
|
|
||||||
|
rotateCW: function () {
|
||||||
|
rotate(SPIN.CW)
|
||||||
|
},
|
||||||
|
|
||||||
|
rotateCCW: function () {
|
||||||
|
rotate(SPIN.CCW)
|
||||||
|
},
|
||||||
|
|
||||||
|
hold: function () {
|
||||||
|
if (matrix.piece.holdEnabled) {
|
||||||
|
scheduler.clearInterval(move)
|
||||||
|
scheduler.clearInterval(lockDown)
|
||||||
|
var shape = matrix.piece.shape
|
||||||
|
matrix.piece = holdQueue.piece
|
||||||
|
holdQueue.piece = new Tetromino(START_POSITION.HOLD, shape)
|
||||||
|
holdQueue.piece.holdEnabled = false
|
||||||
|
holdQueue.draw()
|
||||||
|
generationPhase(matrix.piece)
|
||||||
|
matrix.piece.holdEnabled = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
pauseResume: function() {
|
||||||
|
if (state == STATE.PLAYING) {
|
||||||
|
pause()
|
||||||
|
} else {
|
||||||
|
resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function autorepeat() {
|
function autorepeat() {
|
||||||
if (actionsToRepeat.length) {
|
if (actionsToRepeat.length) {
|
||||||
actionsToRepeat[0]()
|
actionsToRepeat[0]()
|
||||||
if (scheduler.timeoutTasks.has(autorepeat)) {
|
if (scheduler.timeoutTasks.has(autorepeat)) {
|
||||||
scheduler.clearTimeout(autorepeat)
|
scheduler.clearTimeout(autorepeat)
|
||||||
scheduler.setInterval(autorepeat, autorepeatPeriod)
|
scheduler.setInterval(autorepeat, autorepeatPeriodRange.value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scheduler.clearTimeout(autorepeat)
|
scheduler.clearTimeout(autorepeat)
|
||||||
@ -648,31 +809,36 @@ function autorepeat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyDownHandler(e) {
|
// Handle player inputs
|
||||||
if (e.key in actions[state])
|
const REPEATABLE_ACTION = [playerAction.moveLeft, playerAction.moveRight, playerAction.softDrop]
|
||||||
e.preventDefault()
|
pressedKeys = new Set()
|
||||||
if (!pressedKeys.has(e.key)) {
|
actionsToRepeat = []
|
||||||
pressedKeys.add(e.key)
|
|
||||||
if (e.key in actions[state]) {
|
function onkeydown(event) {
|
||||||
action = actions[state][e.key]
|
if (event.key in settings.keyBind)
|
||||||
|
event.preventDefault()
|
||||||
|
if (!pressedKeys.has(event.key)) {
|
||||||
|
pressedKeys.add(event.key)
|
||||||
|
if (event.key in settings.keyBind) {
|
||||||
|
action = settings.keyBind[event.key]
|
||||||
action()
|
action()
|
||||||
if (REPEATABLE_ACTIONS.includes(action)) {
|
if (REPEATABLE_ACTION.includes(action)) {
|
||||||
actionsToRepeat.unshift(action)
|
actionsToRepeat.unshift(action)
|
||||||
scheduler.clearTimeout(autorepeat)
|
scheduler.clearTimeout(autorepeat)
|
||||||
scheduler.clearInterval(autorepeat)
|
scheduler.clearInterval(autorepeat)
|
||||||
if (action == softDrop)
|
if (action == softDrop)
|
||||||
scheduler.setInterval(autorepeat, stats.fallPeriod / 20)
|
scheduler.setInterval(autorepeat, stats.fallPeriod / 20)
|
||||||
else
|
else
|
||||||
scheduler.setTimeout(autorepeat, autorepeatDelay)
|
scheduler.setTimeout(autorepeat, autorepeatDelayRange.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyUpHandler(e) {
|
function onkeyup(event) {
|
||||||
pressedKeys.delete(e.key)
|
pressedKeys.delete(event.key)
|
||||||
if (e.key in actions[state]) {
|
if (event.key in settings.keyBind) {
|
||||||
action = actions[state][e.key]
|
action = settings.keyBind[event.key]
|
||||||
if (actionsToRepeat.includes(action)) {
|
if (actionsToRepeat.includes(action)) {
|
||||||
actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1)
|
actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1)
|
||||||
if (!actionsToRepeat.length) {
|
if (!actionsToRepeat.length) {
|
||||||
@ -683,102 +849,18 @@ function keyUpHandler(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions
|
// Text display
|
||||||
function moveLeft() {
|
tempTexts = []
|
||||||
move(MOVEMENT.LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveRight() {
|
|
||||||
move(MOVEMENT.RIGHT)
|
|
||||||
}
|
|
||||||
|
|
||||||
function softDrop() {
|
|
||||||
if (move(MOVEMENT.DOWN))
|
|
||||||
stats.score++
|
|
||||||
}
|
|
||||||
|
|
||||||
function hardDrop() {
|
|
||||||
scheduler.clearInterval(lockPhase)
|
|
||||||
scheduler.clearTimeout(lockDown)
|
|
||||||
matrix.trail.minoesPos = Array.from(matrix.piece.minoesAbsPos)
|
|
||||||
for (matrix.trail.height = 0; move(MOVEMENT.DOWN, matrix.piece.minoesPos, true); matrix.trail.height++) {}
|
|
||||||
stats.score += 2 * matrix.trail.height
|
|
||||||
matrix.draw()
|
|
||||||
lockDown()
|
|
||||||
scheduler.setTimeout(clearTrail, DELAY.ANIMATION)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearTrail() {
|
|
||||||
matrix.trail.height = 0
|
|
||||||
matrix.draw()
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateCW() {
|
|
||||||
rotate(SPIN.CW)
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateCCW() {
|
|
||||||
rotate(SPIN.CCW)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hold() {
|
|
||||||
if (matrix.piece.holdEnabled) {
|
|
||||||
scheduler.clearInterval(move)
|
|
||||||
scheduler.clearInterval(lockDown)
|
|
||||||
var shape = matrix.piece.shape
|
|
||||||
matrix.piece = holdQueue.piece
|
|
||||||
holdQueue.piece = new Tetromino(HOLD.PIECE_POSITION, shape)
|
|
||||||
holdQueue.piece.holdEnabled = false
|
|
||||||
holdQueue.draw()
|
|
||||||
generationPhase(matrix.piece)
|
|
||||||
matrix.piece.holdEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause() {
|
|
||||||
state = STATE.PAUSED
|
|
||||||
location.hash = "#pause"
|
|
||||||
stats.startTime = performance.now() - stats.startTime
|
|
||||||
actionsToRepeat = []
|
|
||||||
scheduler.clearInterval(lockPhase)
|
|
||||||
scheduler.clearTimeout(lockDown)
|
|
||||||
scheduler.clearTimeout(autorepeat)
|
|
||||||
scheduler.clearInterval(clock)
|
|
||||||
scheduler.clearInterval(delTempTexts)
|
|
||||||
holdQueue.draw()
|
|
||||||
matrix.draw()
|
|
||||||
nextQueue.draw()
|
|
||||||
gameSection.style.display = "none"
|
|
||||||
settingsSection.style.display = "block"
|
|
||||||
}
|
|
||||||
|
|
||||||
function resume() {
|
|
||||||
applySettings()
|
|
||||||
settingsSection.style.display = "none"
|
|
||||||
gameSection.style.display = "block"
|
|
||||||
location.hash = "#level" + stats.level
|
|
||||||
state = STATE.PLAYING
|
|
||||||
stats.startTime = performance.now() - stats.startTime
|
|
||||||
messageSpan.innerHTML = ""
|
|
||||||
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
|
||||||
if (matrix.piece.locked)
|
|
||||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
|
||||||
scheduler.setInterval(clock, 1000)
|
|
||||||
holdQueue.draw()
|
|
||||||
matrix.draw()
|
|
||||||
nextQueue.draw()
|
|
||||||
if (tempTexts.length)
|
|
||||||
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
function printTempTexts(text) {
|
function printTempTexts(text) {
|
||||||
tempTexts.push(text)
|
tempTexts.push(text)
|
||||||
messageSpan.innerHTML = tempTexts[0]
|
messageSpan.innerHTML = tempTexts[0]
|
||||||
if (!scheduler.intervalTasks.has(delTempTexts))
|
if (!scheduler.intervalTasks.has(delTempTexts))
|
||||||
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
||||||
|
messageSpan.classList.add(CLASSNAME.MESSAGE_SPAN_FADE_OUT)
|
||||||
}
|
}
|
||||||
|
|
||||||
function delTempTexts(self) {
|
function delTempTexts(self) {
|
||||||
|
messageSpan.classList.remove(CLASSNAME.MESSAGE_SPAN_FADE_OUT)
|
||||||
if (tempTexts.length)
|
if (tempTexts.length)
|
||||||
tempTexts.shift()
|
tempTexts.shift()
|
||||||
if (tempTexts.length)
|
if (tempTexts.length)
|
||||||
@ -789,144 +871,28 @@ function delTempTexts(self) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clock(timestamp) {
|
function clock() {
|
||||||
stats.timeCell.innerText = timeFormat(1000 * ++stats.time)
|
timeCell.innerText = stats.timeFormat.format(1000 * ++stats.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeyName(action) {
|
|
||||||
return localStorage.getItem(action) || actionsDefaultKeys[action]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings functions
|
|
||||||
function applySettings() {
|
|
||||||
actions[STATE.PLAYING] = {}
|
|
||||||
actions[STATE.PLAYING][getKeyName("moveLeft")] = moveLeft
|
|
||||||
actions[STATE.PLAYING][getKeyName("moveRight")] = moveRight
|
|
||||||
actions[STATE.PLAYING][getKeyName("softDrop")] = softDrop
|
|
||||||
actions[STATE.PLAYING][getKeyName("hardDrop")] = hardDrop
|
|
||||||
actions[STATE.PLAYING][getKeyName("rotateCW")] = rotateCW
|
|
||||||
actions[STATE.PLAYING][getKeyName("rotateCCW")] = rotateCCW
|
|
||||||
actions[STATE.PLAYING][getKeyName("hold")] = hold
|
|
||||||
actions[STATE.PLAYING][getKeyName("pause")] = pause
|
|
||||||
actions[STATE.PAUSED] = {}
|
|
||||||
actions[STATE.PAUSED][getKeyName("pause")] = resume
|
|
||||||
actions[STATE.GAME_OVER] = {}
|
|
||||||
|
|
||||||
autorepeatDelay = localStorage.getItem("autorepeatDelay") || DELAY.AUTOREPEAT
|
|
||||||
autorepeatPeriod = localStorage.getItem("autorepeatPeriod") || DELAY.AUTOREPEAT_PERIOD
|
|
||||||
|
|
||||||
themeName = localStorage.getItem("themeName") || DEFAULT_THEME
|
|
||||||
loadTheme()
|
|
||||||
|
|
||||||
showGhost = localStorage.getItem("showGhost")
|
|
||||||
if (showGhost)
|
|
||||||
showGhost = (showGhost == "true")
|
|
||||||
else
|
|
||||||
showGhost = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceSpace(key) {
|
|
||||||
return (key == " ") ? "Space" : key
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings() {
|
|
||||||
if (state == STATE.PLAYING)
|
|
||||||
pause()
|
|
||||||
|
|
||||||
moveLeftSetKeyButton.innerHTML = replaceSpace(getKeyName("moveLeft"))
|
|
||||||
moveRightSetKeyButton.innerHTML = replaceSpace(getKeyName("moveRight"))
|
|
||||||
softDropSetKeyButton.innerHTML = replaceSpace(getKeyName("softDrop"))
|
|
||||||
hardDropSetKeyButton.innerHTML = replaceSpace(getKeyName("hardDrop"))
|
|
||||||
rotateCWSetKeyButton.innerHTML = replaceSpace(getKeyName("rotateCW"))
|
|
||||||
rotateCCWSetKeyButton.innerHTML = replaceSpace(getKeyName("rotateCCW"))
|
|
||||||
holdSetKeyButton.innerHTML = replaceSpace(getKeyName("hold"))
|
|
||||||
pauseSetKeyButton.innerHTML = replaceSpace(getKeyName("pause"))
|
|
||||||
|
|
||||||
autorepeatDelayRange.value = autorepeatDelay
|
|
||||||
autorepeatDelayRangeLabel.innerText = `Délai initial : ${autorepeatDelay}ms`
|
|
||||||
autorepeatPeriodRange.value = autorepeatPeriod
|
|
||||||
autorepeatPeriodRangeLabel.innerText = `Période : ${autorepeatPeriod}ms`
|
|
||||||
|
|
||||||
themeSelect.value=themeName;
|
|
||||||
|
|
||||||
showGhostCheckbox.checked = showGhost
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitKey(button, action) {
|
|
||||||
button.innerHTML = "Touche ?"
|
|
||||||
selectedButton = button
|
|
||||||
selectedAction = action
|
|
||||||
button.blur()
|
|
||||||
addEventListener("keyup", changeKey, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeKey(e) {
|
|
||||||
if (selectedButton) {
|
|
||||||
localStorage.setItem(selectedAction, e.key)
|
|
||||||
selectedButton.innerHTML = (e.key == " ") ? "Space" : e.key
|
|
||||||
selectedButton = null
|
|
||||||
}
|
|
||||||
removeEventListener("keyup", changeKey, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function autorepeatDelayChanged() {
|
|
||||||
localStorage.setItem("autorepeatDelay", autorepeatDelayRange.value)
|
|
||||||
document.getElementById("autorepeatDelayRangeLabel").innerText = `Délai initial : ${autorepeatDelayRange.value}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function autorepeatPeriodChanged() {
|
|
||||||
localStorage.setItem("autorepeatPeriod", autorepeatPeriodRange.value)
|
|
||||||
document.getElementById("autorepeatPeriodRangeLabel").innerText = `Période : ${autorepeatPeriodRange.value}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function themeChanged() {
|
|
||||||
themeName = document.getElementById("themeSelect").value
|
|
||||||
localStorage.setItem("themeName", themeName)
|
|
||||||
loadTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTheme() {
|
|
||||||
var link = document.createElement('link')
|
|
||||||
link.id = "theme";
|
|
||||||
link.rel = 'stylesheet'
|
|
||||||
link.type = 'text/css'
|
|
||||||
link.href = 'themes/' + themeName+ '/style.css'
|
|
||||||
link.media = 'all'
|
|
||||||
if (theme) document.head.removeChild(theme)
|
|
||||||
document.head.appendChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showGhostChanged() {
|
|
||||||
showGhost = (showGhostCheckbox.checked == true)
|
|
||||||
localStorage.setItem("showGhost", showGhost)
|
|
||||||
}
|
|
||||||
|
|
||||||
// global variables
|
|
||||||
timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
timeZone: "UTC"
|
|
||||||
}).format
|
|
||||||
state = STATE.WAITING
|
|
||||||
tempTexts = []
|
|
||||||
actions = {}
|
|
||||||
selectedButton = null
|
|
||||||
selectedAction = ""
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
location.hash = ""
|
|
||||||
|
|
||||||
startLevelInput.value = localStorage.getItem("startLevel") || 1
|
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
window.onload = async function() {
|
||||||
scheduler = new Scheduler()
|
scheduler = new Scheduler()
|
||||||
holdQueue = new HoldQueue()
|
holdQueue = new HoldQueue()
|
||||||
stats = new Stats()
|
stats = new Stats()
|
||||||
matrix = new Matrix()
|
matrix = new Matrix()
|
||||||
nextQueue = new NextQueue()
|
nextQueue = new NextQueue()
|
||||||
|
settings = new Settings()
|
||||||
applySettings()
|
await settings.applyTheme()
|
||||||
loadSettings()
|
let startLevel = parseInt(location.hash.slice(1))
|
||||||
|
body.style.display = "block"
|
||||||
startButton.disabled = false
|
if (1 <= startLevel && startLevel <= 15) {
|
||||||
startButton.focus()
|
newGame(startLevel)
|
||||||
|
} else {
|
||||||
|
location.hash = ""
|
||||||
|
startButton.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.onblur = pause
|
129
index.php
129
index.php
@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
$actions = [
|
$actions = [
|
||||||
"moveLeft" => ["label"=>"Gauche", "defaultKey"=>"ArrowLeft"],
|
"moveLeft" => ["label"=>"Gauche", "defaultKey"=>"ArrowLeft"],
|
||||||
"moveRight" => ["label"=>"Droite", "defaultKey"=>"ArrowRight"],
|
"moveRight" => ["label"=>"Droite", "defaultKey"=>"ArrowRight"],
|
||||||
"softDrop" => ["label"=>"Chute lente", "defaultKey"=>"ArrowDown"],
|
"softDrop" => ["label"=>"Chute lente", "defaultKey"=>"ArrowDown"],
|
||||||
"hardDrop" => ["label"=>"Chute rapide", "defaultKey"=>"Space"],
|
"hardDrop" => ["label"=>"Chute rapide", "defaultKey"=>"Space"],
|
||||||
"rotateCW" => ["label"=>"Rotation horaire", "defaultKey"=>"ArrowUp"],
|
"rotateCW" => ["label"=>"Rotation horaire", "defaultKey"=>"ArrowUp"],
|
||||||
"rotateCCW" => ["label"=>"Rotation anti-horaire", "defaultKey"=>"z"],
|
"rotateCCW" => ["label"=>"Rotation anti-horaire", "defaultKey"=>"z"],
|
||||||
"hold" => ["label"=>"Garde", "defaultKey"=>"c"],
|
"hold" => ["label"=>"Garde", "defaultKey"=>"c"],
|
||||||
"pause" => ["label"=>"Pause/Reprise", "defaultKey"=>"Escape"]
|
"pauseResume" => ["label"=>"Pause/Reprise", "defaultKey"=>"Escape"]
|
||||||
];
|
];
|
||||||
|
|
||||||
function echoTable($id, $invisibleRows, $visibleRows, $columns) {
|
function echoTable($id, $invisibleRows, $visibleRows, $columns) {
|
||||||
@ -38,17 +38,19 @@
|
|||||||
<script type="text/javascript" src="app.js"></script>
|
<script type="text/javascript" src="app.js"></script>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body id="body" style="display:none">
|
||||||
<section id="settingsSection">
|
<header id="header">
|
||||||
<h1>WEBTRIS</h1>
|
<h1>WEBTRIS</h1>
|
||||||
|
</header>
|
||||||
|
<section id="settingsSection">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Clavier</legend>
|
<legend>Clavier</legend>
|
||||||
<div>
|
<div>
|
||||||
<?php
|
<?php
|
||||||
foreach($actions as $action=>$parameters) {
|
foreach($actions as $action=>$parameters) {
|
||||||
?>
|
?>
|
||||||
<label for='<?=$action?>SetKeyButton'><?=$parameters["label"]?></label>
|
<label for='<?=$action?>'><?=$parameters["label"]?></label>
|
||||||
<button id='<?=$action?>SetKeyButton' type='button' onclick="waitKey(this, '<?=$action?>')"><?=$parameters["defaultKey"]?></button>
|
<button id='<?=$action?>' type='button' onclick="settings.waitKey(this)"><?=$parameters["defaultKey"]?></button>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@ -58,16 +60,16 @@
|
|||||||
<legend>Répétition automatique</legend>
|
<legend>Répétition automatique</legend>
|
||||||
<div>
|
<div>
|
||||||
<label id="autorepeatDelayRangeLabel" for="autorepeatDelayRange">Délai initial : 300ms</label>
|
<label id="autorepeatDelayRangeLabel" for="autorepeatDelayRange">Délai initial : 300ms</label>
|
||||||
<input id="autorepeatDelayRange" type="range" oninput="autorepeatDelayChanged()" min="100" max="500" step="10" />
|
<input id="autorepeatDelayRange" type="range" oninput="this.previousElementSibling.innerText = `Délai initial : ${this.value}ms`" value="300" min="100" max="500" step="10"/>
|
||||||
<label id="autorepeatPeriodRangeLabel" for="autorepeatPeriodRange">Période : 10ms</label>
|
<label id="autorepeatPeriodRangeLabel" for="autorepeatPeriodRange">Période : 10ms</label>
|
||||||
<input id="autorepeatPeriodRange" type="range" id="autorepeatPeriodRange" oninput="autorepeatPeriodChanged()" min="2" max="50" step="2" />
|
<input id="autorepeatPeriodRange" type="range" id="autorepeatPeriodRange" oninput="this.previousElementSibling.innerText = `Période : ${autorepeatPeriodRange.value}ms`" value="10" min="2" max="50" step="2"/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Thème</legend>
|
<legend>Style</legend>
|
||||||
<div>
|
<div>
|
||||||
<div></div>
|
<label for="themeSelect">Thème</label>
|
||||||
<select id="themeSelect" onchange="themeChanged()">
|
<select id="themeSelect" oninput="settings.applyTheme()" value="default">
|
||||||
<?php
|
<?php
|
||||||
foreach(array_slice(scandir("themes"), 2) as $theme) {
|
foreach(array_slice(scandir("themes"), 2) as $theme) {
|
||||||
if (is_dir(pathinfo($theme)['dirname']))
|
if (is_dir(pathinfo($theme)['dirname']))
|
||||||
@ -75,52 +77,52 @@
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</select>
|
</select>
|
||||||
<table id="themePreviewTable" class=minoes-table>
|
<label for="showGhostCheckbox">Afficher le fantôme</label>
|
||||||
<tr>
|
<input id="showGhostCheckbox" type="checkbox" checked/>
|
||||||
<th class="mino I"></th>
|
<table id="themePreviewTable" class="minoes-table">
|
||||||
<th></th>
|
<tbody>
|
||||||
<th class="mino J"></th>
|
<tr>
|
||||||
<th class="mino J"></th>
|
<th class="mino I"></th>
|
||||||
<th class="mino J"></th>
|
<th></th>
|
||||||
<th></th>
|
<th class="mino J"></th>
|
||||||
<th class="mino S"></th>
|
<th class="mino J"></th>
|
||||||
<th></th>
|
<th class="mino J"></th>
|
||||||
</tr>
|
<th></th>
|
||||||
<tr>
|
<th class="mino S"></th>
|
||||||
<th class="mino I"></th>
|
<th></th>
|
||||||
<th class="mino O"></th>
|
</tr>
|
||||||
<th class="mino O"></th>
|
<tr>
|
||||||
<th></th>
|
<th class="mino I"></th>
|
||||||
<th class="mino J"></th>
|
<th class="mino O"></th>
|
||||||
<th class="mino Z"></th>
|
<th class="mino O"></th>
|
||||||
<th class="mino S"></th>
|
<th></th>
|
||||||
<th class="mino S"></th>
|
<th class="mino J"></th>
|
||||||
</tr>
|
<th class="mino Z"></th>
|
||||||
<tr>
|
<th class="mino S"></th>
|
||||||
<th class="mino I"></th>
|
<th class="mino S"></th>
|
||||||
<th class="mino O"></th>
|
</tr>
|
||||||
<th class="mino O"></th>
|
<tr>
|
||||||
<th class="mino L"></th>
|
<th class="mino I"></th>
|
||||||
<th class="mino Z"></th>
|
<th class="mino O"></th>
|
||||||
<th class="mino Z"></th>
|
<th class="mino O"></th>
|
||||||
<th class="mino T"></th>
|
<th class="mino L"></th>
|
||||||
<th class="mino S"></th>
|
<th class="mino Z"></th>
|
||||||
</tr>
|
<th class="mino Z"></th>
|
||||||
<tr>
|
<th class="mino T"></th>
|
||||||
<th class="mino I"></th>
|
<th class="mino S"></th>
|
||||||
<th class="mino L"></th>
|
</tr>
|
||||||
<th class="mino L"></th>
|
<tr>
|
||||||
<th class="mino L"></th>
|
<th class="mino I"></th>
|
||||||
<th class="mino Z"></th>
|
<th class="mino L"></th>
|
||||||
<th class="mino T"></th>
|
<th class="mino L"></th>
|
||||||
<th class="mino T"></th>
|
<th class="mino L"></th>
|
||||||
<th class="mino T"></th>
|
<th class="mino Z"></th>
|
||||||
</tr>
|
<th class="mino T"></th>
|
||||||
|
<th class="mino T"></th>
|
||||||
|
<th class="mino T"></th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div id="showGhostDiv">
|
|
||||||
<input id="showGhostCheckbox" type="checkbox" checked onchange="showGhostChanged()"/>
|
|
||||||
<label for="showGhostCheckbox">Afficher le fantôme</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
@ -153,7 +155,7 @@
|
|||||||
<label for="startLevel">Niveau</label>
|
<label for="startLevel">Niveau</label>
|
||||||
<input type="number" id="startLevelInput" min="1" max="15" step="1" value="1">
|
<input type="number" id="startLevelInput" min="1" max="15" step="1" value="1">
|
||||||
<div></div>
|
<div></div>
|
||||||
<button id="startButton" type="button" onclick="newGame(Number(startLevelInput.value))" disabled>JOUER</button>
|
<button id="startButton" type="button" onclick="newGame(Number(startLevelInput.value))">JOUER</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
@ -164,7 +166,6 @@
|
|||||||
<div id="credits">
|
<div id="credits">
|
||||||
Sources d'inspiration des thèmes :
|
Sources d'inspiration des thèmes :
|
||||||
<a href="https://github.com/kubowania/Tetris">Ania Kubow</a>
|
<a href="https://github.com/kubowania/Tetris">Ania Kubow</a>
|
||||||
<a href="https://manjaro.org/">Manjaro</a>
|
|
||||||
<a href="https://www.tetriseffect.game/">Tetris Effect</a>
|
<a href="https://www.tetriseffect.game/">Tetris Effect</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
const DEFAULT_THEME = "default"
|
const DEFAULT_THEME = "default"
|
||||||
|
|
||||||
function loadTheme() {
|
function applyTheme() {
|
||||||
var link = document.createElement('link')
|
var link = document.createElement('link')
|
||||||
link.id = "theme";
|
link.id = "theme";
|
||||||
link.rel = 'stylesheet'
|
link.rel = 'stylesheet'
|
||||||
link.type = 'text/css'
|
link.type = 'text/css'
|
||||||
link.href = 'themes/' + themeName + '/style.css'
|
link.href = `themes/'${themeName}/style.css`
|
||||||
link.media = 'all'
|
link.media = 'all'
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ legend, label {
|
|||||||
|
|
||||||
fieldset > div {
|
fieldset > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 2fr 3fr 2fr;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
grid-column-gap: 2em;
|
grid-column-gap: 2em;
|
||||||
grid-row-gap: 1em;
|
grid-row-gap: 1em;
|
||||||
justify-items: right;
|
justify-items: right;
|
||||||
@ -73,18 +73,9 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#themePreviewTable {
|
#themePreviewTable {
|
||||||
grid-column: 3 / 5;
|
grid-column: 1 / 5;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
margin: auto;
|
||||||
|
|
||||||
fieldset input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showGhostDiv {
|
|
||||||
grid-row: 2;
|
|
||||||
grid-column: 1/5;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#gameSection div {
|
#gameSection div {
|
||||||
@ -125,7 +116,7 @@ fieldset input[type="checkbox"] {
|
|||||||
.minoes-table {
|
.minoes-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
margin: auto;
|
margin: -6vmin 0 auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
@ -189,12 +180,9 @@ td {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer > * {
|
||||||
position: absolute;
|
margin: 1em auto;
|
||||||
left: 50%;
|
width: 100%;
|
||||||
bottom: 1em;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -208,6 +196,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#credits {
|
#credits {
|
||||||
|
width: 100%;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
gap: 0.8em;
|
gap: 0.8em;
|
||||||
}
|
}
|
||||||
@ -220,25 +209,23 @@ a:hover {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px solid white;
|
border-top: 1px solid white;
|
||||||
caption-side: top;
|
caption-side: top;
|
||||||
|
border-spacing: 1em 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#leaderboard caption {
|
#leaderboard caption {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#leaderboard tr,
|
||||||
#leaderboard td {
|
#leaderboard td {
|
||||||
font-family: 'Share Tech';
|
border: 0 !important;
|
||||||
text-align: center;
|
margin: auto 10em;
|
||||||
font-size: 2.5vmin;
|
|
||||||
color: white;
|
|
||||||
border: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#leaderboard td:first-child{
|
#leaderboard td:first-child {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#leaderboard td:last-child {
|
#leaderboard td:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ legend, #leaderboard caption {
|
|||||||
|
|
||||||
fieldset > div {
|
fieldset > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 2fr 3fr 2fr;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
grid-column-gap: 1em;
|
grid-column-gap: 1em;
|
||||||
grid-row-gap: 1em;
|
grid-row-gap: 1em;
|
||||||
justify-items: right;
|
justify-items: right;
|
||||||
@ -143,20 +143,10 @@ select:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#themePreviewTable {
|
#themePreviewTable {
|
||||||
grid-column: 3 / 5;
|
grid-column: 1 / 5;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showGhostDiv {
|
|
||||||
grid-row: 2;
|
|
||||||
grid-column: 1/5;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#gameSection div {
|
#gameSection div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 3vmin;
|
grid-gap: 3vmin;
|
||||||
@ -289,6 +279,8 @@ a {
|
|||||||
gap: 0.8em;
|
gap: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#leaderboard {
|
#leaderboard {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: auto;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: white;
|
color: white;
|
||||||
background: #222;
|
background: #222;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
margin: 1em;
|
margin: 1em auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@ -19,12 +20,9 @@ div {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsSection {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
@ -34,7 +32,7 @@ legend {
|
|||||||
|
|
||||||
fieldset > div {
|
fieldset > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 2fr 3fr 2fr;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
grid-column-gap: 2em;
|
grid-column-gap: 2em;
|
||||||
grid-row-gap: 1em;
|
grid-row-gap: 1em;
|
||||||
justify-items: right;
|
justify-items: right;
|
||||||
@ -50,18 +48,9 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#themePreviewTable {
|
#themePreviewTable {
|
||||||
grid-column: 3 / 5;
|
grid-column: 1 / 5;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
margin: auto;
|
||||||
|
|
||||||
fieldset input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showGhostDiv {
|
|
||||||
grid-row: 2;
|
|
||||||
grid-column: 1/5;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#gameSection div {
|
#gameSection div {
|
||||||
@ -92,6 +81,13 @@ fieldset input[type="checkbox"] {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
@keyframes message-fade-out {
|
||||||
|
from {opacity: 1;}
|
||||||
|
to {opacity: 0;}
|
||||||
|
}
|
||||||
|
.messageSpan-fade-out {
|
||||||
|
animation: message-fade-out 500ms 500ms;
|
||||||
|
}
|
||||||
|
|
||||||
#nextTable {
|
#nextTable {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
@ -102,7 +98,7 @@ fieldset input[type="checkbox"] {
|
|||||||
.minoes-table {
|
.minoes-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
margin: auto;
|
margin: -6vmin 0 auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
@ -160,16 +156,27 @@ td {
|
|||||||
border-color: #ffdada;
|
border-color: #ffdada;
|
||||||
}
|
}
|
||||||
|
|
||||||
.locked-mino, .cleared-line {
|
.locked-mino {
|
||||||
background: #AAA;
|
background: #AAA;
|
||||||
border: 1px solid #CCC;
|
border: 1px solid #CCC;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes mino-fade-out {
|
||||||
|
from {
|
||||||
|
background: rgba(170, 170, 170, 1);
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background: rgba(170, 170, 170, 0);
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cleared-line,
|
||||||
.trail {
|
.trail {
|
||||||
background: #333;
|
animation: mino-fade-out 200ms;
|
||||||
|
background: transparent;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
border-radius: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
@ -193,39 +200,41 @@ td {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 1em;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: lightblue;
|
color: lightblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer > * {
|
||||||
|
margin: 1em auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#credits {
|
#credits {
|
||||||
width: max-content;
|
width: 100%;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
gap: 0.8em;
|
gap: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#leaderboard {
|
#leaderboard {
|
||||||
min-width: 25%;
|
min-width: 25%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px solid white;
|
border-top: 1px solid white;
|
||||||
caption-side: top;
|
caption-side: top;
|
||||||
|
border-spacing: 1em 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#leaderboard caption {
|
#leaderboard caption {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#leaderboard tr,
|
||||||
#leaderboard td {
|
#leaderboard td {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
|
margin: auto 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#leaderboard td:first-child {
|
#leaderboard td:first-child {
|
||||||
@ -235,5 +244,3 @@ a {
|
|||||||
#leaderboard td:last-child {
|
#leaderboard td:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
BIN
thumbnail.png
Normal file
BIN
thumbnail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
Reference in New Issue
Block a user