Compare commits
71 Commits
2012c628ea
...
master
Author | SHA1 | Date | |
---|---|---|---|
c3a3e45c18 | |||
958ba01f6b | |||
ccd85b6bd8 | |||
a161826a37 | |||
5d7795b3da | |||
0f1093541e | |||
078120e4d1 | |||
1d1b2b24d2 | |||
3fccefc57c | |||
198ac07b84 | |||
5ef94fcf73 | |||
70cfd94bc3 | |||
a7a3c630e9 | |||
060662f706 | |||
c2050f224f | |||
7495206d88 | |||
bb33748a30 | |||
1dcd33b2e8 | |||
12080960f4 | |||
01cecac2a5 | |||
e39216f7bf | |||
2494b0795d | |||
2cd305fee0 | |||
55da5e6b28 | |||
a404f22801 | |||
fc95b7384b | |||
a9c364be55 | |||
ae39fda660 | |||
4fe9175641 | |||
125ba37f2e | |||
4171731dc7 | |||
f12ff135fa | |||
0f47785b43 | |||
5de5f03b3c | |||
bfe422bbdd | |||
7249f46c37 | |||
1577fb5dbf | |||
40911443ed | |||
001a0a05b4 | |||
703638425a | |||
2f07a096be | |||
ecbb8552cd | |||
826e71d0a0 | |||
7a8978a4f6 | |||
ecfb08fb4d | |||
02252885f3 | |||
15c8cdf9fd | |||
1d9e1d3442 | |||
5697684ac0 | |||
20ed771442 | |||
88837d8992 | |||
350186ab06 | |||
0c8b42f3ea | |||
affa958ab8 | |||
46c2782e4e | |||
d04fc53849 | |||
7c62e1a316 | |||
dd3f9f41ce | |||
88ab307384 | |||
8796d5b604 | |||
95829b0b91 | |||
2d41f0ecf2 | |||
209f48699d | |||
32b1799c6b | |||
29387428b6 | |||
eba036cffd | |||
e4dce4c2f4 | |||
b054401625 | |||
d2dbdf10bb | |||
ea5da16dac | |||
cef167a6ab |
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
db_connect.php
|
898
app.js
Normal file
@ -0,0 +1,898 @@
|
||||
// Constants
|
||||
const NEXT_PIECES = 6
|
||||
const MATRIX_INVISIBLE_ROWS = 4
|
||||
const START_POSITION = {
|
||||
HOLD: [2, 3],
|
||||
MATRIX: [4, 3],
|
||||
NEXT: Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+3])
|
||||
}
|
||||
|
||||
const CLASSNAME = {
|
||||
EMPTY_CELL: "",
|
||||
MINO: "mino",
|
||||
LOCKED: "locked",
|
||||
TRAIL: "mino trail",
|
||||
GHOST: "ghost",
|
||||
CLEARED_LINE: "cleared-line",
|
||||
MESSAGE_SPAN_FADE_OUT: "messageSpan-fade-out"
|
||||
}
|
||||
const DELAY = {
|
||||
LOCK: 500,
|
||||
FALL: 1000,
|
||||
ANIMATION: 200,
|
||||
MESSAGE: 700
|
||||
}
|
||||
const MOVEMENT = {
|
||||
LEFT: [-1, 0],
|
||||
RIGHT: [ 1, 0],
|
||||
DOWN: [ 0, 1]
|
||||
}
|
||||
const SPIN = {
|
||||
CW: 1, // ClockWise
|
||||
CCW: -1 // Counterstats.ClockWise
|
||||
}
|
||||
const T_SPIN = {
|
||||
NONE: "",
|
||||
MINI: "MINI T-SPIN",
|
||||
T_SPIN: "T-SPIN"
|
||||
}
|
||||
const T_SLOT = {
|
||||
A: 0,
|
||||
B: 1,
|
||||
C: 3,
|
||||
D: 2
|
||||
}
|
||||
const T_SLOT_POS = [
|
||||
[-1, -1],
|
||||
[1, -1],
|
||||
[1, 1],
|
||||
[-1, 1]
|
||||
]
|
||||
const SCORES = [
|
||||
{linesClearedName: "", "": 000, "MINI T-SPIN": 100, "T-SPIN": 400},
|
||||
{linesClearedName: "SINGLE", "": 100, "MINI T-SPIN": 200, "T-SPIN": 800},
|
||||
{linesClearedName: "DOUBLE", "": 300, "T-SPIN": 1200},
|
||||
{linesClearedName: "TRIPLE", "": 500, "T-SPIN": 1600},
|
||||
{linesClearedName: "TETRIS", "": 800},
|
||||
]
|
||||
const STATE = {
|
||||
WAITING: "WAITING",
|
||||
PLAYING: "PLAYING",
|
||||
PAUSED: "PAUSE",
|
||||
GAME_OVER: "GAME OVER",
|
||||
}
|
||||
const RETRIES = 3
|
||||
|
||||
|
||||
// Customize Array to be use as coordinates
|
||||
Object.defineProperty(Array.prototype, "x", {
|
||||
get: function () { return this[0] },
|
||||
set: function (x) { this[0] = x}
|
||||
})
|
||||
Object.defineProperty(Array.prototype, "y", {
|
||||
get: function () { return this[1] },
|
||||
set: function (y) { this[1] = y}
|
||||
})
|
||||
Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) }
|
||||
Array.prototype.mul = function(k) { return this.map(x => k * x) }
|
||||
Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) }
|
||||
Array.prototype.rotate = function(spin) { return [-spin*this.y, spin*this.x] }
|
||||
Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] }
|
||||
|
||||
|
||||
|
||||
// Classes
|
||||
class Scheduler {
|
||||
constructor() {
|
||||
this.intervalTasks = new Map()
|
||||
this.timeoutTasks = new Map()
|
||||
}
|
||||
|
||||
setInterval(func, delay, ...args) {
|
||||
this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
|
||||
}
|
||||
|
||||
setTimeout(func, delay, ...args) {
|
||||
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
|
||||
}
|
||||
|
||||
clearInterval(func) {
|
||||
if (this.intervalTasks.has(func)) {
|
||||
window.clearInterval(this.intervalTasks.get(func))
|
||||
this.intervalTasks.delete(func)
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(func) {
|
||||
if (this.timeoutTasks.has(func)) {
|
||||
window.clearTimeout(this.timeoutTasks.get(func))
|
||||
this.timeoutTasks.delete(func)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
randomBag = []
|
||||
class Tetromino {
|
||||
constructor(position=null, shape=null) {
|
||||
this.pos = position
|
||||
this.orientation = 0
|
||||
this.rotatedLast = false
|
||||
this.rotationPoint5Used = false
|
||||
this.holdEnabled = true
|
||||
this.locked = false
|
||||
this.srs = {} // Super Rotation System
|
||||
this.srs[SPIN.CW] = [
|
||||
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]],
|
||||
[[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]],
|
||||
[[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]],
|
||||
[[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]],
|
||||
]
|
||||
this.srs[SPIN.CCW] = [
|
||||
[[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]],
|
||||
[[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]],
|
||||
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]],
|
||||
[[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]],
|
||||
]
|
||||
if (shape)
|
||||
this.shape = shape
|
||||
else {
|
||||
if (!randomBag.length)
|
||||
randomBag = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
|
||||
this.shape = randomBag.pick()
|
||||
}
|
||||
switch(this.shape) {
|
||||
case 'I':
|
||||
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]]
|
||||
this.srs[SPIN.CW] = [
|
||||
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
|
||||
[[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]],
|
||||
[[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]],
|
||||
[[ 0, 1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]],
|
||||
]
|
||||
this.srs[SPIN.CCW] = [
|
||||
[[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]],
|
||||
[[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]],
|
||||
[[ 0, -1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]],
|
||||
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
|
||||
]
|
||||
break
|
||||
case 'J':
|
||||
this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]]
|
||||
break
|
||||
case 'L':
|
||||
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]]
|
||||
break
|
||||
case 'O':
|
||||
this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]]
|
||||
this.srs[SPIN.CW] = [[]]
|
||||
this.srs[SPIN.CCW] = [[]]
|
||||
break
|
||||
case 'S':
|
||||
this.minoesPos = [[-1, 0], [0, 0], [0, -1], [1, -1]]
|
||||
break
|
||||
case 'T':
|
||||
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]]
|
||||
break
|
||||
case 'Z':
|
||||
this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]]
|
||||
break
|
||||
}
|
||||
this.className = CLASSNAME.MINO + " " + this.shape
|
||||
}
|
||||
|
||||
get minoesAbsPos() {
|
||||
return this.minoesPos.translate(this.pos)
|
||||
}
|
||||
|
||||
get ghost() {
|
||||
var ghost = new Tetromino(Array.from(this.pos), this.shape)
|
||||
ghost.minoesPos = Array.from(this.minoesPos)
|
||||
ghost.className = CLASSNAME.GHOST
|
||||
return ghost
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MinoesTable {
|
||||
constructor(id) {
|
||||
this.table = document.getElementById(id)
|
||||
this.rows = this.table.rows.length
|
||||
this.columns = this.table.rows[0].childElementCount
|
||||
}
|
||||
|
||||
drawMino(x, y, className) {
|
||||
this.table.rows[y].cells[x].className = className
|
||||
}
|
||||
|
||||
drawPiece(piece=this.piece, className=piece.locked ? CLASSNAME.LOCKED + " "+ piece.className: piece.className) {
|
||||
piece.minoesAbsPos.forEach(pos => this.drawMino(...pos, className))
|
||||
}
|
||||
|
||||
clearTable() {
|
||||
for(var y = 0; y < this.rows; y++) {
|
||||
for (var x = 0; x < this.columns; x++) {
|
||||
this.drawMino(x, y, CLASSNAME.EMPTY_CELL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HoldQueue extends MinoesTable {
|
||||
constructor() {
|
||||
super("holdTable")
|
||||
}
|
||||
|
||||
newGame() {
|
||||
this.piece = null
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.clearTable()
|
||||
if (this.piece)
|
||||
this.drawPiece(this.piece)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Matrix extends MinoesTable {
|
||||
constructor() {
|
||||
super("matrixTable")
|
||||
}
|
||||
|
||||
newGame() {
|
||||
this.lockedMinoes = Array.from(Array(this.rows), row => Array(this.columns))
|
||||
this.piece = null
|
||||
this.clearedLines = []
|
||||
this.trail = {
|
||||
minoesPos: [],
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
|
||||
cellIsOccupied(x, y) {
|
||||
return 0 <= x && x < this.columns && y < this.rows ? Boolean(this.lockedMinoes[y][x]) : true
|
||||
}
|
||||
|
||||
spaceToMove(minoesAbsPos) {
|
||||
return !minoesAbsPos.some(pos => this.cellIsOccupied(...pos))
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.clearTable()
|
||||
|
||||
// ghost
|
||||
if (showGhostCheckbox.value && !this.piece.locked && state != STATE.GAME_OVER) {
|
||||
for (var ghost = this.piece.ghost; this.spaceToMove(ghost.minoesAbsPos); ghost.pos.y++) {}
|
||||
ghost.pos.y--
|
||||
this.drawPiece(ghost)
|
||||
}
|
||||
|
||||
// trail
|
||||
if (this.trail.height) {
|
||||
this.trail.minoesPos.forEach(pos => {
|
||||
for (var y = pos.y; y < pos.y + this.trail.height; y++)
|
||||
this.drawMino(pos.x, y, CLASSNAME.TRAIL)
|
||||
})
|
||||
}
|
||||
|
||||
this.drawPiece(this.piece)
|
||||
|
||||
// locked minoes
|
||||
for (var y = 0; y < this.rows; y++) {
|
||||
for (var x = 0; x < this.columns; x++) {
|
||||
if (this.clearedLines.includes(y))
|
||||
this.drawMino(x, y, CLASSNAME.CLEARED_LINE)
|
||||
else if (this.lockedMinoes[y][x])
|
||||
this.drawMino(x, y, this.lockedMinoes[y][x])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NextQueue extends MinoesTable {
|
||||
constructor() {
|
||||
super("nextTable")
|
||||
}
|
||||
|
||||
newGame() {
|
||||
this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(START_POSITION.NEXT[k]))
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.clearTable()
|
||||
this.pieces.forEach(piece => this.drawPiece(piece))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Stats {
|
||||
constructor() {
|
||||
this.scoreCell = document.getElementById("score")
|
||||
this.highScoreCell = document.getElementById("highScore")
|
||||
this.timeCell = document.getElementById("time")
|
||||
this.levelCell = document.getElementById("level")
|
||||
this.goalCell = document.getElementById("goal")
|
||||
this.clearedLinesCell = document.getElementById("clearedLines")
|
||||
this.highScore = Number(localStorage.getItem('highScore'))
|
||||
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
||||
this.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC"
|
||||
})
|
||||
}
|
||||
|
||||
newGame() {
|
||||
this.score = 0
|
||||
this.goal = 0
|
||||
this.goalCell.innerText = this.goal
|
||||
this.clearedLines = 0
|
||||
this.clearedLinesCell.innerText = this.clearedLines
|
||||
this.time = 0
|
||||
this.timeCell.innerText = this.timeFormat.format(0)
|
||||
this.combo = -1
|
||||
this.lockDelay = DELAY.LOCK
|
||||
this.fallPeriod = DELAY.FALL
|
||||
this.b2bSequence = false
|
||||
}
|
||||
|
||||
get score() {
|
||||
return this._score
|
||||
}
|
||||
|
||||
set score(score) {
|
||||
this._score = score
|
||||
this.scoreCell.innerText = this._score.toLocaleString()
|
||||
if (score > this.highScore) {
|
||||
this.highScore = score
|
||||
this.highScoreCell.innerText = this.highScore.toLocaleString()
|
||||
}
|
||||
document.title = `Webtris - Score : ${score}`
|
||||
}
|
||||
|
||||
newLevel(level=null) {
|
||||
this.level = level || this.level + 1
|
||||
location.hash = this.level
|
||||
this.levelCell.innerText = this.level
|
||||
printTempTexts(`NIVEAU<br/>${this.level}`)
|
||||
this.goal += 5 * this.level
|
||||
this.goalCell.innerText = this.goal
|
||||
if (this.level <= 20)
|
||||
this.fallPeriod = 1000 * Math.pow(0.8 - ((this.level - 1) * 0.007), this.level - 1)
|
||||
if (this.level > 15)
|
||||
this.lockDelay = 500 * Math.pow(0.9, this.level - 15)
|
||||
}
|
||||
|
||||
lockDown(tSpin, clearedLines) {
|
||||
var patternName = []
|
||||
var patternScore = 0
|
||||
var b2bScore = 0
|
||||
var comboScore = 0
|
||||
|
||||
if (tSpin)
|
||||
patternName.push(tSpin)
|
||||
if (clearedLines) {
|
||||
patternName.push(SCORES[clearedLines].linesClearedName)
|
||||
this.combo++
|
||||
} else
|
||||
this.combo = -1
|
||||
|
||||
if (clearedLines || tSpin) {
|
||||
this.clearedLines += clearedLines
|
||||
this.clearedLinesCell.innerText = this.clearedLines
|
||||
patternScore = SCORES[clearedLines][tSpin] * this.level
|
||||
this.goal -= clearedLines
|
||||
this.goalCell.innerText = this.goal
|
||||
patternName = patternName.join("\n")
|
||||
}
|
||||
|
||||
if (this.b2bSequence) {
|
||||
if ((clearedLines == 4) || (tSpin && clearedLines)) {
|
||||
b2bScore = patternScore / 2
|
||||
} else if ((0 < clearedLines) && (clearedLines < 4) && !tSpin) {
|
||||
this.b2bSequence = false
|
||||
}
|
||||
} else if ((clearedLines == 4) || (tSpin && clearedLines)) {
|
||||
this.b2bSequence = true
|
||||
}
|
||||
|
||||
if (this.combo >= 1)
|
||||
comboScore = (clearedLines == 1 ? 20 : 50) * this.combo * this.level
|
||||
|
||||
if (patternScore) {
|
||||
var messages = [patternName, patternScore]
|
||||
if (b2bScore)
|
||||
messages.push(`BACK TO BACK BONUS`, b2bScore)
|
||||
if (comboScore)
|
||||
messages.push(`COMBO x${this.combo}`, comboScore)
|
||||
printTempTexts(messages.join("<br/>"))
|
||||
}
|
||||
|
||||
this.score += patternScore + comboScore + b2bScore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Settings {
|
||||
constructor() {
|
||||
this.keyBind = {}
|
||||
for (let button of settingsSection.getElementsByTagName("button")) {
|
||||
let keyName = localStorage.getItem(button.id)
|
||||
if (keyName) {
|
||||
button.innerHTML = keyName
|
||||
this.keyBind[keyName == "Space"? " ": keyName] = playerAction[button.id]
|
||||
}
|
||||
}
|
||||
|
||||
let autorepeatDelay = localStorage.getItem("autorepeatDelay")
|
||||
if (autorepeatDelay) {
|
||||
autorepeatDelayRange.value = autorepeatDelay
|
||||
autorepeatDelayRange.oninput()
|
||||
}
|
||||
let autorepeatPeriod = localStorage.getItem("autorepeatPeriod")
|
||||
if (autorepeatPeriod) {
|
||||
autorepeatPeriodRange.value = autorepeatPeriod
|
||||
autorepeatPeriodRange.oninput()
|
||||
}
|
||||
|
||||
let themeName = localStorage.getItem("themeName")
|
||||
if (themeName) themeSelect.value = themeName
|
||||
let showGhost = localStorage.getItem("showGhost")
|
||||
if (showGhost) showGhostCheckbox.checked = showGhost == "true"
|
||||
|
||||
let startLevel = localStorage.getItem("startLevel")
|
||||
if (startLevel) startLevelInput.value = startLevel
|
||||
}
|
||||
|
||||
applyTheme = () => new Promise((resolve, reject) => {
|
||||
var link = document.createElement('link')
|
||||
link.id = 'theme'
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
link.href = `themes/${themeSelect.value}/style.css`
|
||||
link.media = 'all'
|
||||
link.onload = resolve
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
|
||||
save() {
|
||||
for (let button of settingsSection.getElementsByTagName("button")) {
|
||||
localStorage.setItem(button.id, button.innerHTML)
|
||||
}
|
||||
localStorage.setItem("autorepeatDelay", autorepeatDelayRange.value)
|
||||
localStorage.setItem("autorepeatPeriod", autorepeatPeriodRange.value)
|
||||
localStorage.setItem("themeName", themeSelect.value)
|
||||
localStorage.setItem("showGhost", showGhostCheckbox.checked)
|
||||
localStorage.setItem("startLevel", startLevelInput.value)
|
||||
}
|
||||
|
||||
waitKey(button) {
|
||||
document.onkeydown = null
|
||||
document.onkeyup = null
|
||||
button.previousKey = button.innerHTML
|
||||
button.innerHTML = "Touche ?"
|
||||
button.onkeyup = function(event) {
|
||||
event.preventDefault()
|
||||
button.innerHTML = (event.key == " ") ? "Space" : event.key
|
||||
settings.keyBind[event.key] = playerAction[button.id]
|
||||
button.onkeyup = null
|
||||
button.onblur = null
|
||||
document.onkeydown = onkeydown
|
||||
document.onkeyup = onkeyup
|
||||
}
|
||||
button.onblur = function(event) {
|
||||
button.innerHTML = button.previousKey
|
||||
button.onkeyup = null
|
||||
button.onblur = null
|
||||
document.onkeydown = onkeydown
|
||||
document.onkeyup = onkeyup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Functions
|
||||
|
||||
// Game logic
|
||||
state = STATE.WAITING
|
||||
|
||||
function newGame(startLevel) {
|
||||
startButton.blur()
|
||||
|
||||
settings.save()
|
||||
|
||||
holdQueue.newGame()
|
||||
matrix.newGame()
|
||||
nextQueue.newGame()
|
||||
stats.newGame()
|
||||
|
||||
startSection.style.display = "none"
|
||||
gameSection.style.display = "block"
|
||||
settingsSection.style.display = "none"
|
||||
footer.style.display = "none"
|
||||
|
||||
state = STATE.PLAYING
|
||||
pressedKeys = new Set()
|
||||
actionsToRepeat = []
|
||||
scheduler.setInterval(stats.clock, 1000)
|
||||
document.onkeydown = onkeydown
|
||||
document.onkeyup = onkeyup
|
||||
newLevel(startLevel)
|
||||
}
|
||||
|
||||
function newLevel(level) {
|
||||
stats.newLevel(level)
|
||||
generationPhase()
|
||||
}
|
||||
|
||||
function generationPhase(held_piece=null) {
|
||||
if (!held_piece) {
|
||||
matrix.piece = nextQueue.pieces.shift()
|
||||
nextQueue.pieces.push(new Tetromino())
|
||||
nextQueue.pieces.forEach((piece, i) => piece.pos = START_POSITION.NEXT[i])
|
||||
}
|
||||
nextQueue.draw()
|
||||
matrix.piece.pos = START_POSITION.MATRIX
|
||||
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos))){
|
||||
scheduler.clearInterval(lockPhase)
|
||||
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
||||
fallingPhase()
|
||||
} else
|
||||
gameOver()
|
||||
}
|
||||
|
||||
function lockPhase() {
|
||||
move(MOVEMENT.DOWN)
|
||||
}
|
||||
|
||||
function fallingPhase() {
|
||||
scheduler.clearTimeout(lockDown)
|
||||
matrix.piece.locked = false
|
||||
matrix.draw()
|
||||
}
|
||||
|
||||
function lockDown(){
|
||||
scheduler.clearInterval(lockPhase)
|
||||
if (matrix.piece.minoesAbsPos.every(pos => pos.y < MATRIX_INVISIBLE_ROWS)) {
|
||||
matrix.piece.locked = false
|
||||
matrix.draw()
|
||||
gameOver()
|
||||
} else {
|
||||
matrix.piece.minoesAbsPos.forEach(pos => matrix.lockedMinoes[pos.y][pos.x] = matrix.piece.className)
|
||||
|
||||
// T-Spin detection
|
||||
var tSpin = T_SPIN.NONE
|
||||
if (matrix.piece.rotatedLast && matrix.piece.shape == "T") {
|
||||
const tSlots = T_SLOT_POS.translate(matrix.piece.pos).map(pos => matrix.cellIsOccupied(...pos)),
|
||||
a = tSlots[(matrix.piece.orientation+T_SLOT.A)%4],
|
||||
b = tSlots[(matrix.piece.orientation+T_SLOT.B)%4],
|
||||
c = tSlots[(matrix.piece.orientation+T_SLOT.C)%4],
|
||||
d = tSlots[(matrix.piece.orientation+T_SLOT.D)%4]
|
||||
if (a && b && (c || d))
|
||||
tSpin = T_SPIN.T_SPIN
|
||||
else if (c && d && (a || b))
|
||||
tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI
|
||||
}
|
||||
|
||||
// Complete lines
|
||||
matrix.clearedLines = []
|
||||
matrix.lockedMinoes.forEach((row, y) => {
|
||||
if (row.filter(lockedMino => lockedMino.length).length == matrix.columns) {
|
||||
matrix.lockedMinoes.splice(y, 1)
|
||||
matrix.lockedMinoes.unshift(Array(matrix.columns))
|
||||
matrix.clearedLines.push(y)
|
||||
}
|
||||
})
|
||||
|
||||
stats.lockDown(tSpin, matrix.clearedLines.length)
|
||||
matrix.draw()
|
||||
scheduler.setTimeout(clearLinesCleared, DELAY.ANIMATION)
|
||||
|
||||
if (stats.goal <= 0)
|
||||
newLevel()
|
||||
else
|
||||
generationPhase()
|
||||
}
|
||||
}
|
||||
|
||||
function clearLinesCleared() {
|
||||
matrix.clearedLines = []
|
||||
matrix.draw()
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
state = STATE.GAME_OVER
|
||||
document.onkeydown = null
|
||||
document.onkeyup = null
|
||||
messageSpan.innerHTML = "GAME<br/>OVER"
|
||||
scheduler.clearInterval(lockPhase)
|
||||
scheduler.clearTimeout(lockDown)
|
||||
scheduler.clearInterval(stats.clock)
|
||||
|
||||
var info = `GAME OVER\nScore : ${stats.score.toLocaleString()}`
|
||||
if (stats.score == stats.highScore) {
|
||||
localStorage.setItem('highScore', stats.highScore)
|
||||
info += "\nBravo ! Vous avez battu votre précédent record."
|
||||
}
|
||||
|
||||
var retry = 0
|
||||
var fd = new FormData()
|
||||
fd.append("score", stats.score)
|
||||
var request = new XMLHttpRequest()
|
||||
request.onload = function(event) {
|
||||
if (event.target.responseText == "true") {
|
||||
var player = prompt(info + "\nBravo ! Vous êtes dans le Top 20.\nEntrez votre nom pour publier votre score :" , localStorage.getItem("player") || "")
|
||||
if (player && player.length) {
|
||||
localStorage.setItem("player", player)
|
||||
fd.append("player", player)
|
||||
request = new XMLHttpRequest()
|
||||
request.onload = function(event) {
|
||||
open("leaderboard.php")
|
||||
}
|
||||
request.onerror = function(event) {
|
||||
if (confirm('Erreur de connexion.\nRéessayer ?')) {
|
||||
request.open('POST', 'publish.php')
|
||||
request.send(fd)
|
||||
}
|
||||
}
|
||||
request.open('POST', 'publish.php')
|
||||
request.send(fd)
|
||||
} else
|
||||
alert(info)
|
||||
}
|
||||
}
|
||||
request.onerror = function(event) {
|
||||
retry++
|
||||
if (retry < RETRIES) {
|
||||
request.open('POST', 'inleaderboard.php')
|
||||
request.send(fd)
|
||||
} else
|
||||
alert(info)
|
||||
}
|
||||
request.open('POST', 'inleaderboard.php')
|
||||
request.send(fd)
|
||||
|
||||
location.hash = "game-over"
|
||||
|
||||
startSection.style.display = "block"
|
||||
footer.style.display = "block"
|
||||
}
|
||||
|
||||
function move(movement, testMinoesPos=matrix.piece.minoesPos, hardDrop=false) {
|
||||
const testPos = matrix.piece.pos.add(movement)
|
||||
if (matrix.spaceToMove(testMinoesPos.translate(testPos))) {
|
||||
matrix.piece.pos = testPos
|
||||
matrix.piece.minoesPos = testMinoesPos
|
||||
matrix.piece.rotatedLast = false
|
||||
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN))))
|
||||
fallingPhase()
|
||||
else if (!hardDrop) {
|
||||
matrix.piece.locked = true
|
||||
scheduler.clearTimeout(lockDown)
|
||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||
}
|
||||
if (!hardDrop)
|
||||
matrix.draw()
|
||||
return true
|
||||
} else {
|
||||
if (movement == MOVEMENT.DOWN) {
|
||||
matrix.piece.locked = true
|
||||
if (!scheduler.timeoutTasks.has(lockDown))
|
||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||
matrix.draw()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function rotate(spin) {
|
||||
const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin))
|
||||
let rotationPoint = 1
|
||||
for (const movement of matrix.piece.srs[spin][matrix.piece.orientation]) {
|
||||
if (move(movement, test_minoes_pos)) {
|
||||
matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4
|
||||
matrix.piece.rotatedLast = true
|
||||
if (rotationPoint == 5)
|
||||
matrix.piece.rotationPoint5Used = true
|
||||
return true
|
||||
}
|
||||
rotationPoint++
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function pause() {
|
||||
state = STATE.PAUSED
|
||||
location.hash = "pause"
|
||||
stats.startTime = performance.now() - stats.startTime
|
||||
actionsToRepeat = []
|
||||
scheduler.clearInterval(lockPhase)
|
||||
scheduler.clearTimeout(lockDown)
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(stats.clock)
|
||||
scheduler.clearInterval(delTempTexts)
|
||||
holdQueue.draw()
|
||||
matrix.draw()
|
||||
nextQueue.draw()
|
||||
header.style.display = "block"
|
||||
gameSection.style.display = "none"
|
||||
settingsSection.style.display = "block"
|
||||
}
|
||||
|
||||
function resume() {
|
||||
settings.save()
|
||||
settingsSection.style.display = "none"
|
||||
gameSection.style.display = "block"
|
||||
location.hash = stats.level
|
||||
state = STATE.PLAYING
|
||||
stats.startTime = performance.now() - stats.startTime
|
||||
messageSpan.innerHTML = ""
|
||||
scheduler.setInterval(lockPhase, stats.fallPeriod)
|
||||
if (matrix.piece.locked)
|
||||
scheduler.setTimeout(lockDown, stats.lockDelay)
|
||||
scheduler.setInterval(stats.clock, 1000)
|
||||
holdQueue.draw()
|
||||
matrix.draw()
|
||||
nextQueue.draw()
|
||||
if (tempTexts.length)
|
||||
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
||||
}
|
||||
|
||||
playerAction = {
|
||||
moveLeft: function () {
|
||||
move(MOVEMENT.LEFT)
|
||||
},
|
||||
|
||||
moveRight: function () {
|
||||
move(MOVEMENT.RIGHT)
|
||||
},
|
||||
|
||||
softDrop: function () {
|
||||
if (move(MOVEMENT.DOWN))
|
||||
stats.score++
|
||||
},
|
||||
|
||||
hardDrop: function () {
|
||||
scheduler.clearInterval(lockPhase)
|
||||
scheduler.clearTimeout(lockDown)
|
||||
matrix.trail.minoesPos = Array.from(matrix.piece.minoesAbsPos)
|
||||
for (matrix.trail.height = 0; move(MOVEMENT.DOWN, matrix.piece.minoesPos, true); matrix.trail.height++) {}
|
||||
stats.score += 2 * matrix.trail.height
|
||||
matrix.draw()
|
||||
lockDown()
|
||||
scheduler.setTimeout(() => {matrix.trail.height = 0; matrix.draw()}, DELAY.ANIMATION)
|
||||
},
|
||||
|
||||
rotateCW: function () {
|
||||
rotate(SPIN.CW)
|
||||
},
|
||||
|
||||
rotateCCW: function () {
|
||||
rotate(SPIN.CCW)
|
||||
},
|
||||
|
||||
hold: function () {
|
||||
if (matrix.piece.holdEnabled) {
|
||||
scheduler.clearInterval(move)
|
||||
scheduler.clearInterval(lockDown)
|
||||
var shape = matrix.piece.shape
|
||||
matrix.piece = holdQueue.piece
|
||||
holdQueue.piece = new Tetromino(START_POSITION.HOLD, shape)
|
||||
holdQueue.piece.holdEnabled = false
|
||||
holdQueue.draw()
|
||||
generationPhase(matrix.piece)
|
||||
matrix.piece.holdEnabled = false
|
||||
}
|
||||
},
|
||||
|
||||
pauseResume: function() {
|
||||
if (state == STATE.PLAYING) {
|
||||
pause()
|
||||
} else {
|
||||
resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function autorepeat() {
|
||||
if (actionsToRepeat.length) {
|
||||
actionsToRepeat[0]()
|
||||
if (scheduler.timeoutTasks.has(autorepeat)) {
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.setInterval(autorepeat, autorepeatPeriodRange.value)
|
||||
}
|
||||
} else {
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle player inputs
|
||||
const REPEATABLE_ACTION = [playerAction.moveLeft, playerAction.moveRight, playerAction.softDrop]
|
||||
pressedKeys = new Set()
|
||||
actionsToRepeat = []
|
||||
|
||||
function onkeydown(event) {
|
||||
if (event.key in settings.keyBind)
|
||||
event.preventDefault()
|
||||
if (!pressedKeys.has(event.key)) {
|
||||
pressedKeys.add(event.key)
|
||||
if (event.key in settings.keyBind) {
|
||||
action = settings.keyBind[event.key]
|
||||
action()
|
||||
if (REPEATABLE_ACTION.includes(action)) {
|
||||
actionsToRepeat.unshift(action)
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
if (action == softDrop)
|
||||
scheduler.setInterval(autorepeat, stats.fallPeriod / 20)
|
||||
else
|
||||
scheduler.setTimeout(autorepeat, autorepeatDelayRange.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onkeyup(event) {
|
||||
pressedKeys.delete(event.key)
|
||||
if (event.key in settings.keyBind) {
|
||||
action = settings.keyBind[event.key]
|
||||
if (actionsToRepeat.includes(action)) {
|
||||
actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1)
|
||||
if (!actionsToRepeat.length) {
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text display
|
||||
tempTexts = []
|
||||
function printTempTexts(text) {
|
||||
tempTexts.push(text)
|
||||
messageSpan.innerHTML = tempTexts[0]
|
||||
if (!scheduler.intervalTasks.has(delTempTexts))
|
||||
scheduler.setInterval(delTempTexts, DELAY.MESSAGE)
|
||||
messageSpan.classList.add(CLASSNAME.MESSAGE_SPAN_FADE_OUT)
|
||||
}
|
||||
|
||||
function delTempTexts(self) {
|
||||
messageSpan.classList.remove(CLASSNAME.MESSAGE_SPAN_FADE_OUT)
|
||||
if (tempTexts.length)
|
||||
tempTexts.shift()
|
||||
if (tempTexts.length)
|
||||
messageSpan.innerHTML = tempTexts[0]
|
||||
else {
|
||||
scheduler.clearInterval(delTempTexts)
|
||||
messageSpan.innerHTML = ""
|
||||
}
|
||||
}
|
||||
|
||||
function clock() {
|
||||
timeCell.innerText = stats.timeFormat.format(1000 * ++stats.time)
|
||||
}
|
||||
|
||||
|
||||
// Initialization
|
||||
window.onload = async function() {
|
||||
scheduler = new Scheduler()
|
||||
holdQueue = new HoldQueue()
|
||||
stats = new Stats()
|
||||
matrix = new Matrix()
|
||||
nextQueue = new NextQueue()
|
||||
settings = new Settings()
|
||||
await settings.applyTheme()
|
||||
let startLevel = parseInt(location.hash.slice(1))
|
||||
body.style.display = "block"
|
||||
if (1 <= startLevel && startLevel <= 15) {
|
||||
newGame(startLevel)
|
||||
} else {
|
||||
location.hash = ""
|
||||
startButton.focus()
|
||||
}
|
||||
}
|
||||
|
||||
window.onblur = pause
|
@ -1,67 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Share Tech Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Share Tech Mono'), local('ShareTechMono-Regular'), url(../fonts/ShareTechMono.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
background-image: url("../images/bg.jpg");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 5% 2%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.hold {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 10% 0;
|
||||
font-family: 'Share Tech Mono';
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.stats-values {
|
||||
text-align: right;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.matrix {
|
||||
margin: 5% 2%;
|
||||
border: 0.5px solid grey;
|
||||
}
|
||||
|
||||
.next {
|
||||
max-width: 120px;
|
||||
margin: 5% 2%;
|
||||
}
|
7
db_connect.php.example
Normal file
@ -0,0 +1,7 @@
|
||||
// Complete and rename to db_connect.php
|
||||
<?php
|
||||
$DB_NAME = "webtris";
|
||||
$DB_USER = XXX;
|
||||
$DB_PASSWORD = XXX;
|
||||
$DB_HOST = "localhost";
|
||||
?>
|
BIN
favicon.png
Normal file
After Width: | Height: | Size: 211 B |
BIN
images/bg.jpg
Before Width: | Height: | Size: 218 KiB |
37
index.html
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Webtris</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css" />
|
||||
<!--[if lt IE 9]><script type="text/javascript" src="js/excanvas.js"></script><![endif]-->
|
||||
<script type="text/javascript" src="js/webtris.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="columns">
|
||||
<div class="space"></div>
|
||||
<div class="rows">
|
||||
<div class="columns">
|
||||
<div class="space"></div>
|
||||
<canvas id="hold" class="hold" width="120" height="120"></canvas>
|
||||
</div>
|
||||
<div class="space"></div>
|
||||
<div class="stats">
|
||||
<div class="stats-names">
|
||||
SCORE<br/>
|
||||
HIGH<br/>
|
||||
LEVEL<br/>
|
||||
GOAL<br/>
|
||||
LINES<br/>
|
||||
TIME<br/>
|
||||
</div>
|
||||
<div id="stats-values" class="stats-values"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<canvas id="matrix" class="matrix" width="200" height="400">Votre navigateur ne supporte pas HTML5, veuillez le mettre à jour pour jouer.</canvas>
|
||||
<canvas id="next" class="next" width="120" height="400"></canvas>
|
||||
<div class="space"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
173
index.php
Normal file
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
$actions = [
|
||||
"moveLeft" => ["label"=>"Gauche", "defaultKey"=>"ArrowLeft"],
|
||||
"moveRight" => ["label"=>"Droite", "defaultKey"=>"ArrowRight"],
|
||||
"softDrop" => ["label"=>"Chute lente", "defaultKey"=>"ArrowDown"],
|
||||
"hardDrop" => ["label"=>"Chute rapide", "defaultKey"=>"Space"],
|
||||
"rotateCW" => ["label"=>"Rotation horaire", "defaultKey"=>"ArrowUp"],
|
||||
"rotateCCW" => ["label"=>"Rotation anti-horaire", "defaultKey"=>"z"],
|
||||
"hold" => ["label"=>"Garde", "defaultKey"=>"c"],
|
||||
"pauseResume" => ["label"=>"Pause/Reprise", "defaultKey"=>"Escape"]
|
||||
];
|
||||
|
||||
function echoTable($id, $invisibleRows, $visibleRows, $columns) {
|
||||
echo " <table id='$id' class=minoes-table>\n";
|
||||
for ($y = 0; $y < $invisibleRows; $y++) {
|
||||
echo " <tr>";
|
||||
for ($x = 0; $x < $columns; $x++) {
|
||||
echo "<th></td>";
|
||||
}
|
||||
echo "</tr>\n";
|
||||
}
|
||||
for ($y = 0; $y < $visibleRows; $y++) {
|
||||
echo " <tr>";
|
||||
for ($x = 0; $x < $columns; $x++) {
|
||||
echo "<td class=empty-cell></td>";
|
||||
}
|
||||
echo "</tr>\n";
|
||||
}
|
||||
echo " </table>\n";
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Webtris</title>
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<script type="text/javascript" src="app.js"></script>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body id="body" style="display:none">
|
||||
<header id="header">
|
||||
<h1>WEBTRIS</h1>
|
||||
</header>
|
||||
<section id="settingsSection">
|
||||
<fieldset>
|
||||
<legend>Clavier</legend>
|
||||
<div>
|
||||
<?php
|
||||
foreach($actions as $action=>$parameters) {
|
||||
?>
|
||||
<label for='<?=$action?>'><?=$parameters["label"]?></label>
|
||||
<button id='<?=$action?>' type='button' onclick="settings.waitKey(this)"><?=$parameters["defaultKey"]?></button>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Répétition automatique</legend>
|
||||
<div>
|
||||
<label id="autorepeatDelayRangeLabel" for="autorepeatDelayRange">Délai initial : 300ms</label>
|
||||
<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>
|
||||
<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>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Style</legend>
|
||||
<div>
|
||||
<label for="themeSelect">Thème</label>
|
||||
<select id="themeSelect" oninput="settings.applyTheme()" value="default">
|
||||
<?php
|
||||
foreach(array_slice(scandir("themes"), 2) as $theme) {
|
||||
if (is_dir(pathinfo($theme)['dirname']))
|
||||
echo " <option>" . $theme . "</option>\n";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<label for="showGhostCheckbox">Afficher le fantôme</label>
|
||||
<input id="showGhostCheckbox" type="checkbox" checked/>
|
||||
<table id="themePreviewTable" class="minoes-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="mino I"></th>
|
||||
<th></th>
|
||||
<th class="mino J"></th>
|
||||
<th class="mino J"></th>
|
||||
<th class="mino J"></th>
|
||||
<th></th>
|
||||
<th class="mino S"></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mino I"></th>
|
||||
<th class="mino O"></th>
|
||||
<th class="mino O"></th>
|
||||
<th></th>
|
||||
<th class="mino J"></th>
|
||||
<th class="mino Z"></th>
|
||||
<th class="mino S"></th>
|
||||
<th class="mino S"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mino I"></th>
|
||||
<th class="mino O"></th>
|
||||
<th class="mino O"></th>
|
||||
<th class="mino L"></th>
|
||||
<th class="mino Z"></th>
|
||||
<th class="mino Z"></th>
|
||||
<th class="mino T"></th>
|
||||
<th class="mino S"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="mino I"></th>
|
||||
<th class="mino L"></th>
|
||||
<th class="mino L"></th>
|
||||
<th class="mino L"></th>
|
||||
<th class="mino Z"></th>
|
||||
<th class="mino T"></th>
|
||||
<th class="mino T"></th>
|
||||
<th class="mino T"></th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
<section id="gameSection" style="display: none">
|
||||
<div>
|
||||
<?php
|
||||
echoTable("holdTable", 6, 0, 6);
|
||||
echoTable("matrixTable", 4, 20, 10);
|
||||
echoTable("nextTable", 24, 0, 6);
|
||||
?>
|
||||
<table id="statsTable">
|
||||
<tr><th colspan=2>SCORE</th></tr>
|
||||
<tr><td id="score" colspan=2>0</td></tr>
|
||||
<tr><th colspan=2>RECORD</th></tr>
|
||||
<tr><td id="highScore" colspan=2>0</td></tr>
|
||||
<tr><th colspan=2>TEMPS</th></tr>
|
||||
<tr><td id="time" colspan=2>00:00</td></tr>
|
||||
<tr><td colspan=2><br/></td></tr>
|
||||
<tr><th>NIVEAU</th><td id="level">0</td></tr>
|
||||
<tr><th>OBJECTIF</th><td id="goal">0</td></tr>
|
||||
<tr><th>LIGNES</th><td id="clearedLines">0</td></tr>
|
||||
</table>
|
||||
<span id="messageSpan"></span>
|
||||
</div>
|
||||
</section>
|
||||
<section id="startSection">
|
||||
<fieldset>
|
||||
<legend>Nouvelle partie</legend>
|
||||
<div>
|
||||
<label for="startLevel">Niveau</label>
|
||||
<input type="number" id="startLevelInput" min="1" max="15" step="1" value="1">
|
||||
<div></div>
|
||||
<button id="startButton" type="button" onclick="newGame(Number(startLevelInput.value))">JOUER</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
<footer id="footer">
|
||||
<div>
|
||||
<a href="leaderboard.php" target="_blank">TABLEAU DE SCORE</a>
|
||||
</div>
|
||||
<div id="credits">
|
||||
Sources d'inspiration des thèmes :
|
||||
<a href="https://github.com/kubowania/Tetris">Ania Kubow</a>
|
||||
<a href="https://www.tetriseffect.game/">Tetris Effect</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
20
inleaderboard.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
include "db_connect.php";
|
||||
if (isset($_POST['score'])) {
|
||||
try {
|
||||
$db = new PDO("mysql:host=$DB_HOST;dbname=$DB_NAME;charset=utf8", $DB_USER, $DB_PASSWORD);
|
||||
} catch (Exception $e) {
|
||||
die('Erreur : ' . $e->getMessage());
|
||||
}
|
||||
$score = (int) $_POST['score'];
|
||||
$entryScore = (int) $db->query('SELECT score FROM `leaderboard` ORDER BY score DESC LIMIT 19, 1;')->fetch()['score'];
|
||||
if ($score > $entryScore)
|
||||
echo "true";
|
||||
else
|
||||
echo "false";
|
||||
$query = null;
|
||||
$db = null;
|
||||
} else {
|
||||
header($_SERVER["SERVER_PROTOCOL"] . " 405 Method Not Allowed", true, 405);
|
||||
}
|
||||
?>
|
1414
js/excanvas.js
631
js/webtris.js
@ -1,631 +0,0 @@
|
||||
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[1], spin*this[0]]
|
||||
}
|
||||
|
||||
Array.prototype.pick = function() {
|
||||
return this.splice(Math.floor(Math.random()*this.length), 1)[0]
|
||||
}
|
||||
|
||||
|
||||
const MINO_SIZE = 20
|
||||
const NEXT_PIECES = 5
|
||||
const HOLD_ROWS = 6
|
||||
const HOLD_COLUMNS = 6
|
||||
const MATRIX_ROWS = 20
|
||||
const MATRIX_COLUMNS = 10
|
||||
const NEXT_ROWS = 20
|
||||
const NEXT_COLUMNS = 6
|
||||
const HELD_PIECE_POSITION = [2, 2]
|
||||
const FALLING_PIECE_POSITION = [4, 0]
|
||||
const NEXT_PIECES_POSITIONS = Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+2])
|
||||
const LOCK_DELAY = 500
|
||||
const FALL_DELAY = 1000
|
||||
const AUTOREPEAT_DELAY = 250
|
||||
const AUTOREPEAT_PERIOD = 10
|
||||
const MOVEMENT = {
|
||||
LEFT: [-1, 0],
|
||||
RIGHT: [ 1, 0],
|
||||
DOWN: [ 0, 1]
|
||||
}
|
||||
const SPIN = {
|
||||
CW: 1,
|
||||
CCW: -1
|
||||
}
|
||||
const T_SPIN = {
|
||||
NONE: "",
|
||||
MINI: "MINI\nT-SPIN",
|
||||
T_SPIN: "T-SPIN"
|
||||
}
|
||||
const T_SLOT = {
|
||||
A: 0,
|
||||
B: 1,
|
||||
C: 3,
|
||||
D: 2
|
||||
}
|
||||
const SCORES = [
|
||||
{linesClearedName: "", "": 0, "MINI\nT-SPIN": 1, "T-SPIN": 4},
|
||||
{linesClearedName: "SINGLE", "": 1, "MINI\nT-SPIN": 2, "T-SPIN": 8},
|
||||
{linesClearedName: "DOUBLE", "": 3, "T-SPIN": 12},
|
||||
{linesClearedName: "TRIPLE", "": 5, "T-SPIN": 16},
|
||||
{linesClearedName: "TETRIS", "": 8},
|
||||
]
|
||||
const REPEATABLE_ACTIONS = [moveLeft, moveRight, softDrop]
|
||||
const T_SLOT_POS = [[-1, -1], [1, -1], [1, 1], [-1, 1]]
|
||||
const STATE = {
|
||||
PLAYING: "",
|
||||
PAUSE: "PAUSE",
|
||||
GAME_OVER: "GAME\nOVER"
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
shapes = []
|
||||
class Tetromino {
|
||||
constructor(position=null, shape=null) {
|
||||
this.pos = position
|
||||
this.orientation = 0
|
||||
this.rotatedLast = false
|
||||
this.rotationPoint5Used = false
|
||||
this.holdEnabled = true
|
||||
this.locked = false
|
||||
this.srs = {}
|
||||
this.srs[SPIN.CW] = [
|
||||
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]],
|
||||
[[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]],
|
||||
[[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]],
|
||||
[[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]],
|
||||
]
|
||||
this.srs[SPIN.CCW] = [
|
||||
[[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]],
|
||||
[[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]],
|
||||
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]],
|
||||
[[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]],
|
||||
]
|
||||
if (shape)
|
||||
this.shape = shape
|
||||
else {
|
||||
if (!shapes.length)
|
||||
shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
|
||||
this.shape = shapes.pick()
|
||||
}
|
||||
switch(this.shape) {
|
||||
case 'I':
|
||||
this.color = "cyan"
|
||||
this.lightColor = "rgb(234, 250, 250)"
|
||||
this.ghostColor = "rgba(234, 250, 250, 0.5)"
|
||||
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]]
|
||||
this.srs[SPIN.CW] = [
|
||||
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
|
||||
[[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]],
|
||||
[[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]],
|
||||
[[ 0, 1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]],
|
||||
]
|
||||
this.srs[SPIN.CCW] = [
|
||||
[[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]],
|
||||
[[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]],
|
||||
[[ 0, -1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]],
|
||||
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
|
||||
]
|
||||
break
|
||||
case 'J':
|
||||
this.color = "blue"
|
||||
this.lightColor = "rgb(230, 240, 255)"
|
||||
this.ghostColor = "rgba(230, 240, 255, 0.5)"
|
||||
this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]]
|
||||
break
|
||||
case 'L':
|
||||
this.color = "orange"
|
||||
this.lightColor = "rgb(255, 224, 204)"
|
||||
this.ghostColor = "rgba(255, 224, 204, 0.5)"
|
||||
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]]
|
||||
break
|
||||
case 'O':
|
||||
this.color = "yellow"
|
||||
this.lightColor = "rgb(255, 255, 230)"
|
||||
this.ghostColor = "rgba(255, 255, 230, 0.5)"
|
||||
this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]]
|
||||
this.srs[SPIN.CW] = [[]]
|
||||
this.srs[SPIN.CCW] = [[]]
|
||||
break
|
||||
case 'S':
|
||||
this.color = "green"
|
||||
this.lightColor = "rgb(236, 255, 230)"
|
||||
this.ghostColor = "rgba(236, 255, 230, 0.5)"
|
||||
this.minoesPos = [[-1, 0], [0, 0], [0, -1], [1, -1]]
|
||||
break
|
||||
case 'T':
|
||||
this.color = "magenta"
|
||||
this.lightColor= "rgb(242, 230, 255)"
|
||||
this.ghostColor = "rgba(242, 230, 255, 0.5)"
|
||||
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]]
|
||||
break
|
||||
case 'Z':
|
||||
this.color = "red"
|
||||
this.lightColor = "rgb(255, 230, 230)"
|
||||
this.ghostColor = "rgba(255, 230, 230, 0.5)"
|
||||
this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
get minoesAbsPos() {
|
||||
return this.minoesPos.translate(this.pos)
|
||||
}
|
||||
|
||||
draw(context, ghost_pos=[0, 0]) {
|
||||
const color = this.locked ? this.lightColor : this.color
|
||||
if (ghost_pos[1]) {
|
||||
context.save()
|
||||
context.shadowColor = this.ghostColor
|
||||
context.shadowOffsetX = 0
|
||||
context.shadowOffsetY = (ghost_pos[1]-this.pos[1]) * MINO_SIZE
|
||||
context.shadowBlur = 3
|
||||
this.minoesAbsPos.forEach(pos => drawMino(context, pos, color))
|
||||
context.restore()
|
||||
}
|
||||
this.minoesAbsPos.forEach(pos => drawMino(context, pos, this.lightColor, color, ghost_pos))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawMino(context, pos, color1, color2=null, spotlight=[0, 0]) {
|
||||
if (color2) {
|
||||
var center = pos.add([0.5, 0.5])
|
||||
spotlight = spotlight.add([0.5, 0.5])
|
||||
var glint = spotlight.mul(0.1).add(center.mul(0.9)).mul(MINO_SIZE)
|
||||
const gradient = context.createRadialGradient(
|
||||
...glint, 2, ...glint.add([6, 4]), 2*MINO_SIZE
|
||||
)
|
||||
gradient.addColorStop(0, color1)
|
||||
gradient.addColorStop(1, color2)
|
||||
context.fillStyle = gradient
|
||||
}
|
||||
else
|
||||
context.fillStyle = color1
|
||||
var topLeft = pos.mul(MINO_SIZE)
|
||||
context.fillRect(...topLeft, MINO_SIZE, MINO_SIZE)
|
||||
context.lineWidth = 0.5
|
||||
context.strokeStyle = "white"
|
||||
context.strokeRect(...topLeft, MINO_SIZE, MINO_SIZE)
|
||||
}
|
||||
|
||||
|
||||
class HoldQueue {
|
||||
constructor(context) {
|
||||
this.context = context
|
||||
this.piece = null
|
||||
this.width = HOLD_COLUMNS*MINO_SIZE
|
||||
this.height = HOLD_ROWS*MINO_SIZE
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.context.clearRect(0, 0, this.width, this.height)
|
||||
if (this.piece)
|
||||
this.piece.draw(this.context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
timeFormat = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric", minute: "2-digit", second: "2-digit", hourCycle: "h24", timeZone: "UTC"
|
||||
}).format
|
||||
|
||||
|
||||
class Stats {
|
||||
constructor (div) {
|
||||
this.div = div
|
||||
this._score = 0
|
||||
this.highScore = 0
|
||||
this.goal = 0
|
||||
this.linesCleared = 0
|
||||
this.startTime = Date.now()
|
||||
this.combo = -1
|
||||
this.lockDelay = LOCK_DELAY
|
||||
this.fallDelay = FALL_DELAY
|
||||
}
|
||||
|
||||
get score() {
|
||||
return this._score
|
||||
}
|
||||
|
||||
set score(score) {
|
||||
this._score = score
|
||||
if (score > this.highScore)
|
||||
this.highScore = score
|
||||
}
|
||||
|
||||
newLevel(level=null) {
|
||||
if (level)
|
||||
this.level = level
|
||||
else
|
||||
this.level++
|
||||
this.goal += 5 * this.level
|
||||
if (this.level <= 20)
|
||||
this.fallDelay = 1000 * Math.pow(0.8 - ((this.level - 1) * 0.007), this.level - 1)
|
||||
if (this.level > 15)
|
||||
this.lockDelay = 500 * Math.pow(0.9, this.level - 15)
|
||||
}
|
||||
|
||||
locksDown(tSpin, linesCleared) {
|
||||
var pattern_name = []
|
||||
var pattern_score = 0
|
||||
var combo_score = 0
|
||||
|
||||
if (tSpin)
|
||||
pattern_name.push(tSpin)
|
||||
if (linesCleared) {
|
||||
pattern_name.push(SCORES[linesCleared].linesClearedName)
|
||||
this.combo++
|
||||
}
|
||||
else
|
||||
this.combo = -1
|
||||
|
||||
if (linesCleared || tSpin) {
|
||||
pattern_score = SCORES[linesCleared][tSpin]
|
||||
this.goal -= pattern_score
|
||||
pattern_score *= 100 * this.level
|
||||
pattern_name = pattern_name.join("\n")
|
||||
}
|
||||
if (this.combo >= 1)
|
||||
combo_score = (linesCleared == 1 ? 20 : 50) * this.combo * this.level
|
||||
|
||||
this.score += pattern_score + combo_score
|
||||
|
||||
//console.log(pattern_name, pattern_score, this.combo, combo_score)
|
||||
}
|
||||
|
||||
print() {
|
||||
this.div.innerHTML = this.score + "<br/>"
|
||||
+ this.highScore + "<br/>"
|
||||
+ this.level + "<br/>"
|
||||
+ this.goal + "<br/>"
|
||||
+ this.linesCleared + "<br/>"
|
||||
+ timeFormat(Date.now() - this.startTime)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Matrix {
|
||||
constructor(context) {
|
||||
this.context = context
|
||||
this.cells = Array.from(Array(MATRIX_ROWS+3), row => Array(MATRIX_COLUMNS))
|
||||
this.width = MATRIX_COLUMNS*MINO_SIZE
|
||||
this.height = MATRIX_ROWS*MINO_SIZE
|
||||
this.piece = null
|
||||
}
|
||||
|
||||
cellIsOccupied(x, y) {
|
||||
return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_ROWS ? this.cells[y+3][x] : true
|
||||
}
|
||||
|
||||
spaceToMove(minoesAbsPos) {
|
||||
return !minoesAbsPos.some(pos => this.cellIsOccupied(...pos))
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.context.clearRect(0, 0, this.width, this.height)
|
||||
|
||||
// grid
|
||||
this.context.strokeStyle = "rgba(128, 128, 128, 128)"
|
||||
this.context.lineWidth = 0.5
|
||||
this.context.beginPath()
|
||||
for (var x = 0; x <= this.width; x += MINO_SIZE) {
|
||||
this.context.moveTo(x, 0);
|
||||
this.context.lineTo(x, this.height);
|
||||
}
|
||||
for (var y = 0; y <= this.height; y += MINO_SIZE) {
|
||||
this.context.moveTo(0, y);
|
||||
this.context.lineTo(this.width, y);
|
||||
}
|
||||
this.context.stroke()
|
||||
|
||||
// ghost position
|
||||
for (var ghost_pos = Array.from(this.piece.pos); this.spaceToMove(this.piece.minoesPos.translate(ghost_pos)); ghost_pos[1]++) {}
|
||||
ghost_pos[1]--
|
||||
|
||||
// locked minoes
|
||||
this.cells.slice(3).forEach((row, y) => row.forEach((colors, x) => {
|
||||
if (colors) drawMino(this.context, [x, y], ...colors, ghost_pos)
|
||||
}))
|
||||
|
||||
// falling piece
|
||||
if (this.piece)
|
||||
this.piece.draw(this.context, ghost_pos)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NextQueue {
|
||||
constructor(context) {
|
||||
this.context = context
|
||||
this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(NEXT_PIECES_POSITIONS[k]))
|
||||
this.width = NEXT_COLUMNS*MINO_SIZE
|
||||
this.height = NEXT_ROWS*MINO_SIZE
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.context.clearRect(0, 0, this.width, this.height)
|
||||
this.pieces.forEach(piece => piece.draw(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function newLevel(startLevel) {
|
||||
stats.newLevel(startLevel)
|
||||
generationPhase()
|
||||
}
|
||||
|
||||
function generationPhase(held_piece=null) {
|
||||
if (!held_piece) {
|
||||
matrix.piece = nextQueue.pieces.shift()
|
||||
nextQueue.pieces.push(new Tetromino())
|
||||
nextQueue.pieces.forEach((piece, i) => piece.pos = NEXT_PIECES_POSITIONS[i])
|
||||
}
|
||||
matrix.piece.pos = FALLING_PIECE_POSITION
|
||||
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos)))
|
||||
fallingPhase()
|
||||
else
|
||||
gameOver()
|
||||
}
|
||||
|
||||
function fallingPhase() {
|
||||
scheduler.clearTimeout(lockPhase)
|
||||
scheduler.clearTimeout(locksDown)
|
||||
matrix.piece.locked = false
|
||||
scheduler.setTimeout(lockPhase, stats.fallDelay)
|
||||
}
|
||||
|
||||
function lockPhase() {
|
||||
if (!move(MOVEMENT.DOWN))
|
||||
locksDown()
|
||||
}
|
||||
|
||||
function move(movement, lock=true, testMinoesPos=matrix.piece.minoesPos) {
|
||||
const testPos = matrix.piece.pos.add(movement)
|
||||
if (matrix.spaceToMove(testMinoesPos.translate(testPos))) {
|
||||
matrix.piece.pos = testPos
|
||||
matrix.piece.minoesPos = testMinoesPos
|
||||
if (movement != MOVEMENT.DOWN)
|
||||
matrix.piece.rotatedLast = false
|
||||
if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN))))
|
||||
fallingPhase()
|
||||
else if (lock) {
|
||||
matrix.piece.locked = true
|
||||
scheduler.clearTimeout(locksDown)
|
||||
scheduler.setTimeout(locksDown, stats.lockDelay)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else {
|
||||
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, false, 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 locksDown(){
|
||||
scheduler.clearInterval(move)
|
||||
if (matrix.piece.minoesAbsPos.every(pos => pos.y < 0))
|
||||
game_over()
|
||||
else {
|
||||
matrix.piece.minoesAbsPos.forEach(pos => matrix.cells[pos[1]+3][pos[0]] = [matrix.piece.lightColor, matrix.piece.color])
|
||||
|
||||
// T-Spin detection
|
||||
var tSpin = T_SPIN.NONE
|
||||
const tSlots = T_SLOT_POS.translate(matrix.piece.pos).map(pos => matrix.cellIsOccupied(pos))
|
||||
if (matrix.piece.rotatedLast && matrix.piece.shape == "T") {
|
||||
const a = tSlots[(matrix.piece.orientation+T_SLOT.A)%4],
|
||||
b = tSlots[(matrix.piece.orientation+T_SLOT.B)%4],
|
||||
c = tSlots[(matrix.piece.orientation+T_SLOT.C)%4],
|
||||
d = tSlots[(matrix.piece.orientation+T_SLOT.D)%4]
|
||||
if (a && b && (c || d))
|
||||
tSpin = T_SPIN.T_SPIN
|
||||
else if (c && d && (a || b))
|
||||
tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI
|
||||
}
|
||||
|
||||
// Complete lines
|
||||
var linesCleared = 0
|
||||
matrix.cells.forEach((row, y) => {
|
||||
if (row.filter(mino => mino.length).length == MATRIX_COLUMNS) {
|
||||
matrix.cells.splice(y, 1)
|
||||
matrix.cells.unshift(Array(MATRIX_COLUMNS))
|
||||
linesCleared++
|
||||
}
|
||||
})
|
||||
|
||||
stats.locksDown(tSpin, linesCleared)
|
||||
|
||||
if (stats.goal <= 0)
|
||||
newLevel()
|
||||
else
|
||||
generationPhase()
|
||||
}
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
state = STATE.GAME_OVER
|
||||
scheduler.clearTimeout(lockPhase)
|
||||
scheduler.clearTimeout(locksDown)
|
||||
console.log("GAME OVER")
|
||||
}
|
||||
|
||||
function autorepeat() {
|
||||
if (actionsToRepeat.length) {
|
||||
actionsToRepeat[0]()
|
||||
if (scheduler.timeoutTasks.has(autorepeat)) {
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.setInterval(autorepeat, AUTOREPEAT_PERIOD)
|
||||
}
|
||||
}
|
||||
else {
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
}
|
||||
}
|
||||
|
||||
function keyDownHandler(e) {
|
||||
if (e.key in actions) {
|
||||
if (!pressedKeys.has(e.key)) {
|
||||
pressedKeys.add(e.key)
|
||||
action = actions[e.key]
|
||||
action()
|
||||
if (REPEATABLE_ACTIONS.includes(action)) {
|
||||
actionsToRepeat.unshift(action)
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
if (actionsToRepeat == softDrop)
|
||||
scheduler.setInterval(autorepeat, FALL_DELAY / 20)
|
||||
else
|
||||
scheduler.setTimeout(autorepeat, AUTOREPEAT_DELAY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function keyUpHandler(e) {
|
||||
if (e.key in actions) {
|
||||
pressedKeys.delete(e.key)
|
||||
action = actions[e.key]
|
||||
if (actionsToRepeat.includes(action)) {
|
||||
actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1)
|
||||
if (!actionsToRepeat.length) {
|
||||
scheduler.clearTimeout(autorepeat)
|
||||
scheduler.clearInterval(autorepeat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveLeft() {
|
||||
move(MOVEMENT.LEFT);
|
||||
}
|
||||
|
||||
function moveRight() {
|
||||
move(MOVEMENT.RIGHT)
|
||||
}
|
||||
|
||||
function softDrop() {
|
||||
if (move(MOVEMENT.DOWN))
|
||||
stats.score++
|
||||
}
|
||||
|
||||
function hardDrop() {
|
||||
scheduler.clearTimeout(lockPhase)
|
||||
scheduler.clearTimeout(locksDown)
|
||||
while(move(MOVEMENT.DOWN, false)) {
|
||||
stats.score += 2
|
||||
}
|
||||
locksDown()
|
||||
}
|
||||
|
||||
function rotateCW() {
|
||||
rotate(SPIN.CW)
|
||||
}
|
||||
|
||||
function rotateCCW() {
|
||||
rotate(SPIN.CCW)
|
||||
}
|
||||
|
||||
function hold() {
|
||||
if (this.matrix.piece.holdEnabled) {
|
||||
this.matrix.piece.holdEnabled = false
|
||||
scheduler.clearInterval(move)
|
||||
scheduler.clearInterval(locksDown)
|
||||
var shape = this.matrix.piece.shape
|
||||
this.matrix.piece = this.holdQueue.piece
|
||||
this.holdQueue.piece = new Tetromino(HELD_PIECE_POSITION, shape)
|
||||
this.generationPhase(this.matrix.piece)
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
holdQueue.draw()
|
||||
stats.print()
|
||||
matrix.draw()
|
||||
nextQueue.draw()
|
||||
|
||||
if (state != STATE.GAME_OVER)
|
||||
requestAnimationFrame(draw)
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
holdQueue = new HoldQueue(document.getElementById("hold").getContext("2d"))
|
||||
stats = new Stats(document.getElementById("stats-values"))
|
||||
matrix = new Matrix(document.getElementById("matrix").getContext("2d"))
|
||||
nextQueue = new NextQueue(document.getElementById("next").getContext("2d"))
|
||||
scheduler = new Scheduler()
|
||||
|
||||
actions = {
|
||||
"ArrowLeft": moveLeft,
|
||||
"ArrowRight": moveRight,
|
||||
"ArrowDown": softDrop,
|
||||
" ": hardDrop,
|
||||
"ArrowUp": rotateCW,
|
||||
"z": rotateCCW,
|
||||
"c": hold
|
||||
}
|
||||
pressedKeys = new Set()
|
||||
actionsToRepeat = []
|
||||
addEventListener("keydown", keyDownHandler, false)
|
||||
addEventListener("keyup", keyUpHandler, false)
|
||||
requestAnimationFrame(draw)
|
||||
|
||||
state = STATE.PLAYING
|
||||
this.newLevel(1)
|
||||
}
|
16
leaderboard.js
Normal file
@ -0,0 +1,16 @@
|
||||
const DEFAULT_THEME = "default"
|
||||
|
||||
function applyTheme() {
|
||||
var link = document.createElement('link')
|
||||
link.id = "theme";
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
link.href = `themes/'${themeName}/style.css`
|
||||
link.media = 'all'
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
themeName = localStorage.getItem("themeName") || DEFAULT_THEME
|
||||
loadTheme()
|
||||
}
|
35
leaderboard.php
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Meilleurs scores - Webtris</title>
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<script type="text/javascript" src="leaderboard.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>WEBTRIS</h1>
|
||||
</header>
|
||||
<table id="leaderboard">
|
||||
<caption>MEILLEURS SCORES</caption>
|
||||
<?php
|
||||
include "db_connect.php";
|
||||
try {
|
||||
$db = new PDO("mysql:host=$DB_HOST;dbname=$DB_NAME;charset=utf8", $DB_USER, $DB_PASSWORD);
|
||||
} catch (Exception $e) {
|
||||
die('Erreur : ' . $e->getMessage());
|
||||
}
|
||||
$top10 = $db->query('SELECT player, score FROM `leaderboard` ORDER BY score DESC LIMIT 20;');
|
||||
for ($i = 1; $row = $top10->fetch(); $i++) {
|
||||
$score = number_format($row['score'], 0, ",", " ");
|
||||
echo ' <tr><th class="name">' . $i . '<td class="player">' . $row['player'] . '</td><td class="value">' . $score . "</td></tr>\n";
|
||||
}
|
||||
$top10 = null;
|
||||
$db = null;
|
||||
?>
|
||||
</table>
|
||||
<footer>
|
||||
<button onclick="window.close()">Fermer</button>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
32
manifest.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"short_name": "Webtris",
|
||||
"name": "Webtris",
|
||||
"description": "Falling blocks",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "16x16"
|
||||
}
|
||||
],
|
||||
"start_url": "index.php",
|
||||
"background_color": "#222",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"theme_color": "#222",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Webtris",
|
||||
"short_name": "Webtris",
|
||||
"description": "Falling blocks",
|
||||
"url": "index.php",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "16x16"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
25
publish.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
include "db_connect.php";
|
||||
if (isset($_POST['player']) && isset($_POST['score'])) {
|
||||
try {
|
||||
$db = new PDO("mysql:host=$DB_HOST;dbname=$DB_NAME;charset=utf8", $DB_USER, $DB_PASSWORD);
|
||||
} catch (Exception $e) {
|
||||
die('Erreur : ' . $e->getMessage());
|
||||
}
|
||||
$query = $db->prepare('INSERT INTO `leaderboard` (`player`, `score`) VALUES (:player, :score);');
|
||||
$query->execute(array(
|
||||
"player" => strip_tags($_POST['player']),
|
||||
"score" => (int) $_POST['score']
|
||||
));
|
||||
|
||||
$RowsToDelete = $db->query('SELECT id FROM `leaderboard` ORDER BY score DESC LIMIT 20, 1000;');
|
||||
while($row = $RowsToDelete->fetch()) {
|
||||
$id = $row['id'];
|
||||
$db->exec("DELETE FROM `leaderboard` WHERE id=" . $row['id'] . ";");
|
||||
}
|
||||
$row->closeCursor();
|
||||
$db->close();
|
||||
} else {
|
||||
header($_SERVER["SERVER_PROTOCOL"] . " 405 Method Not Allowed", true, 405);
|
||||
}
|
||||
?>
|
BIN
themes/Effect/fonts/ShareTech.woff2
Normal file
BIN
themes/Effect/images/bg.jpg
Normal file
After Width: | Height: | Size: 368 KiB |
231
themes/Effect/style.css
Normal file
@ -0,0 +1,231 @@
|
||||
@font-face {
|
||||
font-family: 'Share Tech';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Share Tech Regular'), local('ShareTech-Regular'), url(fonts/ShareTech.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Share Tech Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Share Tech Mono Regular'), local('ShareTechMono-Regular'), url(fonts/ShareTechMono.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Share Tech';
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background-color: #0D111D;
|
||||
background-image: url("images/bg.jpg");
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#settingsSection {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px white solid;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
margin: 1vmin auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
legend, label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
fieldset > div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-column-gap: 2em;
|
||||
grid-row-gap: 1em;
|
||||
justify-items: right;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
fieldset > div > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#themePreviewTable {
|
||||
grid-column: 1 / 5;
|
||||
width: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#gameSection div {
|
||||
display: grid;
|
||||
grid-gap: 3vmin;
|
||||
margin: -6vmin 0 auto 0;
|
||||
}
|
||||
|
||||
#holdTable {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
#matrixTable {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
#messageSpan {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 4vmin;
|
||||
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#nextTable {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
|
||||
.minoes-table {
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
margin: -6vmin 0 auto 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
height: 3vmin;
|
||||
width: 3vmin;
|
||||
}
|
||||
|
||||
th {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
td {
|
||||
background: transparent;
|
||||
border: 1px inset rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
.mino {
|
||||
background: rgba(127, 229, 255, 0.3);
|
||||
border: 1px solid rgba(127, 229, 255, 0.7);
|
||||
border-radius: 0.3vmin;
|
||||
}
|
||||
|
||||
.locked-mino {
|
||||
background: rgba(242, 255, 255, 0.5);
|
||||
border-color: rgba(242, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.cleared-line {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.trail {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.3vmin;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 0.3vmin;
|
||||
}
|
||||
|
||||
#statsTable {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
height: 0;
|
||||
justify-self: end;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#statsTable td {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer > * {
|
||||
margin: 1em auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: lightcyan;
|
||||
}
|
||||
|
||||
#credits {
|
||||
width: 100%;
|
||||
font-size: 0.8em;
|
||||
gap: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#leaderboard {
|
||||
min-width: 25%;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
border-top: 1px solid white;
|
||||
caption-side: top;
|
||||
border-spacing: 1em 0.2em;
|
||||
}
|
||||
|
||||
#leaderboard caption {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#leaderboard tr,
|
||||
#leaderboard td {
|
||||
border: 0 !important;
|
||||
margin: auto 10em;
|
||||
}
|
||||
|
||||
#leaderboard td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#leaderboard td:last-child {
|
||||
text-align: right;
|
||||
}
|
BIN
themes/Kubow/images/blue_block.png
Normal file
After Width: | Height: | Size: 385 B |
BIN
themes/Kubow/images/green_block.png
Normal file
After Width: | Height: | Size: 404 B |
BIN
themes/Kubow/images/navy_block.png
Normal file
After Width: | Height: | Size: 377 B |
BIN
themes/Kubow/images/peach_block.png
Normal file
After Width: | Height: | Size: 335 B |
BIN
themes/Kubow/images/pink_block.png
Normal file
After Width: | Height: | Size: 374 B |
BIN
themes/Kubow/images/purple_block.png
Normal file
After Width: | Height: | Size: 387 B |
BIN
themes/Kubow/images/yellow_block.png
Normal file
After Width: | Height: | Size: 371 B |
300
themes/Kubow/style.css
Normal file
@ -0,0 +1,300 @@
|
||||
/* source: https://github.com/kubowania/Tetris */
|
||||
|
||||
:root {
|
||||
/* default font size in browsers is 16px = 1em, we make
|
||||
things easier for us and make 10px our base size.
|
||||
We have 10/16 = 0.625 = 1rem as it is set on root element.
|
||||
So 1rem is now 10px throughout our stylesheet.*/
|
||||
font-size: 0.625em;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-size: 1.6rem;
|
||||
margin: auto;
|
||||
max-width: 60rem;
|
||||
color: #d8edea;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(175, 196, 174, 1) 0%,
|
||||
rgba(104, 204, 191, 1) 89%,
|
||||
rgba(94, 191, 178, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
letter-spacing: 1.5rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#settingsSection {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
fieldset, #statsTable, #leaderboard {
|
||||
margin-top: 1rem;
|
||||
background: #f0f0f0;
|
||||
background-image: linear-gradient(#d0d0d0, #f0f0f0);
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) inset, 0 1px 0 #fff;
|
||||
color: #85796b;
|
||||
}
|
||||
|
||||
legend, #leaderboard caption {
|
||||
font-size: 0.9em;
|
||||
color: #d8edea;
|
||||
transform: translate(0, -0.6em);
|
||||
}
|
||||
|
||||
fieldset > div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-column-gap: 1em;
|
||||
grid-row-gap: 1em;
|
||||
justify-items: right;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
fieldset > div > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="number"],
|
||||
select {
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
width: 22rem;
|
||||
height: 2.2rem;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
line-height: 23px;
|
||||
font-size: 10px;
|
||||
display: block;
|
||||
text-shadow: -1px -1px 0 #a84155;
|
||||
background: #d25068;
|
||||
border: 1px solid #d25068;
|
||||
width: 12rem;
|
||||
background-image: linear-gradient(to bottom, #f66c7b, #d25068);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) inset,
|
||||
0 -1px 0 rgba(255, 255, 255, 0.1) inset, 0 4px 0 #ad4257,
|
||||
0 4px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
button:before,
|
||||
input[type="number"]:before,
|
||||
select:before {
|
||||
background: #f0f0f0;
|
||||
background-image: linear-gradient(#d0d0d0, #f0f0f0);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) inset, 0 1px 0 #fff;
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: -6px;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
bottom: -10px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
button:active,
|
||||
select:active {
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) inset,
|
||||
0 -1px 0 rgba(255, 255, 255, 0.1) inset;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
button:active:before,
|
||||
input[type="number"]:active:before,
|
||||
select:active:before {
|
||||
top: -11px;
|
||||
bottom: -5px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
button:hover,
|
||||
input[type="number"]:hover,
|
||||
select:hover {
|
||||
background: #f66c7b;
|
||||
background-image: linear-gradient(top, #d25068, #f66c7b);
|
||||
}
|
||||
|
||||
#themePreviewTable {
|
||||
grid-column: 1 / 5;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#gameSection div {
|
||||
display: grid;
|
||||
grid-gap: 3vmin;
|
||||
}
|
||||
|
||||
#holdTable {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
#matrixTable {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
#messageSpan {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 4vmin;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px rgba(153, 145, 175, 0.5);
|
||||
}
|
||||
|
||||
#nextTable {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
|
||||
.minoes-table {
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
th, td {
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.I {
|
||||
background-image: url(images/blue_block.png);
|
||||
}
|
||||
|
||||
.J {
|
||||
background-image: url(images/navy_block.png);
|
||||
}
|
||||
|
||||
.L {
|
||||
background-image: url(images/peach_block.png);
|
||||
}
|
||||
|
||||
.O {
|
||||
background-image: url(images/yellow_block.png);
|
||||
}
|
||||
|
||||
.S {
|
||||
background-image: url(images/green_block.png);
|
||||
}
|
||||
|
||||
.T {
|
||||
background-image: url(images/purple_block.png);
|
||||
}
|
||||
|
||||
.Z {
|
||||
background-image: url(images/pink_block.png);
|
||||
}
|
||||
|
||||
.locked {
|
||||
background: #f0f0f0;
|
||||
border: 3px outset rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.cleared-line {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.trail {
|
||||
border-top: 0 !important;
|
||||
border-bottom: 0 !important;
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.2) !important;
|
||||
border-right: 3px solid rgba(125, 125, 125, 0.2) !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 3px outset rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
#statsTable {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
color: rgb(133, 121, 107, 0.5);
|
||||
padding-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#statsTable td {
|
||||
text-align: center;
|
||||
border: 0;
|
||||
font-size: 2.3rem;
|
||||
color: rgb(133, 121, 107, 0.5);
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 1em;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
#credits {
|
||||
font-size: 0.8em;
|
||||
gap: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#leaderboard {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#leaderboard td {
|
||||
margin: auto;
|
||||
padding-left: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#leaderboard td:first-child{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#leaderboard td:last-child {
|
||||
text-align: right;
|
||||
}
|
246
themes/default/style.css
Normal file
@ -0,0 +1,246 @@
|
||||
body {
|
||||
margin: auto;
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
background: #222;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #444;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 0.9em;
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
fieldset > div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-column-gap: 2em;
|
||||
grid-row-gap: 1em;
|
||||
justify-items: right;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
fieldset > div > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#themePreviewTable {
|
||||
grid-column: 1 / 5;
|
||||
width: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#gameSection div {
|
||||
display: grid;
|
||||
grid-gap: 3vmin;
|
||||
margin: -3vmin 0 auto 0;
|
||||
}
|
||||
|
||||
#holdTable {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
#matrixTable {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
#messageSpan {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 4vmin;
|
||||
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
@keyframes message-fade-out {
|
||||
from {opacity: 1;}
|
||||
to {opacity: 0;}
|
||||
}
|
||||
.messageSpan-fade-out {
|
||||
animation: message-fade-out 500ms 500ms;
|
||||
}
|
||||
|
||||
#nextTable {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
|
||||
.minoes-table {
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
margin: -6vmin 0 auto 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
height: 3vmin;
|
||||
width: 3vmin;
|
||||
}
|
||||
|
||||
th {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.mino {
|
||||
border: 1px solid;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.I { /* cyan */
|
||||
background: #99d9ea;
|
||||
border-color: #d1edf5;
|
||||
}
|
||||
|
||||
.J { /* blue */
|
||||
background: #7f92ff;
|
||||
border-color: #c2cbff;
|
||||
}
|
||||
|
||||
.L { /* orange */
|
||||
background: #ffb27f;
|
||||
border-color: #ffe1cd;
|
||||
}
|
||||
|
||||
.O { /* yellow */
|
||||
background: #ffe97f;
|
||||
border-color: #fff5ca;
|
||||
}
|
||||
|
||||
.S { /* green */
|
||||
background: #7fff8e;
|
||||
border-color: #ccffd2;
|
||||
}
|
||||
|
||||
.T { /* magenta */
|
||||
background: #d67fff;
|
||||
border-color: #edc9ff;
|
||||
}
|
||||
|
||||
.Z { /* red */
|
||||
background: #ff7f7f;
|
||||
border-color: #ffdada;
|
||||
}
|
||||
|
||||
.locked-mino {
|
||||
background: #AAA;
|
||||
border: 1px solid #CCC;
|
||||
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 {
|
||||
animation: mino-fade-out 200ms;
|
||||
background: transparent;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: #555;
|
||||
border: 1px solid #666;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
|
||||
#statsTable {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
height: 0;
|
||||
justify-self: end;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#statsTable td {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
footer > * {
|
||||
margin: 1em auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#credits {
|
||||
width: 100%;
|
||||
font-size: 0.8em;
|
||||
gap: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#leaderboard {
|
||||
min-width: 25%;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
border-top: 1px solid white;
|
||||
caption-side: top;
|
||||
border-spacing: 1em 0.2em;
|
||||
}
|
||||
|
||||
#leaderboard caption {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#leaderboard tr,
|
||||
#leaderboard td {
|
||||
border: 0 !important;
|
||||
margin: auto 10em;
|
||||
}
|
||||
|
||||
#leaderboard td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#leaderboard td:last-child {
|
||||
text-align: right;
|
||||
}
|
BIN
thumbnail.png
Normal file
After Width: | Height: | Size: 96 KiB |
51
webtris.sql
Normal file
@ -0,0 +1,51 @@
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
SET AUTOCOMMIT = 0;
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Database: `webtris`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `leaderboard`
|
||||
--
|
||||
|
||||
CREATE TABLE `leaderboard` (
|
||||
`id` int(11) NOT NULL,
|
||||
`player` varchar(20) NOT NULL,
|
||||
`score` int(11) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indexes for table `leaderboard`
|
||||
--
|
||||
ALTER TABLE `leaderboard`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `leaderboard`
|
||||
--
|
||||
ALTER TABLE `leaderboard`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|