diff --git a/src/front/index.html b/src/front/index.html index 28b2775..233ae24 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -12,7 +12,7 @@ - + @@ -35,15 +35,22 @@
-
- -
+
+
+ +
- diff --git a/src/front/static/css/style.css b/src/front/static/css/style.css index e189baa..35cd827 100644 --- a/src/front/static/css/style.css +++ b/src/front/static/css/style.css @@ -42,8 +42,37 @@ text-neutral-900 dark:text-white px-4 py-2 delay-0 duration-150 transition-colors - + border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700 active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500 ; } + +.taskbar-button { + @apply shadow-2x1 + bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700 + text-neutral-900 dark:text-white + px-4 py-0.5 + content-center text-center + delay-0 duration-150 transition-colors + + border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700 + active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500 + ; +} + +.menu-default-button { + @apply w-46 h-12 + text-neutral-900 dark:text-white + bg-neutral-200 hover:bg-neutral-300 + dark:bg-neutral-800 dark:hover:bg-neutral-700 + ; +} + +.menu-default-label { + @apply w-46 h-12 + text-neutral-900 dark:text-white + bg-neutral-200 + dark:bg-neutral-800 + ; +} diff --git a/src/front/static/ts/main.ts b/src/front/static/ts/main.ts index 1b4e0db..6533fd8 100644 --- a/src/front/static/ts/main.ts +++ b/src/front/static/ts/main.ts @@ -1,6 +1,8 @@ import { oneko } from "./oneko.ts"; +import Profile from "./views/Profile.ts"; +let profile_view = new Profile; -export async function isLogged(): boolean { +export async function isLogged(): Promise { let uuid_req = await fetch("http://localhost:3001/me", { method: "GET", credentials: "include", @@ -9,63 +11,11 @@ export async function isLogged(): boolean { { let uuid = await uuid_req.json(); document.cookie = `uuid=${uuid.user};max-age=${60*60*24*7}`; - - const old_button = document.getElementById("profile-button"); - - const dropdown = document.createElement("div"); - dropdown.classList.add("relative", "inline-block", "group"); - dropdown.id = "profile-button"; - const button_dropdown = dropdown.appendChild(document.createElement("button")); - button_dropdown.innerHTML = uuid.user; - button_dropdown.classList.add("text-neutral-900", "group-hover:text-neutral-700", "dark:text-white", "dark:group-hover:text-neutral-400"); - - const menu_div = dropdown.appendChild(document.createElement("div")); - menu_div.classList.add("float:right", "hidden", "absolute", "left-0", "bottom-full", "dark:bg-neutral-800", "dark:text-white", "min-w-[160px]", "shadow-lg", "z-10", "group-hover:block"); - - const profile_a = menu_div.appendChild(document.createElement("a")); - const settings_a = menu_div.appendChild(document.createElement("a")); - const logout_button = menu_div.appendChild(document.createElement("button")); - - profile_a.text = "profile"; - profile_a.classList.add("block", "no-underline", "px-4", "py-3"); - profile_a.href = "/profile"; - profile_a.setAttribute("data-link", ""); - - settings_a.text = "settings"; - settings_a.classList.add("block", "no-underline", "px-4", "py-3"); - settings_a.href = "/settings"; - settings_a.setAttribute("data-link", ""); - - logout_button.innerHTML = "logout"; - logout_button.classList.add("block", "no-underline", "px-4", "py-3"); - logout_button.id = "logout-button"; - //document.getElementById("logout-button")?.addEventListener("click", async () => { - logout_button.addEventListener("click", async () => { - let req = await fetch("http://localhost:3001/logout", { - method: "GET", - credentials: "include", - }); - if (req.status === 200) - isLogged(); - else - console.error("logout failed"); - }); - - old_button.replaceWith(dropdown); return true; } else // 401 { document.cookie = `uuid=;max-age=0`; - const old_button = document.getElementById("profile-button"); - const login_button = document.createElement("a"); - login_button.id = "profile-button"; - login_button.text = "login"; - login_button.classList.add("text-neutral-900", "hover:text-neutral-700", "dark:text-white", "dark:hover:text-neutral-400"); - login_button.href = "/login"; - login_button.setAttribute("data-link", ""); - - old_button.replaceWith(login_button); return false; } } @@ -90,8 +40,6 @@ const routes = [ { path: "/login", view: () => import("./views/LoginPage.ts") }, { path: "/register", view: () => import("./views/RegisterPage.ts") }, - - { path: "/profile", view: () => import("./views/Profile.ts") }, ]; const router = async () => { @@ -107,7 +55,7 @@ const router = async () => { if (view) view.running = false; - + //console.log(match); const module = await match.route.view(); @@ -117,12 +65,18 @@ const router = async () => { view.run(); }; +document.getElementById("profile-button")?.addEventListener("click", () => {profile_view.run();}); window.addEventListener("popstate", router); document.addEventListener("DOMContentLoaded", () => { isLogged(); document.body.addEventListener("click", e=> { + if (!e.target.closest("#taskbar-menu") && !e.target.matches("#profile-button")) + { + profile_view.open = false; + document.getElementById("taskbar-menu").innerHTML = ""; + } if (e.target.matches("[data-link]")) { e.preventDefault(); @@ -144,3 +98,21 @@ document.addEventListener("DOMContentLoaded", () => { }); oneko(); + +function updateClock() +{ + const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + const clock = document.getElementById("taskbar-clock"); + const now = new Date(); + let hours = now.getHours(); + let minutes = now.getMinutes(); + + hours = hours < 10 ? "0" + hours : hours; + minutes = minutes < 10 ? "0" + minutes : minutes; + + clock.innerHTML = `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]} ` + hours + ":" + minutes; +} + +setInterval(updateClock, 5000); +updateClock(); diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index bf30229..f1b04bd 100644 --- a/src/front/static/ts/views/LoginPage.ts +++ b/src/front/static/ts/views/LoginPage.ts @@ -1,4 +1,5 @@ import Aview from "./Aview.ts" +import { dragElement } from "./drag.js" import { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" @@ -13,22 +14,30 @@ export default class extends Aview { async getHTML() { return ` -
-

login

+
+
+ login.ts +
+ + + × +
+
- - - - - - - register - - +
+

welcome back ! please login.

+ + + +
+ +
+
`; } async run() { + dragElement(document.getElementById("window")); const login = async () => { const username = (document.getElementById("username") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value; diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts index 16fa93d..502b15c 100644 --- a/src/front/static/ts/views/Profile.ts +++ b/src/front/static/ts/views/Profile.ts @@ -6,36 +6,81 @@ export default class extends Aview { constructor() { super(); - if (!isLogged()) - navigationManager("/login"); this.setTitle("profile"); } async getHTML() { return ` -
+
+
+
+ +
girls kissing :3
+
+ +
+
+
+ +
+
`; } + open: boolean = false; + async run() { - const uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; - const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { + let uuid: String; + if (this.open) + { + this.open = false; + document.getElementById("taskbar-menu").innerHTML = ""; + return ; + } + this.open = true; + document.getElementById("taskbar-menu").innerHTML = await this.getHTML(); + + async function getMainHTML() { + if (!(await isLogged())) + { + document.getElementById("menu-bottom-div").classList.add("hidden"); + return ` + login + register + `; + } + document.getElementById("menu-bottom-div").classList.remove("hidden"); + + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + return ` + hi, ${uuid} ! +
+ + + `; + } + + + document.getElementById("profile-items").innerHTML = await getMainHTML(); + + /*const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { method: "GET", credentials: "include", }); - if (userdata_req.status == 404) { console.error("invalid user"); return ; } - let userdata = await userdata_req.json(); - console.log(userdata_req); + console.log(userdata_req);*/ - const main = document.getElementById("main-window"); + /*const main = document.getElementById("profile-profile"); const nametag = main.appendChild(document.createElement("span")); nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`; @@ -44,6 +89,17 @@ export default class extends Aview { const winrate = main.appendChild(document.createElement("div")); winrate.innerHTML = `wins: ${userdata.wins} | losses: ${userdata.losses} | winrate: ${userdata.wins / (userdata.wins + userdata.losses)}`; - winrate.classList.add("text-neutral-900", "dark:text-white"); + winrate.classList.add("text-neutral-900", "dark:text-white");*/ + //console.log(document.getElementById("menu-logout")); + document.getElementById("menu-logout").addEventListener("click", async () => { + let req = await fetch("http://localhost:3001/logout", { + method: "GET", + credentials: "include", + }); + if (req.status === 200) + this.run(); + else + console.error("logout failed"); + }); } } diff --git a/src/front/static/ts/views/RegisterPage.ts b/src/front/static/ts/views/RegisterPage.ts index ab30708..67c828f 100644 --- a/src/front/static/ts/views/RegisterPage.ts +++ b/src/front/static/ts/views/RegisterPage.ts @@ -1,6 +1,7 @@ import Aview from "./Aview.ts" import { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" +import { dragElement } from "./drag.ts"; export default class extends Aview { @@ -13,22 +14,30 @@ export default class extends Aview { async getHTML() { return ` -
-

register

+
+
+ register.ts +
+ + + × +
+
- - - - - - - i already have an account - - +
+

welcome ! please register.

+ + + +
+ +
+
`; } async run() { + dragElement(document.getElementById("window")); const login = async () => { const username = (document.getElementById("username") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value; diff --git a/src/front/static/ts/views/Tetris.ts b/src/front/static/ts/views/Tetris.ts index 15d94e2..f01eb08 100644 --- a/src/front/static/ts/views/Tetris.ts +++ b/src/front/static/ts/views/Tetris.ts @@ -1,18 +1,17 @@ -import Aview from "./Aview.ts" +import Aview from "./Aview.ts"; +import { dragElement } from "./drag.js"; export default class extends Aview { + running: boolean; - running: boolean; + constructor() { + super(); + this.setTitle("tetris (local match)"); + this.running = true; + } - constructor() - { - super(); - this.setTitle("tetris (local match)"); - this.running = true; - } - - async getHTML() { - return ` + async getHTML() { + return `
pong_game.ts @@ -23,7 +22,7 @@ export default class extends Aview {
- +
@@ -37,663 +36,671 @@ export default class extends Aview {
`; - } - - async run() { - const COLS = 10; - const ROWS = 20; - const BLOCK = 30; // pixels per block - - type Cell = number; // 0 empty, >0 occupied (color index) - - // Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix - const TETROMINOES: { [key: string]: number[][][] } = { - I: [ - [[0,0,0,0] - ,[1,1,1,1] - ,[0,0,0,0] - ,[0,0,0,0]], - - [[0,0,1,0] - ,[0,0,1,0] - ,[0,0,1,0] - ,[0,0,1,0]], - - [[0,0,0,0] - ,[0,0,0,0] - ,[1,1,1,1] - ,[0,0,0,0]], - - [[0,1,0,0] - ,[0,1,0,0] - ,[0,1,0,0] - ,[0,1,0,0]], - ], - J: [ - [[2,0,0] - ,[2,2,2] - ,[0,0,0]], - - [[0,2,2] - ,[0,2,0] - ,[0,2,0]], - - [[0,0,0] - ,[2,2,2] - ,[0,0,2]], - - [[0,2,0] - ,[0,2,0] - ,[2,2,0]], - ], - L: [ - [[0,0,3] - ,[3,3,3] - ,[0,0,0]], - - [[0,3,0] - ,[0,3,0] - ,[0,3,3]], - - [[0,0,0] - ,[3,3,3] - ,[3,0,0]], - - [[3,3,0] - ,[0,3,0] - ,[0,3,0]], - ], - O: [ - [[4,4] - ,[4,4]], - ], - S: [ - [[0,5,5] - ,[5,5,0] - ,[0,0,0]], - - [[0,5,0] - ,[0,5,5] - ,[0,0,5]], - - [[0,0,0] - ,[0,5,5] - ,[5,5,0]], - - [[5,0,0] - ,[5,5,0] - ,[0,5,0]], - - ], - T: [ - [[0,6,0] - ,[6,6,6] - ,[0,0,0]], - - [[0,6,0] - ,[0,6,6] - ,[0,6,0]], - - [[0,0,0] - ,[6,6,6] - ,[0,6,0]], - - [[0,6,0] - ,[6,6,0] - ,[0,6,0]], - ], - Z: [ - [[7,7,0] - ,[0,7,7] - ,[0,0,0]], - - [[0,0,7] - ,[0,7,7] - ,[0,7,0]], - - [[0,0,0] - ,[7,7,0] - ,[0,7,7]], - - [[0,7,0] - ,[7,7,0] - ,[7,0,0]], - ], - }; - - const COLORS = [ - '#000000', // placeholder for 0 - '#00ffff', // I - cyan - '#0000ff', // J - blue - '#ff7f00', // L - orange - '#ffff00', // O - yellow - '#00ff00', // S - green - '#800080', // T - purple - '#ff0000', // Z - red - ]; - - class Piece { - shape: number[][]; - rotations: number[][][]; - rotationIndex: number; - x: number; - y: number; - colorIndex: number; - - constructor(public type: string) { - this.rotations = TETROMINOES[type]; - this.rotationIndex = 0; - this.shape = this.rotations[this.rotationIndex]; - this.colorIndex = this.findColorIndex(); - - this.x = Math.floor((COLS - this.shape[0].length) / 2); - this.y = -2; //start on tiles 21 and 22 - } - - findColorIndex() { - for (const row of this.shape) - for (const v of row) - if (v) - return v; - return 1; - } - - rotateCW() { - this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length; - this.shape = this.rotations[this.rotationIndex]; - } - - rotateCCW() { - this.rotationIndex = (this.rotationIndex - 1 + this.rotations.length) % this.rotations.length; - this.shape = this.rotations[this.rotationIndex]; - } - - getCells(): { x: number; y: number; val: number }[] { - const cells: { x: number; y: number; val: number }[] = []; - - for (let r = 0; r < this.shape.length; r++) - { - for (let c = 0; c < this.shape[r].length; c++) - { - const val = this.shape[r][c]; - if (val) - cells.push({ x: this.x + c, y: this.y + r, val }); - } - } - return cells; - } - } - - class Game { - board: Cell[][]; - canvas: HTMLCanvasElement; - holdCanvas: HTMLCanvasElement; - queueCanvas: HTMLCanvasElement; - ctx: CanvasRenderingContext2D; - holdCtx: CanvasRenderingContext2D; - queueCtx: CanvasRenderingContext2D; - piece: Piece | null = null; - holdPiece: Piece | null = null; - canHold: boolean = true; - nextQueue: string[] = []; - score = 0; - level = 1; - lines = 0; - dropInterval = 1000; - lastDrop = 0; - isGameOver = false; - isPaused = false; - - constructor(canvasId: string) { - const el = document.getElementById(canvasId); - this.canvas = el; - this.canvas.width = COLS * BLOCK; - this.canvas.height = ROWS * BLOCK; - const ctx = this.canvas.getContext('2d'); - this.ctx = ctx; - - this.holdCanvas = document.getElementById('hold'); - this.queueCanvas = document.getElementById('queue'); - this.holdCtx = this.holdCanvas.getContext('2d'); - this.queueCtx = this.queueCanvas.getContext('2d'); - - this.board = this.createEmptyBoard(); - this.fillBag(); - - this.spawnPiece(); - this.registerListeners(); - requestAnimationFrame(this.loop.bind(this)); - } - - createEmptyBoard(): Cell[][] { - const b: Cell[][] = []; - for (let r = 0; r < ROWS; r++) - { - const row: Cell[] = new Array(COLS).fill(0); - b.push(row); - } - return b; - } - - fillBag() { - // classic 7-bag randomizer - const pieces = Object.keys(TETROMINOES); - const bag = [...pieces]; - for (let i = bag.length - 1; i > 0; i--) - { - const j = Math.floor(Math.random() * (i + 1)); - [bag[i], bag[j]] = [bag[j], bag[i]]; - } - this.nextQueue.push(...bag); - } - - hold() { - if (!this.canHold) - return; - - [this.piece, this.holdPiece] = [this.holdPiece, this.piece]; - if (!this.piece) - this.spawnPiece(); - - this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2); - this.piece.y = -2; - this.piece.rotationIndex = 0; - this.piece.shape = this.piece.rotations[this.piece.rotationIndex]; - - this.canHold = false; - this.drawHold(); - } - - spawnPiece() { - this.canHold = true; - if (this.nextQueue.length < 7) - this.fillBag(); - const type = this.nextQueue.shift()!; - this.piece = new Piece(type); - // If spawn collides immediately -> game over - if (this.collides(this.piece)) - { - this.isGameOver = true; - } - - this.drawHold(); - this.drawQueue(); - } - - collides(piece: Piece): boolean { - for (const cell of piece.getCells()) - { - if (cell.y >= ROWS) - return true; - if (cell.x < 0 || cell.x >= COLS) - return true; - if (cell.y >= 0 && this.board[cell.y][cell.x]) - return true; - } - return false; - } - - getGhostOffset(piece: Piece): boolean { - let y: number = 0; - while (true) - { - for (const cell of piece.getCells()) - { - console.log(cell.y + y); - if ((cell.y + y >= ROWS) || (cell.y + y >= 0 && this.board[cell.y + y][cell.x])) - return y - 1; - } - - y++; - } - } - - lockPiece() { - if (!this.piece) - return; - let isValid:boolean = false; - for (const cell of this.piece.getCells()) - { - if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS) - this.board[cell.y][cell.x] = cell.val; - if (cell.y < 20) - isValid = true; - } - if (!isValid) - this.isGameOver = true; - - this.clearLines(); - this.spawnPiece(); - } - - clearLines() { - let linesCleared = 0; - outer: for (let r = ROWS - 1; r >= 0; r--) - { - for (let c = 0; c < COLS; c++) - if (!this.board[r][c]) - continue outer; - - this.board.splice(r, 1); - this.board.unshift(new Array(COLS).fill(0)); - linesCleared++; - r++; - } - - if (linesCleared > 0) - { - this.lines += linesCleared; - // scoring like classic tetris - const points = [0, 40, 100, 300, 1200]; - this.score += (points[linesCleared] || 0) * this.level; - // level up every 10 lines (Fixed Goal System) - const newLevel = Math.floor(this.lines / 10) + 1; - if (newLevel > this.level) - { - this.level = newLevel; - this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75); - } - } - } - - rotatePiece(dir: 'cw' | 'ccw') { - if (!this.piece) - return; - // Try rotation with wall kicks - const originalIndex = this.piece.rotationIndex; - if (dir === 'cw') - this.piece.rotateCW(); - else - this.piece.rotateCCW(); - const kicks = [0, -1, 1, -2, 2]; - for (const k of kicks) { - this.piece.x += k; - if (!this.collides(this.piece)) - return; - this.piece.x -= k; - } - // no valid kick, revert - this.piece.rotationIndex = originalIndex; - this.piece.shape = this.piece.rotations[originalIndex]; - } - - movePiece(dx: number, dy: number) { - if (!this.piece) - return; - this.piece.x += dx; - this.piece.y += dy; - if (this.collides(this.piece)) - { - this.piece.x -= dx; - this.piece.y -= dy; - return false; - } - return true; - } - - hardDrop() { - if (!this.piece) - return; - let dropped = 0; - while (this.movePiece(0, 1)) - dropped++; - this.score += dropped * 2; - this.lockPiece(); - } - - softDrop() { - if (!this.piece) - return; - if (!this.movePiece(0, 1)) - return; - //this.lockPiece(); - else - this.score += 1; - } - - keys: Record = {}; - - registerListeners() { - window.addEventListener('keydown', (e) => { - this.keys[e.key] = true; - - if (this.isGameOver) return; - if (e.key === 'ArrowLeft') - this.movePiece(-1, 0); - else if (e.key === 'ArrowRight') - this.movePiece(1, 0); - else if (e.key === 'ArrowDown') - this.softDrop(); - else if (e.code === 'Space') - { - e.preventDefault(); - this.hardDrop(); - } - else if (e.code === 'ShiftLeft') - { - e.preventDefault(); - this.hold(); - } - else if (e.key === 'x' || e.key === 'X' || e.key === 'ArrowUp') - this.rotatePiece('cw'); - else if (e.key === 'z' || e.key === 'Z' || e.code === 'ControlLeft') - this.rotatePiece('ccw'); - else if (e.key === 'p' || e.key === 'P' || e.key === 'Escape') - this.isPaused = !this.isPaused; - }); - - document.addEventListener("keyup", e => { this.keys[e.key] = false; }); - } - - loop(timestamp: number) - { - if (!this.lastDrop) - this.lastDrop = timestamp; - if (!this.isPaused && !this.isGameOver) - { - if (timestamp - this.lastDrop > this.dropInterval) - { - if (!this.movePiece(0, 1)) - this.lockPiece(); - this.lastDrop = timestamp; - } - } - this.draw(); - requestAnimationFrame(this.loop.bind(this)); - } - - drawGrid() { - const ctx = this.ctx; - ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - ctx.strokeStyle = '#222'; - for (let r = 0; r <= ROWS; r++) - { - // horizontal lines - ctx.beginPath(); - ctx.moveTo(0, r * BLOCK); - ctx.lineTo(COLS * BLOCK, r * BLOCK); - ctx.stroke(); - } - for (let c = 0; c <= COLS; c++) - { - ctx.beginPath(); - ctx.moveTo(c * BLOCK, 0); - ctx.lineTo(c * BLOCK, ROWS * BLOCK); - ctx.stroke(); - } - } - - drawBoard() { - for (let r = 0; r < ROWS; r++) - { - for (let c = 0; c < COLS; c++) - { - const val = this.board[r][c]; - if (val) - this.fillBlock(c, r, COLORS[val]); - else - this.clearBlock(c, r); - } - } - } - - drawPiece() { - if (!this.piece) - return; - - for (const cell of this.piece.getCells()) - if (cell.y >= 0) - this.fillBlock(cell.x, cell.y, COLORS[cell.val]); - - let offset:number = this.getGhostOffset(this.piece); - for (const cell of this.piece.getCells()) - if (cell.y + offset >= 0) - this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]); - } - - drawHold() { - if (!this.holdPiece) - return ; - - this.holdCtx.clearRect(0, 0, 200, 200) - let y: number = 0; - for (const row of this.holdPiece.rotations[0]) - { - let x:number = 0; - for (const val of row) - { - if (val) - { - this.holdCtx.fillStyle = this.canHold ? COLORS[this.holdPiece.findColorIndex()] : "gray"; - this.holdCtx.fillRect( - x * BLOCK + 1 + ((4 - this.holdPiece.rotations[0].length) * 15) + 10, - y * BLOCK + 1 + 20, - BLOCK - 2, BLOCK - 2); - } - x++; - } - y++; - } - } - - - drawQueue() { - this.queueCtx.clearRect(0, 0, 500, 500) - let placement:number = 0; - console.log(this.nextQueue.slice(0, 5)) - for (const nextPiece of this.nextQueue.slice(0, 5)) - { - let y: number = 0; - for (const row of TETROMINOES[nextPiece][0]) - { - let x:number = 0; - for (const val of row) - { - if (val) - { - this.queueCtx.fillStyle = COLORS[['I', 'J', 'L', 'O', 'S', 'T', 'Z'].indexOf(nextPiece) + 1]; - this.queueCtx.fillRect( - x * BLOCK + 1 + ((4 - TETROMINOES[nextPiece][0].length) * 15) + 10, - y * BLOCK + 1 + (placement * 80) + 20 - (nextPiece === 'I' ? 15 : 0), - BLOCK - 2, BLOCK - 2); - } - x++; - } - y++; - } - placement++; - } - } - - fillBlock(x: number, y: number, color: string) { - const ctx = this.ctx; - ctx.fillStyle = color; - ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); - } - fillGhostBlock(x: number, y: number, color: string) { - const ctx = this.ctx; - ctx.strokeStyle = color; - ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); - } - - clearBlock(x: number, y: number) { - const ctx = this.ctx; - ctx.clearRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); - } - - drawHUD() { - const ctx = this.ctx; - ctx.fillStyle = 'rgba(0,0,0,0.6)'; - ctx.fillRect(4, 4, 120, 60); - ctx.fillStyle = '#fff'; - ctx.font = '12px Kubasta'; - ctx.fillText(`Score: ${this.score}`, 8, 20); - ctx.fillText(`Lines: ${this.lines}`, 8, 36); - ctx.fillText(`Level: ${this.level}`, 8, 52); - - if (this.isPaused) - { - ctx.fillStyle = 'rgba(0,0,0,0.7)'; - ctx.fillRect(0, (this.canvas.height / 2) - 24, this.canvas.width, 48); - ctx.fillStyle = '#fff'; - ctx.font = '24px Kubasta'; - ctx.textAlign = 'center'; - ctx.fillText('PAUSED', this.canvas.width / 2, this.canvas.height / 2 + 8); - ctx.textAlign = 'start'; - } - - if (this.isGameOver) - { - ctx.fillStyle = 'rgba(0,0,0,0.7)'; - ctx.fillRect(0, (this.canvas.height / 2) - 36, this.canvas.width, 72); - ctx.fillStyle = '#fff'; - ctx.font = '28px Kubasta'; - ctx.textAlign = 'center'; - ctx.fillText('GAME OVER', this.canvas.width / 2, this.canvas.height / 2 + 8); - ctx.textAlign = 'start'; - } - } - - draw() { - // clear everything - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - this.ctx.fillStyle = '#000'; - this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - - this.ctx.strokeStyle = '#111'; - for (let r = 0; r <= ROWS; r++) - { - this.ctx.beginPath(); - this.ctx.moveTo(0, r * BLOCK); - this.ctx.lineTo(COLS * BLOCK, r * BLOCK); - this.ctx.stroke(); - } - for (let c = 0; c <= COLS; c++) - { - this.ctx.beginPath(); - this.ctx.moveTo(c * BLOCK, 0); - this.ctx.lineTo(c * BLOCK, ROWS * BLOCK); - this.ctx.stroke(); - } - - this.drawBoard(); - this.drawPiece(); - this.drawHUD(); - } - } - - window.addEventListener('load', () => { - try { - const canvas = document.getElementById('board'); - if (!canvas) { - console.error('D:'); - return; - } - const game = new Game('board'); - } catch (err) { - console.error(err); - } - }); - } + } + + async run() { + dragElement(document.getElementById("window")); + const COLS = 10; + const ROWS = 20; + const BLOCK = 30; // pixels per block + + type Cell = number; // 0 empty, >0 occupied (color index) + + // Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix + const TETROMINOES: { [key: string]: number[][][] } = { + I: [ + [ + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + + [ + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + ], + + [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + ], + + [ + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + ], + ], + J: [ + [ + [2, 0, 0], + [2, 2, 2], + [0, 0, 0], + ], + + [ + [0, 2, 2], + [0, 2, 0], + [0, 2, 0], + ], + + [ + [0, 0, 0], + [2, 2, 2], + [0, 0, 2], + ], + + [ + [0, 2, 0], + [0, 2, 0], + [2, 2, 0], + ], + ], + L: [ + [ + [0, 0, 3], + [3, 3, 3], + [0, 0, 0], + ], + + [ + [0, 3, 0], + [0, 3, 0], + [0, 3, 3], + ], + + [ + [0, 0, 0], + [3, 3, 3], + [3, 0, 0], + ], + + [ + [3, 3, 0], + [0, 3, 0], + [0, 3, 0], + ], + ], + O: [ + [ + [4, 4], + [4, 4], + ], + ], + S: [ + [ + [0, 5, 5], + [5, 5, 0], + [0, 0, 0], + ], + + [ + [0, 5, 0], + [0, 5, 5], + [0, 0, 5], + ], + + [ + [0, 0, 0], + [0, 5, 5], + [5, 5, 0], + ], + + [ + [5, 0, 0], + [5, 5, 0], + [0, 5, 0], + ], + ], + T: [ + [ + [0, 6, 0], + [6, 6, 6], + [0, 0, 0], + ], + + [ + [0, 6, 0], + [0, 6, 6], + [0, 6, 0], + ], + + [ + [0, 0, 0], + [6, 6, 6], + [0, 6, 0], + ], + + [ + [0, 6, 0], + [6, 6, 0], + [0, 6, 0], + ], + ], + Z: [ + [ + [7, 7, 0], + [0, 7, 7], + [0, 0, 0], + ], + + [ + [0, 0, 7], + [0, 7, 7], + [0, 7, 0], + ], + + [ + [0, 0, 0], + [7, 7, 0], + [0, 7, 7], + ], + + [ + [0, 7, 0], + [7, 7, 0], + [7, 0, 0], + ], + ], + }; + + const COLORS = [ + "#000000", // placeholder for 0 + "#00ffff", // I - cyan + "#0000ff", // J - blue + "#ff7f00", // L - orange + "#ffff00", // O - yellow + "#00ff00", // S - green + "#800080", // T - purple + "#ff0000", // Z - red + ]; + + class Piece { + shape: number[][]; + rotations: number[][][]; + rotationIndex: number; + x: number; + y: number; + colorIndex: number; + + constructor(public type: string) { + this.rotations = TETROMINOES[type]; + this.rotationIndex = 0; + this.shape = this.rotations[this.rotationIndex]; + this.colorIndex = this.findColorIndex(); + + this.x = Math.floor((COLS - this.shape[0].length) / 2); + this.y = -2; //start on tiles 21 and 22 + } + + findColorIndex() { + for (const row of this.shape) for (const v of row) if (v) return v; + return 1; + } + + rotateCW() { + this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length; + this.shape = this.rotations[this.rotationIndex]; + } + + rotateCCW() { + this.rotationIndex = + (this.rotationIndex - 1 + this.rotations.length) % + this.rotations.length; + this.shape = this.rotations[this.rotationIndex]; + } + + getCells(): { x: number; y: number; val: number }[] { + const cells: { x: number; y: number; val: number }[] = []; + + for (let r = 0; r < this.shape.length; r++) { + for (let c = 0; c < this.shape[r].length; c++) { + const val = this.shape[r][c]; + if (val) cells.push({ x: this.x + c, y: this.y + r, val }); + } + } + return cells; + } + } + + class Game { + board: Cell[][]; + canvas: HTMLCanvasElement | null; + holdCanvas: HTMLCanvasElement | null; + queueCanvas: HTMLCanvasElement | null; + ctx: CanvasRenderingContext2D; + holdCtx: CanvasRenderingContext2D; + queueCtx: CanvasRenderingContext2D; + piece: Piece | null = null; + holdPiece: Piece | null = null; + canHold: boolean = true; + nextQueue: string[] = []; + score = 0; + level = 1; + lines = 0; + dropInterval = 1000; + lastDrop = 0; + isGameOver = false; + isPaused = false; + + constructor(canvasId: string) { + const el = document.getElementById( + canvasId, + ) as HTMLCanvasElement | null; + this.canvas = el; + this.canvas.width = COLS * BLOCK; + this.canvas.height = ROWS * BLOCK; + const ctx = this.canvas.getContext("2d"); + this.ctx = ctx; + + this.holdCanvas = document.getElementById("hold"); + this.queueCanvas = document.getElementById("queue"); + this.holdCtx = this.holdCanvas.getContext("2d"); + this.queueCtx = this.queueCanvas.getContext("2d"); + + this.board = this.createEmptyBoard(); + this.fillBag(); + + this.spawnPiece(); + this.registerListeners(); + requestAnimationFrame(this.loop.bind(this)); + } + + createEmptyBoard(): Cell[][] { + const b: Cell[][] = []; + for (let r = 0; r < ROWS; r++) { + const row: Cell[] = new Array(COLS).fill(0); + b.push(row); + } + return b; + } + + fillBag() { + // classic 7-bag randomizer + const pieces = Object.keys(TETROMINOES); + const bag = [...pieces]; + for (let i = bag.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [bag[i], bag[j]] = [bag[j], bag[i]]; + } + this.nextQueue.push(...bag); + } + + hold() { + if (!this.canHold) return; + + [this.piece, this.holdPiece] = [this.holdPiece, this.piece]; + if (!this.piece) this.spawnPiece(); + + this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2); + this.piece.y = -2; + this.piece.rotationIndex = 0; + this.piece.shape = this.piece.rotations[this.piece.rotationIndex]; + + this.canHold = false; + this.drawHold(); + } + + spawnPiece() { + this.canHold = true; + if (this.nextQueue.length < 7) this.fillBag(); + const type = this.nextQueue.shift()!; + this.piece = new Piece(type); + // If spawn collides immediately -> game over + if (this.collides(this.piece)) { + this.isGameOver = true; + } + + this.drawHold(); + this.drawQueue(); + } + + collides(piece: Piece): boolean { + for (const cell of piece.getCells()) { + if (cell.y >= ROWS) return true; + if (cell.x < 0 || cell.x >= COLS) return true; + if (cell.y >= 0 && this.board[cell.y][cell.x]) return true; + } + return false; + } + + getGhostOffset(piece: Piece): number { + let y: number = 0; + while (true) { + for (const cell of piece.getCells()) { + console.log(cell.y + y); + if ( + cell.y + y >= ROWS || + (cell.y + y >= 0 && this.board[cell.y + y][cell.x]) + ) + return y - 1; + } + + y++; + } + } + + lockPiece() { + if (!this.piece) return; + let isValid: boolean = false; + for (const cell of this.piece.getCells()) { + if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS) + this.board[cell.y][cell.x] = cell.val; + if (cell.y < 20) isValid = true; + } + if (!isValid) this.isGameOver = true; + + this.clearLines(); + this.spawnPiece(); + } + + clearLines() { + let linesCleared = 0; + outer: for (let r = ROWS - 1; r >= 0; r--) { + for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer; + + this.board.splice(r, 1); + this.board.unshift(new Array(COLS).fill(0)); + linesCleared++; + r++; + } + + if (linesCleared > 0) { + this.lines += linesCleared; + // scoring like classic tetris + const points = [0, 40, 100, 300, 1200]; + this.score += (points[linesCleared] || 0) * this.level; + // level up every 10 lines (Fixed Goal System) + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel > this.level) { + this.level = newLevel; + this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75); + } + } + } + + rotatePiece(dir: "cw" | "ccw") { + if (!this.piece) return; + // Try rotation with wall kicks + const originalIndex = this.piece.rotationIndex; + if (dir === "cw") this.piece.rotateCW(); + else this.piece.rotateCCW(); + const kicks = [0, -1, 1, -2, 2]; + for (const k of kicks) { + this.piece.x += k; + if (!this.collides(this.piece)) return; + this.piece.x -= k; + } + // no valid kick, revert + this.piece.rotationIndex = originalIndex; + this.piece.shape = this.piece.rotations[originalIndex]; + } + + movePiece(dx: number, dy: number) { + if (!this.piece) return; + this.piece.x += dx; + this.piece.y += dy; + if (this.collides(this.piece)) { + this.piece.x -= dx; + this.piece.y -= dy; + return false; + } + return true; + } + + hardDrop() { + if (!this.piece) return; + let dropped = 0; + while (this.movePiece(0, 1)) dropped++; + this.score += dropped * 2; + this.lockPiece(); + } + + softDrop() { + if (!this.piece) return; + if (!this.movePiece(0, 1)) return; + //this.lockPiece(); + else this.score += 1; + } + + keys: Record = {}; + + registerListeners() { + window.addEventListener("keydown", (e) => { + this.keys[e.key] = true; + + if (this.isGameOver) return; + + if (e.key === "p" || e.key === "P" || e.key === "Escape") + this.isPaused = !this.isPaused; + + if (this.isPaused) return; + + if (e.key === "ArrowLeft") this.movePiece(-1, 0); + else if (e.key === "ArrowRight") this.movePiece(1, 0); + else if (e.key === "ArrowDown") this.softDrop(); + else if (e.code === "Space") { + e.preventDefault(); + this.hardDrop(); + } else if (e.code === "ShiftLeft") { + e.preventDefault(); + this.hold(); + } else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") + this.rotatePiece("cw"); + else if (e.key === "z" || e.key === "Z" || e.code === "ControlLeft") + this.rotatePiece("ccw"); + }); + + document.addEventListener("keyup", (e) => { + this.keys[e.key] = false; + }); + } + + loop(timestamp: number) { + if (!this.lastDrop) this.lastDrop = timestamp; + if (!this.isPaused && !this.isGameOver) { + if (timestamp - this.lastDrop > this.dropInterval) { + if (!this.movePiece(0, 1)) this.lockPiece(); + this.lastDrop = timestamp; + } + } + this.draw(); + requestAnimationFrame(this.loop.bind(this)); + } + + drawGrid() { + const ctx = this.ctx; + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + ctx.strokeStyle = "#222"; + for (let r = 0; r <= ROWS; r++) { + // horizontal lines + ctx.beginPath(); + ctx.moveTo(0, r * BLOCK); + ctx.lineTo(COLS * BLOCK, r * BLOCK); + ctx.stroke(); + } + for (let c = 0; c <= COLS; c++) { + ctx.beginPath(); + ctx.moveTo(c * BLOCK, 0); + ctx.lineTo(c * BLOCK, ROWS * BLOCK); + ctx.stroke(); + } + } + + drawBoard() { + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const val = this.board[r][c]; + if (val) this.fillBlock(c, r, COLORS[val]); + else this.clearBlock(c, r); + } + } + } + + drawPiece() { + if (!this.piece) return; + + for (const cell of this.piece.getCells()) + if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val]); + + let offset: number = this.getGhostOffset(this.piece); + for (const cell of this.piece.getCells()) + if (cell.y + offset >= 0) + this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]); + } + + drawHold() { + if (!this.holdPiece) return; + + this.holdCtx.clearRect(0, 0, 200, 200); + let y: number = 0; + for (const row of this.holdPiece.rotations[0]) { + let x: number = 0; + for (const val of row) { + if (val) { + this.holdCtx.fillStyle = this.canHold + ? COLORS[this.holdPiece.findColorIndex()] + : "gray"; + this.holdCtx.fillRect( + x * BLOCK + + 1 + + (4 - this.holdPiece.rotations[0].length) * 15 + + 10, + y * BLOCK + 1 + 20, + BLOCK - 2, + BLOCK - 2, + ); + } + x++; + } + y++; + } + } + + drawQueue() { + this.queueCtx.clearRect(0, 0, 500, 500); + let placement: number = 0; + console.log(this.nextQueue.slice(0, 5)); + for (const nextPiece of this.nextQueue.slice(0, 5)) { + let y: number = 0; + for (const row of TETROMINOES[nextPiece][0]) { + let x: number = 0; + for (const val of row) { + if (val) { + this.queueCtx.fillStyle = + COLORS[ + ["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1 + ]; + this.queueCtx.fillRect( + x * BLOCK + + 1 + + (4 - TETROMINOES[nextPiece][0].length) * 15 + + 10, + y * BLOCK + + 1 + + placement * 80 + + 20 - + (nextPiece === "I" ? 15 : 0), + BLOCK - 2, + BLOCK - 2, + ); + } + x++; + } + y++; + } + placement++; + } + } + + fillBlock(x: number, y: number, color: string) { + const ctx = this.ctx; + ctx.fillStyle = color; + ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + fillGhostBlock(x: number, y: number, color: string) { + const ctx = this.ctx; + ctx.strokeStyle = color; + ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + + clearBlock(x: number, y: number) { + const ctx = this.ctx; + ctx.clearRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + + drawHUD() { + const ctx = this.ctx; + ctx.fillStyle = "rgba(0,0,0,0.6)"; + ctx.fillRect(4, 4, 120, 60); + ctx.fillStyle = "#fff"; + ctx.font = "12px Kubasta"; + ctx.fillText(`Score: ${this.score}`, 8, 20); + ctx.fillText(`Lines: ${this.lines}`, 8, 36); + ctx.fillText(`Level: ${this.level}`, 8, 52); + + if (this.isPaused) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48); + ctx.fillStyle = "#fff"; + ctx.font = "24px Kubasta"; + ctx.textAlign = "center"; + ctx.fillText( + "PAUSED", + this.canvas.width / 2, + this.canvas.height / 2 + 8, + ); + ctx.textAlign = "start"; + } + + if (this.isGameOver) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72); + ctx.fillStyle = "#fff"; + ctx.font = "28px Kubasta"; + ctx.textAlign = "center"; + ctx.fillText( + "GAME OVER", + this.canvas.width / 2, + this.canvas.height / 2 + 8, + ); + ctx.textAlign = "start"; + } + } + + draw() { + // clear everything + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.fillStyle = "#000"; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.strokeStyle = "#111"; + for (let r = 0; r <= ROWS; r++) { + this.ctx.beginPath(); + this.ctx.moveTo(0, r * BLOCK); + this.ctx.lineTo(COLS * BLOCK, r * BLOCK); + this.ctx.stroke(); + } + for (let c = 0; c <= COLS; c++) { + this.ctx.beginPath(); + this.ctx.moveTo(c * BLOCK, 0); + this.ctx.lineTo(c * BLOCK, ROWS * BLOCK); + this.ctx.stroke(); + } + + this.drawBoard(); + this.drawPiece(); + this.drawHUD(); + } + } + + const game = new Game("board"); + } }