From 7d70921297e652e4dbe4554ae1472f55e3a72711 Mon Sep 17 00:00:00 2001 From: y-syo Date: Wed, 8 Oct 2025 22:08:03 +0200 Subject: [PATCH] =?UTF-8?q?=E3=80=8C=E2=9C=A8=E3=80=8D=20feat(src.front):?= =?UTF-8?q?=20:D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/index.html | 19 +- src/front/static/assets/pong.svg | 17 + src/front/static/assets/tetrio.svg | 114 ++++ src/front/static/ts/main.ts | 23 +- src/front/static/ts/oneko.ts | 273 +++++++++ src/front/static/ts/views/Game.ts | 20 +- src/front/static/ts/views/LoginPage.ts | 2 + src/front/static/ts/views/MainMenu.ts | 6 +- src/front/static/ts/views/PongMenu.ts | 12 +- src/front/static/ts/views/RegisterPage.ts | 2 + src/front/static/ts/views/Tetris.ts | 590 ++++++++++++++++++++ src/front/static/ts/views/TetrisMenu.ts | 42 ++ src/front/static/ts/views/TournamentMenu.ts | 2 + src/front/static/ts/views/drag.ts | 43 ++ 14 files changed, 1152 insertions(+), 13 deletions(-) create mode 100644 src/front/static/assets/pong.svg create mode 100644 src/front/static/assets/tetrio.svg create mode 100644 src/front/static/ts/oneko.ts create mode 100644 src/front/static/ts/views/Tetris.ts create mode 100644 src/front/static/ts/views/TetrisMenu.ts create mode 100644 src/front/static/ts/views/drag.ts diff --git a/src/front/index.html b/src/front/index.html index 97d50d2..28b2775 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -13,7 +13,24 @@ - + + + +
+ + + + pong_game.ts + + + + tetris_game.ts + + + + tetr.io + +
diff --git a/src/front/static/assets/pong.svg b/src/front/static/assets/pong.svg new file mode 100644 index 0000000..6da6f5c --- /dev/null +++ b/src/front/static/assets/pong.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/static/assets/tetrio.svg b/src/front/static/assets/tetrio.svg new file mode 100644 index 0000000..7965017 --- /dev/null +++ b/src/front/static/assets/tetrio.svg @@ -0,0 +1,114 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/front/static/ts/main.ts b/src/front/static/ts/main.ts index 8c58b42..442be9e 100644 --- a/src/front/static/ts/main.ts +++ b/src/front/static/ts/main.ts @@ -1,3 +1,5 @@ +import { oneko } from "./oneko.ts"; + export async function isLogged(): boolean { let uuid_req = await fetch("http://localhost:3001/me", { method: "GET", @@ -82,12 +84,14 @@ const routes = [ { path: "/pong/local", view: () => import("./views/Game.ts") }, { path: "/pong/tournament", view: () => import("./views/TournamentMenu.ts") }, + { path: "/tetris", view: () => import("./views/TetrisMenu.ts") }, + { path: "/tetris/solo", view: () => import("./views/Tetris.ts") }, + { path: "/tetris/versus", view: () => import("./views/Tetris.ts") }, + { path: "/login", view: () => import("./views/LoginPage.ts") }, { path: "/register", view: () => import("./views/RegisterPage.ts") }, { path: "/profile", view: () => import("./views/Profile.ts") }, - - { path: "/tetris", view: () => import("./views/Tetris.ts") }, ]; const router = async () => { @@ -124,7 +128,22 @@ document.addEventListener("DOMContentLoaded", () => { e.preventDefault(); navigationManager(e.target.href); } + if (e.target.closest("[data-icon]")) + { + console.log("xd"); + e.preventDefault(); + } + }); + + document.body.addEventListener("dblclick", e=> { + if (e.target.closest("[data-icon]")) + { + e.preventDefault(); + navigationManager(e.target.closest("[data-icon]").href); + } }); router(); }); + +oneko(); diff --git a/src/front/static/ts/oneko.ts b/src/front/static/ts/oneko.ts new file mode 100644 index 0000000..9c579d4 --- /dev/null +++ b/src/front/static/ts/oneko.ts @@ -0,0 +1,273 @@ +// oneko.js: https://github.com/adryd325/oneko.js +// edited by yosyo specificely for knl_meowscendence. + +let oneko_state: number = 0; // 0 = normal, 1 = pong, 2 = tetris +let mousePosX: number = 0; +let mousePosY: number = 0; +let offsetX: number = 0; +let offsetY: number = 0; + +export function setOnekoState(state: string) { + switch (state) { + case "pong": + oneko_state = 1; + break; + case "tetris": + oneko_state = 2; + break; + default: + oneko_state = 0; + } +} + +export function setOnekoOffset() { + if (oneko_state == 1) + { + offsetX = document.getElementById("window").offsetLeft + 44; + offsetY = document.getElementById("window").offsetTop + 44 + 24; + console.log(offsetX, offsetY); + } +} + +export function setBallPos(x: number, y: number) { + mousePosX = x + offsetX; + mousePosY = y + offsetY; +} + +export function oneko() { + const isReducedMotion = + window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || + window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; + + if (isReducedMotion) return; + + const nekoEl = document.createElement("div"); + + let nekoPosX = 256; + let nekoPosY = 256; + + let frameCount = 0; + let idleTime = 0; + let idleAnimation = null; + let idleAnimationFrame = 0; + + const nekoSpeed = 10; + const spriteSets = { + idle: [[-3, -3]], + alert: [[-7, -3]], + scratchSelf: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + scratchWallN: [ + [0, 0], + [0, -1], + ], + scratchWallS: [ + [-7, -1], + [-6, -2], + ], + scratchWallE: [ + [-2, -2], + [-2, -3], + ], + scratchWallW: [ + [-4, 0], + [-4, -1], + ], + tired: [[-3, -2]], + sleeping: [ + [-2, 0], + [-2, -1], + ], + N: [ + [-1, -2], + [-1, -3], + ], + NE: [ + [0, -2], + [0, -3], + ], + E: [ + [-3, 0], + [-3, -1], + ], + SE: [ + [-5, -1], + [-5, -2], + ], + S: [ + [-6, -3], + [-7, -2], + ], + SW: [ + [-5, -3], + [-6, -1], + ], + W: [ + [-4, -2], + [-4, -3], + ], + NW: [ + [-1, 0], + [-1, -1], + ], + }; + + function init() { + nekoEl.id = "oneko"; + nekoEl.ariaHidden = true; + nekoEl.style.width = "32px"; + nekoEl.style.height = "32px"; + nekoEl.style.position = "fixed"; + nekoEl.style.pointerEvents = "none"; + nekoEl.style.imageRendering = "pixelated"; + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + nekoEl.style.zIndex = 2147483647; + + let nekoFile = "https://kanel.ovh/assets/oneko.gif" + const curScript = document.currentScript + if (curScript && curScript.dataset.cat) { + nekoFile = curScript.dataset.cat + } + nekoEl.style.backgroundImage = `url(${nekoFile})`; + + document.body.appendChild(nekoEl); + + document.addEventListener("mousemove", function (event) { + if (oneko_state == 0) + { + mousePosX = event.clientX; + mousePosY = event.clientY; + } + }); + + window.requestAnimationFrame(onAnimationFrame); + } + + let lastFrameTimestamp; + + function onAnimationFrame(timestamp) { + // Stops execution if the neko element is removed from DOM + if (!nekoEl.isConnected) { + return; + } + if (!lastFrameTimestamp) { + lastFrameTimestamp = timestamp; + } + if (timestamp - lastFrameTimestamp > 100) { + lastFrameTimestamp = timestamp + frame() + } + window.requestAnimationFrame(onAnimationFrame); + } + + function setSprite(name, frame) { + const sprite = spriteSets[name][frame % spriteSets[name].length]; + nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; + } + + function resetIdleAnimation() { + idleAnimation = null; + idleAnimationFrame = 0; + } + + function idle() { + idleTime += 1; + + // every ~ 20 seconds + if ( + idleTime > 10 && + Math.floor(Math.random() * 200) == 0 && + idleAnimation == null + ) { + let avalibleIdleAnimations = ["sleeping", "scratchSelf"]; + if (nekoPosX < 32) { + avalibleIdleAnimations.push("scratchWallW"); + } + if (nekoPosY < 32) { + avalibleIdleAnimations.push("scratchWallN"); + } + if (nekoPosX > window.innerWidth - 32) { + avalibleIdleAnimations.push("scratchWallE"); + } + if (nekoPosY > window.innerHeight - 32) { + avalibleIdleAnimations.push("scratchWallS"); + } + idleAnimation = + avalibleIdleAnimations[ + Math.floor(Math.random() * avalibleIdleAnimations.length) + ]; + } + + switch (idleAnimation) { + case "sleeping": + if (idleAnimationFrame < 8) { + setSprite("tired", 0); + break; + } + setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); + if (idleAnimationFrame > 192) { + resetIdleAnimation(); + } + break; + case "scratchWallN": + case "scratchWallS": + case "scratchWallE": + case "scratchWallW": + case "scratchSelf": + setSprite(idleAnimation, idleAnimationFrame); + if (idleAnimationFrame > 9) { + resetIdleAnimation(); + } + break; + default: + setSprite("idle", 0); + return; + } + idleAnimationFrame += 1; + } + + function frame() { + frameCount += 1; + const diffX = nekoPosX - mousePosX; + const diffY = nekoPosY - mousePosY; + const distance = Math.sqrt(diffX ** 2 + diffY ** 2); + + if (distance < nekoSpeed || distance < 48) { + idle(); + return; + } + + idleAnimation = null; + idleAnimationFrame = 0; + + if (idleTime > 1) { + setSprite("alert", 0); + // count down after being alerted before moving + idleTime = Math.min(idleTime, 7); + idleTime -= 1; + return; + } + + let direction; + direction = diffY / distance > 0.5 ? "N" : ""; + direction += diffY / distance < -0.5 ? "S" : ""; + direction += diffX / distance > 0.5 ? "W" : ""; + direction += diffX / distance < -0.5 ? "E" : ""; + setSprite(direction, frameCount); + + nekoPosX -= (diffX / distance) * nekoSpeed; + nekoPosY -= (diffY / distance) * nekoSpeed; + + nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16); + nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16); + + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + } + + init(); +} diff --git a/src/front/static/ts/views/Game.ts b/src/front/static/ts/views/Game.ts index 36cf348..2b73777 100644 --- a/src/front/static/ts/views/Game.ts +++ b/src/front/static/ts/views/Game.ts @@ -1,5 +1,7 @@ import Aview from "./Aview.ts" import { isLogged } from "../main.js" +import { dragElement } from "./drag.js" +import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts" export default class extends Aview { @@ -10,13 +12,14 @@ export default class extends Aview { super(); this.setTitle("pong (local match)"); this.running = true; + setOnekoState("default"); } async getHTML() { return ` -
-
- knl_meowscendence +
+
+ pong_game.ts
@@ -34,14 +37,16 @@ export default class extends Aview {
`; } async run() { + dragElement(document.getElementById("window")); + let start: number = 0; let elapsed: number; @@ -128,6 +133,7 @@ export default class extends Aview { // scoring if (ballX < 0 || ballX > canvas.width - ballSize) { + setOnekoState("default"); game_playing = false; if (ballX < 0) p2_score++; @@ -158,6 +164,7 @@ export default class extends Aview { leftPaddleY = canvas.height / 2 - paddleHeight / 2; rightPaddleY = canvas.height / 2 - paddleHeight / 2; } + setBallPos(ballX, ballY); } function draw() { @@ -235,6 +242,7 @@ export default class extends Aview { document.getElementById("game-retry")?.addEventListener("click", () => { + setOnekoState("pong"); document.getElementById("game-buttons").classList.add("hidden"); game_playing = false; match_over = false; @@ -273,6 +281,8 @@ export default class extends Aview { ballX = canvas.width / 2; ballY = canvas.height / 2; + setOnekoState("pong"); + setOnekoOffset(); requestAnimationFrame(gameLoop); }); } diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index 7b069c6..bf30229 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 { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" export default class extends Aview { @@ -7,6 +8,7 @@ export default class extends Aview { { super(); this.setTitle("login"); + setOnekoState("default"); } async getHTML() { diff --git a/src/front/static/ts/views/MainMenu.ts b/src/front/static/ts/views/MainMenu.ts index d6d70d8..6451c2d 100644 --- a/src/front/static/ts/views/MainMenu.ts +++ b/src/front/static/ts/views/MainMenu.ts @@ -1,4 +1,5 @@ import Aview from "./Aview.ts" +import { setOnekoState } from "../oneko.ts" export default class extends Aview { @@ -6,12 +7,13 @@ export default class extends Aview { { super(); this.setTitle("knl is trans(cendence)"); + setOnekoState("default"); } async getHTML() { //
return ` -
+ `; } } diff --git a/src/front/static/ts/views/PongMenu.ts b/src/front/static/ts/views/PongMenu.ts index 91efa38..e7b3bc0 100644 --- a/src/front/static/ts/views/PongMenu.ts +++ b/src/front/static/ts/views/PongMenu.ts @@ -1,4 +1,6 @@ import Aview from "./Aview.ts" +import { dragElement } from "./drag.js" +import { setOnekoState } from "../oneko.ts" export default class extends Aview { @@ -6,12 +8,13 @@ export default class extends Aview { { super(); this.setTitle("knl is trans(cendence)"); + setOnekoState("default"); } async getHTML() { return ` -
-
+
+
pong_game.ts
@@ -20,7 +23,7 @@ export default class extends Aview {
-

pong is funny yay

+

welcome to pong!! Oo

`; } + async run() { + dragElement(document.getElementById("window")); + } } diff --git a/src/front/static/ts/views/RegisterPage.ts b/src/front/static/ts/views/RegisterPage.ts index 96f0d69..ab30708 100644 --- a/src/front/static/ts/views/RegisterPage.ts +++ b/src/front/static/ts/views/RegisterPage.ts @@ -1,4 +1,5 @@ import Aview from "./Aview.ts" +import { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" export default class extends Aview { @@ -7,6 +8,7 @@ export default class extends Aview { { super(); this.setTitle("register"); + setOnekoState("default"); } async getHTML() { diff --git a/src/front/static/ts/views/Tetris.ts b/src/front/static/ts/views/Tetris.ts new file mode 100644 index 0000000..47bc4b3 --- /dev/null +++ b/src/front/static/ts/views/Tetris.ts @@ -0,0 +1,590 @@ +import Aview from "./Aview.ts" + +export default class extends Aview { + + running: boolean; + + constructor() + { + super(); + this.setTitle("tetris (local match)"); + this.running = true; + } + + async getHTML() { + return ` +
+ + + +
+ + +
+
+ `; + } + + 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; + ctx: 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'); + if (!ctx) + throw new Error('2D context not available'); + this.ctx = ctx; + + 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.canHold = false; + } + + 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; + } + } + + 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; + } + + lockPiece() { + if (!this.piece) + return; + 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; + 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; + } + + registerListeners() { + window.addEventListener('keydown', (e) => { + 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; + }); + } + + 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]); + } + + 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); + /*ctx.strokeStyle = '#111'; + 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); + } + }); + } +} diff --git a/src/front/static/ts/views/TetrisMenu.ts b/src/front/static/ts/views/TetrisMenu.ts new file mode 100644 index 0000000..8646f83 --- /dev/null +++ b/src/front/static/ts/views/TetrisMenu.ts @@ -0,0 +1,42 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.js" +import { setOnekoState } from "../oneko.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("knl is trans(cendence)"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ tetris_game.ts +
+ + + × +
+
+
+

welcome to tetris! :D

+ +
+
+ `; + } + async run() { + dragElement(document.getElementById("window")); + } +} diff --git a/src/front/static/ts/views/TournamentMenu.ts b/src/front/static/ts/views/TournamentMenu.ts index c36b3d7..1f798ae 100644 --- a/src/front/static/ts/views/TournamentMenu.ts +++ b/src/front/static/ts/views/TournamentMenu.ts @@ -1,4 +1,5 @@ import Aview from "./Aview.ts" +import { setOnekoState, setBallPos } from "../oneko.ts" export default class extends Aview { @@ -6,6 +7,7 @@ export default class extends Aview { { super(); this.setTitle("Tournament"); + setOnekoState("default"); } async getHTML() { diff --git a/src/front/static/ts/views/drag.ts b/src/front/static/ts/views/drag.ts new file mode 100644 index 0000000..d1d7b32 --- /dev/null +++ b/src/front/static/ts/views/drag.ts @@ -0,0 +1,43 @@ +import { setOnekoOffset } from "../oneko.ts"; + +export function dragElement(el) { + var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + if (document.getElementById(el.id + "-header")) { + // if present, the header is where you move the DIV from: + document.getElementById(el.id + "-header").onmousedown = dragMouseDown; + } else { + // otherwise, move the DIV from anywhere inside the DIV: + el.onmousedown = dragMouseDown; + } + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + // get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + // call a function whenever the cursor moves: + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + // calculate the new cursor position: + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + // set the element's new position: + el.style.top = (el.offsetTop - pos2) + "px"; + el.style.left = (el.offsetLeft - pos1) + "px"; + setOnekoOffset(); + } + + function closeDragElement() { + // stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + } +}