From 06a2e582fd48ffa27b467207da42171a8150413e Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 18 Jul 2019 11:49:06 +0200 Subject: [PATCH] Major rework of basic game code - pre-work for multiplayer Includes automatic generation of required HTML elements and better responsiveness --- index.html | 34 ++++-- js/arena.js | 7 +- js/game-info.js | 70 +++++++++--- js/game.js | 105 +++++++++-------- js/helper-functions.js | 6 + js/language.js | 11 ++ js/local-player.js | 21 ++++ js/menu.js | 173 ++++++++++------------------ js/player.js | 20 +--- js/tetris-manager.js | 56 +++++++++ js/tetris.js | 52 ++------- js/theme.js | 110 ++++++++++++++++++ lang.json | 70 ++++++++++++ style.css | 249 +++++++++++++++++++++++++---------------- 14 files changed, 627 insertions(+), 357 deletions(-) create mode 100644 js/helper-functions.js create mode 100644 js/local-player.js create mode 100644 js/tetris-manager.js create mode 100644 js/theme.js create mode 100644 lang.json diff --git a/index.html b/index.html index 7713ef4..aae56a6 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ - + @@ -70,16 +70,24 @@ -
-
0
-
00:00
-
- - - - + + + +
+
+

Tetris.js

@@ -93,12 +101,16 @@
+ + + + - + - \ No newline at end of file + diff --git a/js/arena.js b/js/arena.js index 175d3bf..7db484c 100644 --- a/js/arena.js +++ b/js/arena.js @@ -23,14 +23,9 @@ class Arena { this.field.unshift(row); ++y; - this.p.score += rowCount * 10; + this.p.score += rowCount * 20; rowCount *= 2; } - if (this.p.score - this.g.prevUpdateScore > 50) { - this.g.dropInterval -= 20; - this.g.dropInterval = this.g.dropInterval > 100 ? this.g.dropInterval : 100; - this.g.prevUpdateScore = this.p.score; - } this.game.drawArena(); } } \ No newline at end of file diff --git a/js/game-info.js b/js/game-info.js index d6de3db..f4d1992 100644 --- a/js/game-info.js +++ b/js/game-info.js @@ -3,19 +3,43 @@ class GameInfo { this.fieldSize = {x: 12, y: 20}; this.arena = new Arena(this, game); - this.player = new Player(this, game); + this.player = new LocalPlayer(this, game); - this.canvas = document.getElementById('tetris'); - this.context = this.canvas.getContext('2d'); + const container = document.createElement('div'); + container.className = 'game-instance'; + manager.container.appendChild(container); - this.canvasBg = document.getElementById('tetris-background'); - this.contextBg = this.canvasBg.getContext('2d'); + this.score = document.createElement('div'); + this.score.classList.add('game-stats', 'score'); + container.appendChild(this.score); - this.canvasHold = document.getElementById('tetris-hold'); + this.canvasContainer = document.createElement('div'); + this.canvasContainer.className = 'canvas-container'; + container.appendChild(this.canvasContainer); + + this.canvasHold = document.createElement('canvas'); + this.canvasHold.className = 'tetris-hold'; this.contextHold = this.canvasHold.getContext('2d'); + this.canvasContainer.appendChild(this.canvasHold); - this.canvasUpcoming = document.getElementById('tetris-upcoming'); + this.canvasBg = document.createElement('canvas'); + this.canvasBg.className = 'tetris-background'; + this.contextBg = this.canvasBg.getContext('2d'); + this.canvasContainer.appendChild(this.canvasBg); + + this.canvas = document.createElement('canvas'); + this.canvas.className = 'tetris-arena'; + this.context = this.canvas.getContext('2d'); + this.canvasContainer.appendChild(this.canvas); + + this.canvasUpcoming = document.createElement('canvas'); + this.canvasUpcoming.className = 'tetris-upcoming'; this.contextUpcoming = this.canvasUpcoming.getContext('2d'); + this.canvasContainer.appendChild(this.canvasUpcoming); + + this.time = document.createElement('div'); + this.time.classList.add('game-stats', 'time'); + container.appendChild(this.time); this.isPaused = true; @@ -25,28 +49,32 @@ class GameInfo { this.keys = { down: { keys: [40, 83], - action: () => this.player.drop() + action: () => { + this.player.drop(); + this.player.score++; + this.player.game.updateScore(false); + }, }, left: { keys: [37, 65], - action: () => this.player.move(-1) + action: () => this.player.move(-1), }, right: { keys: [39, 68], - action: () => this.player.move(1) + action: () => this.player.move(1), }, rotateLeft: { keys: [81], - action: () => this.player.rotate(-1) + action: () => this.player.rotate(-1), }, rotateRight: { keys: [69], - action: () => this.player.rotate(1) + action: () => this.player.rotate(1), }, holdTile: { keys: [38, 87], - action: () => this.player.hold() - } + action: () => this.player.hold(), + }, }; this.prevUpdateScore = 0; @@ -65,6 +93,16 @@ class GameInfo { modern -> rounded corners snake -> all tiles are connected */ - this.theme = 'default'; + this.theme = new DefaultTheme(); } -} \ No newline at end of file + + updateScore(animate) { + this.score.innerText = this.lastScore; + if (animate) { + this.score.classList.add('update'); + setTimeout(() => { + this.score.classList.remove('update'); + }, 1000); + } + } +} diff --git a/js/game.js b/js/game.js index c47e19b..b279589 100644 --- a/js/game.js +++ b/js/game.js @@ -38,54 +38,14 @@ class Game { } drawTile(x, y, offset, color, matrix, ctx = this.g.context) { - ctx.fillStyle = color; x += offset.x; y += offset.y; - switch (this.g.theme) { - case "default": - ctx.fillRect(x + tileGap / 2, y + tileGap / 2, 1 - tileGap, 1 - tileGap); - break; - case "clean": - ctx.fillRect(x, y, 1, 1); - break; - case "modern": - drawRoundRect(ctx, x + tileGap / 2, y + tileGap / 2, 1 - tileGap, 1 - tileGap, .15); - break; - case "snakes": - let r1 = .15, // top right - r2 = .15, // bottom right - r3 = .15, // bottom left - r4 = .15; // top left - // Is there a tile to the left? - if (matrix[y][x - 1] > 0) { - r3 = 0; - r4 = 0; - } - // Is there a tile to the right? - if (matrix[y][x + 1] > 0) { - r1 = 0; - r2 = 0; - } - // Is there a tile to the top? - if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) { - r1 = 0; - r4 = 0; - } - // Is there a tile to the bottom? - if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) { - r2 = 0; - r3 = 0; - } - drawRoundRect(ctx, x, y, 1, 1, [r1, r2, r3, r4]); - break; - case "retro": - drawReliefRect(ctx, x, y, 1, 1, .15, color); - break; - default: - this.g.theme = "default"; - this.drawTile(x, y, offset, color, matrix, ctx); - break; - } + + ctx.save(); + ctx.translate(x, y); + + this.g.theme.drawTile(color, matrix, ctx, x, y); + ctx.restore(); } drawUpcoming() { @@ -141,9 +101,42 @@ class Game { }); } + rescale() { + let conWidth = manager.container.clientWidth / manager.instances.size; + let conHeight = manager.container.clientHeight - 78; + + if (conHeight < conWidth) { + conWidth = conHeight; + } + + conWidth = Math.floor(conWidth); + + const canvasScale = Math.floor(conWidth * .6 / this.g.fieldSize.x); + const canvasHoldScale = Math.floor(conWidth * .2 / 6); + + const realWidth = canvasScale * this.g.fieldSize.x + 2 * canvasHoldScale * 6; + const realHeight = canvasScale * this.g.fieldSize.y; + + this.g.canvasContainer.style.width = realWidth + 'px'; + this.g.canvasContainer.style.height = realHeight + 'px'; + + this.g.canvasUpcoming.style.height = this.g.canvasUpcoming.clientWidth * 3 + 'px'; + this.g.canvasHold.style.height = this.g.canvasHold.clientWidth + 'px'; + + this.g.canvasBg.adjustResolution(this.g.contextBg, canvasScale); + this.g.canvas.adjustResolution(this.g.context, canvasScale); + this.g.canvasUpcoming.adjustResolution(this.g.contextUpcoming, canvasHoldScale); + this.g.canvasHold.adjustResolution(this.g.contextHold, canvasHoldScale); + + if (!firstRun && this.g.isPaused) { + this.draw(); + } + this.redrawScreen(); + } + saveHighscore() { - if (getCookie("highscore").value < this.p.score) { - document.cookie = "highscore=" + this.p.score + "; max-age=" + 60 * 60 * 24 * 365 * 1000 + "; path=/;"; + if (getCookie('highscore') && getCookie('highscore').value < this.p.score) { + document.cookie = 'highscore=' + this.p.score + '; max-age=' + 60 * 60 * 24 * 365 * 1000 + '; path=/;'; } } @@ -175,18 +168,24 @@ class Game { } } - updateScore() { + updateScore(animate = true) { if (this.g.lastScore !== this.p.score) { - scoreUpdateAni(); + this.g.updateScore(animate); this.g.lastScore = this.p.score; this.saveHighscore(); + + if (this.p.score - this.p.lastLevelScore > 500) { + this.p.lastLevelScore = this.p.score; + this.p.level++; + this.g.dropInterval *= .9; + } } - document.getElementById('score').innerText = this.p.score.toString(); + this.g.updateScore(false); } updateTime() { timePassed += Date.now() - this.g.lastTimeUpdate; - timeElement.innerHTML = formatMillis(timePassed); + this.g.time.innerText = formatMillis(timePassed); this.g.lastTimeUpdate = Date.now(); } -} \ No newline at end of file +} diff --git a/js/helper-functions.js b/js/helper-functions.js new file mode 100644 index 0000000..a72c22a --- /dev/null +++ b/js/helper-functions.js @@ -0,0 +1,6 @@ +HTMLCanvasElement.prototype.adjustResolution = function (ctx, scale) { + this.width = this.clientWidth; + this.height = this.clientHeight; + + ctx.scale(scale, scale); +}; diff --git a/js/language.js b/js/language.js index 13a038d..6de5361 100644 --- a/js/language.js +++ b/js/language.js @@ -53,6 +53,17 @@ let firstRun = true; class Language { constructor(lang) { + const xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function () { + if (this.readyState === 4 && this.status === 200) { + const obj = JSON.parse(this.responseText); + console.log(JSON.stringify(obj)); + } + }; + + xmlhttp.open("GET", "/lang.json", true); + xmlhttp.send(); + this.lang = lang; if(eval('typeof ' + this.lang) === 'undefined') this.lang = "en"; diff --git a/js/local-player.js b/js/local-player.js new file mode 100644 index 0000000..c238914 --- /dev/null +++ b/js/local-player.js @@ -0,0 +1,21 @@ +class LocalPlayer extends Player { + constructor(gameInfo, game) { + super(gameInfo, game); + + this.isHolding = false; + this.holdingTile = null; + } + + hold() { + if (this.isHolding) + return; + if (this.holdingTile === null) { + this.holdingTile = this.matrix; + this.reset(true); + } else { + this.holdingTile = [this.matrix, this.matrix = this.holdingTile][0]; + this.reset(true, false); + } + this.game.drawHolding(); + } +} diff --git a/js/menu.js b/js/menu.js index 9c9d6b5..0ed53c0 100644 --- a/js/menu.js +++ b/js/menu.js @@ -6,69 +6,19 @@ let escState = 1; window.onresize = () => { - scaleWindow(); - game.redrawScreen(); + manager.callAll('rescale', []); }; -function scaleWindow() { - const canvasContainer = document.getElementById("canvas-container"); - let height = .8 * window.innerHeight - 40; - let width = height / (5 / 3); - let conWidth = width + (2 * (height / game.g.fieldSize.y * 5)); - const ratio = width / conWidth; - - if (conWidth > window.innerWidth * .8) { - conWidth = window.innerWidth * .8; - width = conWidth * ratio; - height = width * (5 / 3); - } - - width = Math.floor(width); - height = Math.floor(height); - - canvasContainer.style.height = height + "px"; - canvasContainer.style.width = conWidth + "px"; - - const canvasScale = width / game.g.fieldSize.x; - - game.g.canvasBg.height = height; - game.g.canvasBg.width = width; - game.g.contextBg.scale(canvasScale, canvasScale); - - game.g.canvas.height = height; - game.g.canvas.width = width; - game.g.context.scale(canvasScale, canvasScale); - - game.g.canvasHold.height = game.g.canvasHold.width = height / game.g.fieldSize.y * 5; - game.g.canvasHold.style.transform = "translate(-100%, -.2em) translateX(-" + width / 2 + "px)"; - const contextHoldScale = Math.floor(game.g.canvasHold.width / 6); - game.g.contextHold.scale(contextHoldScale, contextHoldScale); - - game.g.canvasUpcoming.width = height / game.g.fieldSize.y * 5; - game.g.canvasUpcoming.height = game.g.canvasUpcoming.width * 3; - game.g.canvasUpcoming.style.transform = "translate(100%, -.2em) translateX(" + width / 2 + "px)"; - const contextUpcomingScale = Math.floor(game.g.canvasUpcoming.width / 6); - game.g.contextUpcoming.scale(contextUpcomingScale, contextUpcomingScale); - - if (!firstRun && game.g.isPaused) { - game.draw(); - } -} - -scaleWindow(); - -document.addEventListener("keydown", (event) => { - if(event.keyCode === 32) { - if(firstRun) { +document.addEventListener('keyup', (event) => { + console.log(event.key); + if (event.key === ' ') { + if (firstRun) { initGame(); } else { - if (!game.g.isPaused) { - showMenu(); - } else { - hideMenu(); - } + escState = escState === 0 ? 1 : 0; + toggleMenu(); } - } else if(event.keyCode === 27) { + } else if (event.keyCode === 27) { escState++; if (firstRun && escState % 3 === 0) { escState++; @@ -78,15 +28,15 @@ document.addEventListener("keydown", (event) => { } }); -document.getElementById("game-play").addEventListener("click", () => { - if(firstRun) { +document.getElementById('game-play').addEventListener('click', () => { + if (firstRun) { initGame(); } else { hideMenu(); } }); -document.getElementById("game-reset").addEventListener("click", () => { +document.getElementById('game-reset').addEventListener('click', () => { firstRun = true; game.clearScreen(); hideMenu(); @@ -94,17 +44,21 @@ document.getElementById("game-reset").addEventListener("click", () => { showMenu(); }); -document.getElementsByName("theme").forEach((el) => { - el.addEventListener("change", (e) => { - game.g.theme = e.target.getAttribute("data-theme"); - game.redrawScreen(); +document.getElementsByName('theme').forEach((el) => { + el.addEventListener('change', (e) => { + const themeName = e.target.getAttribute('data-theme'); + const theme = themes[themeName]; + manager.callAll(instance => { + instance.g.theme = theme; + instance.redrawScreen(); + }); }); }); let isActive = false; -const menuButton = document.getElementById("menu-opener"); +const menuButton = document.getElementById('menu-opener'); -menuButton.addEventListener("click", () => { +menuButton.addEventListener('click', () => { toggleSettings(); }); @@ -112,120 +66,109 @@ function toggleSettings() { if (isActive) { escState = 1; menuButton.classList.remove('active'); - document.getElementsByTagName('body')[0].classList.remove('menu-open'); + document.body.classList.remove('menu-open'); } else { escState = 2; menuButton.classList.add('active'); - document.getElementsByTagName('body')[0].classList.add('menu-open'); + document.body.classList.add('menu-open'); } isActive = !isActive; } function toggleMenu() { if (escState === 0) { - document.getElementsByTagName("body")[0].classList.remove("menu-open"); + document.body.classList.remove('menu-open'); menuButton.classList.remove('active'); hideMenu(); } else if (escState === 1) { - document.getElementsByTagName("body")[0].classList.remove("menu-open"); + document.body.classList.remove('menu-open'); menuButton.classList.remove('active'); showMenu(); } else { - document.getElementsByTagName("body")[0].classList.add("menu-open"); + document.body.classList.add('menu-open'); menuButton.classList.add('active'); } } function fadeBlurIn() { - const blurEl = document.getElementById("f1").children[0]; + const blurEl = document.getElementById('f1').children[0]; const finalVal = 15; let currentVal = 0; - const id = setInterval(frame, 16); + const interval = 1000 / 60; + const id = setInterval(frame, interval); function frame() { - if(currentVal >= finalVal) { + if (currentVal >= finalVal) { clearInterval(id); } else { - currentVal += 1.6; - blurEl.setAttribute("stdDeviation", currentVal); + currentVal += 0.5; + blurEl.setAttribute('stdDeviation', currentVal); } } setTimeout(() => { if (currentVal < finalVal) { - blurEl.setAttribute("stdDeviation", finalVal); + blurEl.setAttribute('stdDeviation', finalVal); clearInterval(id); - console.log("Performance Issues: system couldn't hold up"); + console.log('Performance Issues: system couldn\'t hold up'); } }, 1000); } function fadeBlurOut() { - const blurEl = document.getElementById("f1").children[0]; + const blurEl = document.getElementById('f1').children[0]; const finalVal = 0; let currentVal = 15; - const id = setInterval(frame, 16); + const interval = 1000 / 60; + const id = setInterval(frame, interval); function frame() { - if(currentVal <= finalVal) { + if (currentVal <= finalVal) { clearInterval(id); } else { - currentVal -= 1.6; - blurEl.setAttribute("stdDeviation", currentVal); + currentVal -= 0.5; + blurEl.setAttribute('stdDeviation', currentVal); } } setTimeout(() => { - blurEl.setAttribute("stdDeviation", finalVal); + blurEl.setAttribute('stdDeviation', finalVal); clearInterval(id); if (currentVal < finalVal) { - console.log("Performance Issues: system couldn't hold up"); + console.log('Performance Issues: system couldn\'t hold up'); } }, 1000); } -const scoreEl = document.getElementById("score"); -const nativeTransform = getComputedStyle(scoreEl).transform; -function scoreUpdateAni() { - scoreEl.classList.add("update"); - setTimeout(() => { - scoreEl.classList.remove("update"); - }, 1500); -} - function showMenu() { - game.g.isPaused = true; + manager.pause(); escState = 1; - document.getElementById("game-title").style.display = "block"; - document.getElementById("game-play").style.display = "block"; - document.getElementById("game-reset").style.display = "block"; + document.getElementById('game-title').style.display = 'block'; + document.getElementById('game-play').style.display = 'block'; + document.getElementById('game-reset').style.display = 'block'; - document.getElementById("game-title").style.opacity = "1"; - document.getElementById("game-play").style.opacity = "1"; + document.getElementById('game-title').style.opacity = '1'; + document.getElementById('game-play').style.opacity = '1'; fadeBlurIn(); - if(!firstRun) { - document.getElementById("game-reset").style.opacity = "1"; + if (!firstRun) { + document.getElementById('game-reset').style.opacity = '1'; } } function hideMenu() { - game.g.isPaused = false; + manager.resume(); escState = 0; - document.getElementById("game-title").style.opacity = "0"; - document.getElementById("game-play").style.opacity = "0"; - document.getElementById("game-reset").style.opacity = "0"; + document.getElementById('game-title').style.opacity = '0'; + document.getElementById('game-play').style.opacity = '0'; + document.getElementById('game-reset').style.opacity = '0'; setTimeout(() => { - document.getElementById("game-title").style.display = "none"; - document.getElementById("game-play").style.display = "none"; - document.getElementById("game-reset").style.display = "none"; + document.getElementById('game-title').style.display = 'none'; + document.getElementById('game-play').style.display = 'none'; + document.getElementById('game-reset').style.display = 'none'; }, 500); - game.g.lastTimeUpdate = Date.now(); fadeBlurOut(); - if(!firstRun) { - game.update(game.g.lastTime); - } } function initGame() { @@ -234,4 +177,4 @@ function initGame() { firstRun = false; switchLang(currentLang); -} \ No newline at end of file +} diff --git a/js/player.js b/js/player.js index 56447da..3be6835 100644 --- a/js/player.js +++ b/js/player.js @@ -6,9 +6,8 @@ class Player { this.pos = {x: 0, y: 0}; this.matrix = null; this.score = 0; - - this.isHolding = false; - this.holdingTile = null; + this.level = 1; + this.lastLevelScore = 0; this.a.p = this; } @@ -29,19 +28,6 @@ class Player { this.g.dropCounter = 0; } - hold() { - if (this.isHolding) - return; - if (this.holdingTile === null) { - this.holdingTile = this.matrix; - this.reset(true); - } else { - this.holdingTile = [this.matrix, this.matrix = this.holdingTile][0]; - this.reset(true, false); - } - this.game.drawHolding(); - } - move(dir) { this.pos.x += dir; if (collide(this.a.field, this)) { @@ -83,4 +69,4 @@ class Player { } } } -} \ No newline at end of file +} diff --git a/js/tetris-manager.js b/js/tetris-manager.js new file mode 100644 index 0000000..3cbcb3c --- /dev/null +++ b/js/tetris-manager.js @@ -0,0 +1,56 @@ +class TetrisManager { + + constructor() { + this.container = document.querySelector('.game-container'); + + this.instances = new Set; + } + + createPlayer() { + const game = new Game(); + this.instances.add(game); + } + + init() { + this.callAll('rescale', []); + } + + removePlayer(tetris) { + this.instances.delete(tetris); + } + + start() { + this.instances.forEach(instance => { + instance.start(); + }); + this.init(); + } + + resume() { + this.instances.forEach(instance => { + instance.g.isPaused = false; + instance.g.lastTimeUpdate = Date.now(); + if (!firstRun) { + instance.update(instance.g.lastTime); + } + }); + } + + pause() { + this.instances.forEach(instance => { + instance.g.isPaused = true; + }); + } + + callAll(method, args) { + if (typeof method === 'string') { + this.instances.forEach(instance => { + instance[method](...args); + }); + } else if (typeof method === 'function') { + this.instances.forEach(instance => { + method(instance); + }); + } + } +} diff --git a/js/tetris.js b/js/tetris.js index 778210e..942719f 100644 --- a/js/tetris.js +++ b/js/tetris.js @@ -25,7 +25,7 @@ function centerOffset(matrix) { let offsetY = 0; matrix.forEach((row, y) => { let onlyZeroesY = true; - row.forEach((value, x) => { + row.forEach((value) => { if (value > 0) { onlyZeroesY = false; } @@ -39,7 +39,7 @@ function centerOffset(matrix) { }); for (let x = 0; x < matrix[0].length; x++) { let onlyZeroesX = true; - matrix.forEach((row, y) => { + matrix.forEach((row) => { if (row[x] > 0) onlyZeroesX = false; }); @@ -165,44 +165,6 @@ function drawRoundRect(ctx, x, y, w, h, r) { } function drawReliefRect(ctx, x, y, w, h, l, clr) { - ctx.fillStyle = clr; - ctx.fillRect(x + l, y + l, w - (2 * l), h - (2 * l)); - - ctx.fillStyle = colorLuminance(clr, .6); - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + w, y); - ctx.lineTo(x + w - l, y + l); - ctx.lineTo(x + l, y + l); - ctx.fill(); - ctx.closePath(); - - ctx.fillStyle = colorLuminance(clr, .3); - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x, y + h); - ctx.lineTo(x + l, y + h - l); - ctx.lineTo(x + l, y + l); - ctx.fill(); - ctx.closePath(); - - ctx.fillStyle = colorLuminance(clr, -.6); - ctx.beginPath(); - ctx.moveTo(x, y + h); - ctx.lineTo(x + w, y + h); - ctx.lineTo(x + w - l, y + h - l); - ctx.lineTo(x + l, y + h - l); - ctx.fill(); - ctx.closePath(); - - ctx.fillStyle = colorLuminance(clr, -.3); - ctx.beginPath(); - ctx.moveTo(x + w, y); - ctx.lineTo(x + w, y + h); - ctx.lineTo(x + w - l, y + h - l); - ctx.lineTo(x + w - l, y + l); - ctx.fill(); - ctx.closePath(); } function formatMillis(millis) { @@ -279,10 +241,14 @@ function rotate(matrix, dir) { } } -const game = new Game(); +const manager = new TetrisManager(); +manager.createPlayer(); + +// const game = new Game(); function startGame() { - game.start(); + // game.start(); + manager.start(); } /** @@ -306,4 +272,4 @@ function colorLuminance(hex, lum) { } return rgb; -} \ No newline at end of file +} diff --git a/js/theme.js b/js/theme.js new file mode 100644 index 0000000..f8cd13e --- /dev/null +++ b/js/theme.js @@ -0,0 +1,110 @@ +class Theme { + drawTile(color, matrix, ctx) { + } +} + +class DefaultTheme extends Theme { + drawTile(color, matrix, ctx) { + ctx.fillStyle = color; + ctx.fillRect(tileGap / 2, tileGap / 2, 1 - tileGap, 1 - tileGap); + } +} + +class CleanTheme extends Theme { + drawTile(color, matrix, ctx) { + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + } +} + +class ModernTheme extends Theme { + drawTile(color, matrix, ctx) { + ctx.fillStyle = color; + drawRoundRect(ctx, tileGap / 2, tileGap / 2, 1 - tileGap, 1 - tileGap, .15); + } +} + +class SnakesTheme extends Theme { + drawTile(color, matrix, ctx, x, y) { + let r1 = .15, // top right + r2 = .15, // bottom right + r3 = .15, // bottom left + r4 = .15; // top left + // Is there a tile to the left? + if (matrix[y][x - 1] > 0) { + r3 = 0; + r4 = 0; + } + // Is there a tile to the right? + if (matrix[y][x + 1] > 0) { + r1 = 0; + r2 = 0; + } + // Is there a tile to the top? + if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) { + r1 = 0; + r4 = 0; + } + // Is there a tile to the bottom? + if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) { + r2 = 0; + r3 = 0; + } + drawRoundRect(ctx, x, y, 1, 1, [r1, r2, r3, r4]); + } +} + +class RetroTheme extends Theme { + static drawReliefRect(ctx, x, y, width, height, elevation, color) { + ctx.fillStyle = color; + ctx.fillRect(x + elevation, y + elevation, width - (2 * elevation), height - (2 * elevation)); + + ctx.fillStyle = colorLuminance(color, .6); + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y); + ctx.lineTo(x + width - elevation, y + elevation); + ctx.lineTo(x + elevation, y + elevation); + ctx.fill(); + ctx.closePath(); + + ctx.fillStyle = colorLuminance(color, .3); + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x, y + height); + ctx.lineTo(x + elevation, y + height - elevation); + ctx.lineTo(x + elevation, y + elevation); + ctx.fill(); + ctx.closePath(); + + ctx.fillStyle = colorLuminance(color, -.6); + ctx.beginPath(); + ctx.moveTo(x, y + height); + ctx.lineTo(x + width, y + height); + ctx.lineTo(x + width - elevation, y + height - elevation); + ctx.lineTo(x + elevation, y + height - elevation); + ctx.fill(); + ctx.closePath(); + + ctx.fillStyle = colorLuminance(color, -.3); + ctx.beginPath(); + ctx.moveTo(x + width, y); + ctx.lineTo(x + width, y + height); + ctx.lineTo(x + width - elevation, y + height - elevation); + ctx.lineTo(x + width - elevation, y + elevation); + ctx.fill(); + ctx.closePath(); + } + + drawTile(color, matrix, ctx) { + RetroTheme.drawReliefRect(ctx, 0, 0, 1, 1, .15, color); + } +} + +const themes = { + default: new DefaultTheme(), + clean: new CleanTheme(), + modern: new ModernTheme(), + snakes: new SnakesTheme(), + retro: new RetroTheme() +}; diff --git a/lang.json b/lang.json new file mode 100644 index 0000000..3bf4806 --- /dev/null +++ b/lang.json @@ -0,0 +1,70 @@ +{ + "en": { + "button": { + "play": "Play!", + "reset": "Reset", + "resume": "Resume" + }, + "controls": { + "down": "Accelerate falling", + "hold": "Hold", + "left": "Move left", + "pause": "Pause", + "right": "Move right", + "tLeft": "Rotate to the left", + "tRight": "Rotate to the right" + }, + "counter": { + "score": "Score: ", + "time": "Time: " + }, + "theme": { + "default": "Default", + "clean": "Clean", + "modern": "Modern", + "retro": "Retro", + "snakes": "Snakes" + }, + "title": { + "appearance": "Appearance", + "controls": "Controls", + "game": "Tetris.js", + "language": "Language", + "paused": "Paused" + } + }, + "de": { + "button": { + "play": "Spielen!", + "reset": "Zurücksetzen", + "resume": "Fortsetzen" + }, + "controls": { + "down": "Fallen beschleunigen", + "hold": "Halten", + "left": "Nach links bewegen", + "pause": "Pausieren", + "right": "Nach rechts bewegen", + "tLeft": "Nach rechts drehen", + "tRight": "Nach links drehen" + }, + "counter": { + "score": "Punkte: ", + "time": "Zeit: " + }, + "theme": { + "default": "Standard", + "clean": "Lückenlos", + "modern": "Futuristisch", + "retro": "Retro", + "snakes": "Schlangen" + }, + "title": { + "appearance": "Erscheinungsbild", + "controls": "Steuerung", + "game": "Tetris.js", + "language": "Sprache", + "paused": "Pausiert" + } + } +} \ No newline at end of file diff --git a/style.css b/style.css index 70b07dd..b9c0a9d 100644 --- a/style.css +++ b/style.css @@ -1,59 +1,59 @@ body { + overflow: hidden; margin: 0; padding: 0; - overflow: hidden; background: #202028; + color: #fff; font-family: Roboto, Helvetica, Arial, sans-serif; font-size: 2.5em; - color: #fff; } .menu { + position: absolute; + -moz-transform: scale(.5); + -ms-transform: scale(.5); + -o-transform: scale(.5); + -webkit-transform: scale(.5); + transform: scale(.5); + z-index: 1; + width: 3.5rem; + height: 2.7rem; + padding: 0; + border: 0; + background: 0; + outline: 0; -webkit-transition: 0.1s -webkit-transform linear; transition: 0.1s -webkit-transform linear; transition: 0.1s transform linear; transition: 0.1s transform linear, 0.1s -webkit-transform linear; - position: absolute; - background: 0; float: left; - height: 2.7rem; - width: 3.5rem; - z-index: 1; - outline: 0; - padding: 0; - border: 0; - -webkit-transform: scale(.5); - -moz-transform: scale(.5); - -ms-transform: scale(.5); - -o-transform: scale(.5); - transform: scale(.5); } #menu { position: fixed; + top: 0; + transform: translateX(-100%); + z-index: 100; width: 100%; height: 100%; background: rgba(0, 0, 0, .75); - transform: translateX(-100%); - -webkit-transition: transform .5s; -moz-transition: transform .5s; -ms-transition: transform .5s; -o-transition: transform .5s; + -webkit-transition: transform .5s; transition: transform .5s; - z-index: 100; - top: 0; } #menu-content { position: absolute; - left: 50%; top: 50%; + left: 50%; transform: translate(-50%, -50%); - font-size: .6em; + overflow: auto; width: 375px; max-width: 100%; max-height: 90%; - overflow: auto; + font-size: .6em; } #menu-content > * { @@ -75,22 +75,22 @@ body { } #help-controls dt { - float: left; width: 30%; margin-bottom: 10px; + float: left; } #help-controls dt code { - background: rgba(255, 255, 255, 1); padding: 5px 10px; border-radius: 5px; + background: rgba(255, 255, 255, 1); color: #000; } #help-controls dd { - float: left; width: 70%; margin: 0 0 10px; + float: left; } .menu-open #menu { @@ -99,16 +99,16 @@ body { #menu-opener { position: absolute; - left: 10px; top: 10px; + left: 10px; z-index: 101; } .lang { display: inline; margin-left: 10px; - border-radius: 2px; padding: 4px 8px; + border-radius: 2px; cursor: pointer; } @@ -118,8 +118,8 @@ body { #game-title { position: absolute; - left: 50%; top: 50%; + left: 50%; transform: translate(-50%, -25%) translateY(-200px); opacity: 1; transition: opacity .5s; @@ -132,69 +132,73 @@ body { transform: translate(-50%, -50%); width: 300px; height: 75px; - font-size: 30px; - box-shadow: none; - -webkit-text-fill-color: #fff; - color: #ffff !important; - background: #3877FF !important; border: 0 !important; border-radius: 0 !important; - transition: box-shadow .2s, opacity .5s; - cursor: pointer; + background: #3877FF !important; + color: #ffff !important; + box-shadow: none; opacity: 1; + font-size: 30px; + cursor: pointer; + transition: box-shadow .2s, opacity .5s; + -webkit-text-fill-color: #fff; } #game-reset { + transform: translate(-50%, -50%) translateY(85px); display: none; background-color: #FF0D72 !important; - transform: translate(-50%, -50%) translateY(85px); opacity: 0; } +#game-play { + z-index: 10; +} + #game-play:hover, #game-play:active, #game-play:focus { - outline: none !important; box-shadow: 3px 4px 0 3px rgba(0, 0, 0, 0.2) !important; + outline: none !important; } #background { + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; - position: absolute; - left: 0; - top: 0; filter: url(#f2); } -#background.blurred { +.blurred { filter: url(#f1); } #canvas-container { - height: 936px; - width: 761px; position: absolute; - left: 50%; top: 50%; + left: 50%; transform: translate(-50%, -50%); + width: 761px; + height: 936px; } #tetris, #tetris-background { position: absolute; - left: 50%; top: 50%; - -webkit-transform: translate(-50%, -50%); + left: 50%; -moz-transform: translate(-50%, -50%); -ms-transform: translate(-50%, -50%); -o-transform: translate(-50%, -50%); + -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); - border: solid .2em #fff; z-index: 1; + border: solid .2em #fff; background: transparent; } #tetris-background { - border: none; z-index: 0; + border: none; } #tetris-hold { @@ -209,36 +213,36 @@ body { border: solid .2em #fff; } -.game-stats { - position: absolute; - left: 50%; - bottom: 3.75vh; - transform: translateX(-50%); - font-size: 2.5vh; -} - -#score { - font-weight: 900; - bottom: initial; - top: 2.5vh; - font-size: 5vh; -} - +/*.game-stats {*/ +/* position: absolute;*/ +/* bottom: 3.75vh;*/ +/* left: 50%;*/ +/* transform: translateX(-50%);*/ +/* font-size: 2.5vh;*/ +/*}*/ +/**/ +/*#score {*/ +/* top: 2.5vh;*/ +/* bottom: initial;*/ +/* font-size: 5vh;*/ +/* font-weight: 900;*/ +/*}*/ +/**/ #score.update { animation: scoreUpdate .5s; } @keyframes scoreUpdate { 0% { - transform: translateX(-50%) scale(1); + transform: scale(1); } 50% { - transform: translateX(-50%) scale(1.5); + transform: scale(1.5); } 100% { - transform: translateX(-50%) scale(1); + transform: scale(1); } } @@ -251,8 +255,8 @@ body { top: 50%; left: 50%; transform: translate(-50%, -50%) translateX(30vw); - font-size: 20px; max-width: 20vw; + font-size: 20px; } .game-over #score { @@ -261,12 +265,12 @@ body { #footer { position: absolute; - bottom: 10px; - width: 95%; - left: 2.5%; right: 2.5%; - font-size: 12px; + bottom: 10px; + left: 2.5%; + width: 95%; color: rgba(255, 255, 255, .5); + font-size: 12px; } #version-author { @@ -274,21 +278,21 @@ body { } #corner-buttons { - display: inline-block; position: absolute; right: 0; + display: inline-block; } #corner-buttons > a { - text-decoration: none; - display: inline-block; left: 10px; + display: inline-block; color: rgba(255, 255, 255, .5); - -webkit-transition: color .2s; -moz-transition: color .2s; -ms-transition: color .2s; -o-transition: color .2s; + -webkit-transition: color .2s; transition: color .2s; + text-decoration: none; } #corner-buttons > a:hover, #corner-buttons > a:active { @@ -296,18 +300,16 @@ body { } .bar, .bar::before, .bar::after { - -webkit-transition: 0.2s background linear 0.1s, 0.2s top linear 0.2s, 0.2s -webkit-transform linear; - transition: 0.2s background linear 0.1s, 0.2s top linear 0.2s, 0.2s -webkit-transform linear; - transition: 0.2s background linear 0.1s, 0.2s top linear 0.2s, 0.2s transform linear; - transition: 0.2s background linear 0.1s, 0.2s top linear 0.2s, 0.2s transform linear, 0.2s -webkit-transform linear; position: absolute; - background: #fff; - margin: auto; - width: 100%; - height: 0.3rem; - content: ''; top: 50%; left: 0; + width: 100%; + height: 0.3rem; + margin: auto; + background: #fff; + content: ''; + -webkit-transition: 0.2s background linear 0.1s, 0.2s top linear 0.2s, 0.2s -webkit-transform linear; + transition: 0.2s background linear 0.1s, 0.2s top linear 0.2s, 0.2s transform linear; } .bar { @@ -366,21 +368,21 @@ Radio Buttons } .radio input[type="radio"] + .radio-label:before { - content: ''; - background: #f4f4f4; - border-radius: 100%; - border: 1px solid #b4b4b4; + position: relative; + top: -0.2em; display: inline-block; width: 1.4em; height: 1.4em; - position: relative; - top: -0.2em; margin-right: 1em; - vertical-align: top; - cursor: pointer; + border: 1px solid #b4b4b4; + border-radius: 100%; + background: #f4f4f4; + content: ''; text-align: center; + cursor: pointer; -webkit-transition: all 250ms ease; transition: all 250ms ease; + vertical-align: top; } .radio input[type="radio"]:checked + .radio-label:before { @@ -390,17 +392,72 @@ Radio Buttons } .radio input[type="radio"]:focus + .radio-label:before { - outline: none; border-color: #3197EE; + outline: none; } .radio input[type="radio"]:disabled + .radio-label:before { - -webkit-box-shadow: inset 0 0 0 4px #f4f4f4; - box-shadow: inset 0 0 0 4px #f4f4f4; border-color: #b4b4b4; background: #b4b4b4; + -webkit-box-shadow: inset 0 0 0 4px #f4f4f4; + box-shadow: inset 0 0 0 4px #f4f4f4; } .radio input[type="radio"] + .radio-label:empty:before { margin-right: 0; -} \ No newline at end of file +} + +/* Game */ +.game-container { + display: flex; + height: calc(100vh - 40px); +} + +.game-instance { + flex-grow: 1; +} + +.score { + margin: 10px 0; + font-size: 40px; + text-align: center; +} + +.score.update { + animation: scoreUpdate .5s; +} + +.time { + font-size: 25px; + text-align: center; + margin: 10px 0; +} + +.canvas-container { + position: relative; + display: block; + width: 80%; + margin: 0 auto; +} + +.tetris-hold { + position: absolute; + top: 0; + left: 0; + width: 20%; +} + +.tetris-arena, .tetris-background { + position: absolute; + top: 0; + left: 20%; + width: 60%; + height: 100%; +} + +.tetris-upcoming { + position: absolute; + top: 0; + right: 0; + width: 20%; +}