diff --git a/.env.example b/.env.example index a2a1af0..f6d25ac 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,6 @@ GRAPH_PORT=3000 ELK_PORT=5601 -GOOGLE_CALLBACK_URL=https://localhost:8443/api/v1 # the url to which the user will be redirected when it logs with google CALLBACK_REDIR=http://localhost:3000 GOOGLE_CLIENT_SECRET=susAF @@ -29,3 +28,4 @@ EMAIL_TO= USER_URL= AUTH_URL= +CORS_ORIGIN= diff --git a/docker/api-base/compose.yml b/docker/api-base/compose.yml index 85b77c4..bed06c8 100644 --- a/docker/api-base/compose.yml +++ b/docker/api-base/compose.yml @@ -15,6 +15,7 @@ services: - API_TARGET=user - LOG_FILE_PATH=/var/log/log.log - JWT_SECRET=${JWT_SECRET} + - CORS_ORIGIN=${CORS_ORIGIN} restart: unless-stopped auth-api: container_name: transcendence-api-auth @@ -35,6 +36,7 @@ services: - API_TARGET=auth - LOG_FILE_PATH=/var/log/log.log - JWT_SECRET=${JWT_SECRET} + - CORS_ORIGIN=${CORS_ORIGIN} restart: unless-stopped scorestore-api: container_name: transcendence-api-scoreStore diff --git a/flake.lock b/flake.lock index 782aaad..dade030 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1753250450, - "narHash": "sha256-i+CQV2rPmP8wHxj0aq4siYyohHwVlsh40kV89f3nw1s=", + "lastModified": 1756542300, + "narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fc02ee70efb805d3b2865908a13ddd4474557ecf", + "rev": "d7600c775f877cd87b4f5a831c28aa94137377aa", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b61cb1a..fb52ec3 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,7 @@ nodejs_22 pnpm just - foundry + foundry ]; shellHook = '' if [ ! -d node_modules/ ]; then @@ -50,6 +50,7 @@ echo Installing foundry env forge i fi + alias jarvis=just export PATH+=:$(pwd)/node_modules/.bin echo entering ft_trans env ''; diff --git a/package.json b/package.json index 7eedbe9..93287eb 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "dependencies": { "@avalabs/avalanchejs": "^5.0.0", "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.1.0", "@fastify/env": "^5.0.2", "@fastify/jwt": "^9.1.0", "axios": "^1.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91f44c7..0a5a097 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fastify/cookie': specifier: ^11.0.2 version: 11.0.2 + '@fastify/cors': + specifier: ^11.1.0 + version: 11.1.0 '@fastify/env': specifier: ^5.0.2 version: 5.0.2 @@ -255,6 +258,9 @@ packages: '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + '@fastify/cors@11.1.0': + resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==} + '@fastify/deepmerge@2.0.2': resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} @@ -1758,6 +1764,11 @@ snapshots: cookie: 1.0.2 fastify-plugin: 5.0.1 + '@fastify/cors@11.1.0': + dependencies: + fastify-plugin: 5.0.1 + toad-cache: 3.7.0 + '@fastify/deepmerge@2.0.2': {} '@fastify/env@5.0.2': diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 0e491ac..56ebf62 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -1,5 +1,6 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; +import cors from '@fastify/cors' import { register } from './register.js'; import { login } from './login.js'; @@ -23,6 +24,12 @@ authDB.prepareDB(); */ export default async function(fastify, options) { + fastify.register(cors, { + origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + methods: [ "GET", "POST", "DELETE", "OPTIONS" ] + }); + fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET || '123456789101112131415161718192021', cookie: { diff --git a/src/api/user/default.js b/src/api/user/default.js index a5efe65..1c63b59 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,5 +1,6 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; +import cors from '@fastify/cors' import Database from 'better-sqlite3'; var env = process.env.NODE_ENV || 'development'; @@ -56,6 +57,13 @@ const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;' * @param {import('fastify').FastifyPluginOptions} options */ export default async function(fastify, options) { + + fastify.register(cors, { + origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + methods: [ "GET", "POST", "DELETE", "OPTIONS" ] + }); + fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET || '123456789101112131415161718192021', cookie: { diff --git a/src/front/index.html b/src/front/index.html index f2f5a6a..233ae24 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -1,19 +1,56 @@ - - - - Vite + Tailwind Test - - - -
-

Vite + Tailwind

-

🚀 Looks like it's working!

- -
- - + + + + + Vite + Tailwind Test + + + + + + + + + + + + +
+
+ +
+ +
+ +
+ + + + diff --git a/src/front/static/assets/favicon.ico b/src/front/static/assets/favicon.ico new file mode 100644 index 0000000..18945be Binary files /dev/null and b/src/front/static/assets/favicon.ico differ diff --git a/src/front/static/assets/fonts/Kubasta.otf b/src/front/static/assets/fonts/Kubasta.otf new file mode 100644 index 0000000..06001b2 Binary files /dev/null and b/src/front/static/assets/fonts/Kubasta.otf differ diff --git a/src/front/static/assets/fonts/Kubasta.ttf b/src/front/static/assets/fonts/Kubasta.ttf new file mode 100644 index 0000000..18f5b82 Binary files /dev/null and b/src/front/static/assets/fonts/Kubasta.ttf differ 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/css/style.css b/src/front/static/css/style.css new file mode 100644 index 0000000..35cd827 --- /dev/null +++ b/src/front/static/css/style.css @@ -0,0 +1,78 @@ +@import "tailwindcss"; + +@font-face { + font-family: Kubasta; + src: url('../assets/fonts/Kubasta.otf') format("opentype"); +} + +@source inline("space-y-{18,46,102,214,438,886,1782,3574,7158,14326,28662,57334,114678,229366,458742,917494}"); +@source inline("mt-{28,56,84,112}"); + +@theme { + --shadow-2x1: 2px 2px 0px black; + /* + --color-kanel-700: #ac5c24; + */ +} + +.default-border { + @apply border-2 + border-t-neutral-100 border-l-neutral-100 border-r-neutral-400 border-b-neutral-400 + dark:border-t-neutral-500 dark:border-l-neutral-500 dark:border-r-neutral-700 dark:border-b-neutral-700 + ; +} + +.reverse-border { + @apply border-2 + border-t-neutral-400 border-l-neutral-400 border-r-neutral-100 border-b-neutral-100 + dark:border-t-neutral-700 dark:border-l-neutral-700 dark:border-r-neutral-500 dark:border-b-neutral-500 + ; +} + +.input-border { + @apply border-2 + border-t-neutral-950 border-l-neutral-950 border-r-neutral-200 border-b-neutral-200 + dark:border-t-neutral-950 dark:border-l-neutral-950 dark:border-r-neutral-600 dark:border-b-neutral-600 + ; +} + +.default-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-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 new file mode 100644 index 0000000..6533fd8 --- /dev/null +++ b/src/front/static/ts/main.ts @@ -0,0 +1,118 @@ +import { oneko } from "./oneko.ts"; +import Profile from "./views/Profile.ts"; +let profile_view = new Profile; + +export async function isLogged(): Promise { + let uuid_req = await fetch("http://localhost:3001/me", { + method: "GET", + credentials: "include", + }); + if (uuid_req.status === 200) + { + let uuid = await uuid_req.json(); + document.cookie = `uuid=${uuid.user};max-age=${60*60*24*7}`; + return true; + } + else // 401 + { + document.cookie = `uuid=;max-age=0`; + return false; + } +} + +export const navigationManager = url => { + history.pushState(null, null, url); + router(); +}; + +let view; + +const routes = [ + { path: "/", view: () => import("./views/MainMenu.ts") }, + + { path: "/pong", view: () => import("./views/PongMenu.ts") }, + { 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") }, +]; + +const router = async () => { + + const routesMap = routes.map(route => { + return { route: route, isMatch: location.pathname === route.path }; + }); + + let match = routesMap.find(routeMap => routeMap.isMatch); + + if (!match) + match = { route: routes[0], isMatch: true }; + + if (view) + view.running = false; + + //console.log(match); + + const module = await match.route.view(); + view = new module.default(); + + document.querySelector("#app").innerHTML = await view.getHTML(); + 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(); + navigationManager(e.target.href); + } + if (e.target.closest("[data-icon]")) + e.preventDefault(); + }); + + document.body.addEventListener("dblclick", e=> { + if (e.target.closest("[data-icon]")) + { + e.preventDefault(); + navigationManager(e.target.closest("[data-icon]").href); + } + }); + + router(); +}); + +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/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/Aview.ts b/src/front/static/ts/views/Aview.ts new file mode 100644 index 0000000..810d3ab --- /dev/null +++ b/src/front/static/ts/views/Aview.ts @@ -0,0 +1,10 @@ +export default class { + contructor() + { + } + + setTitle(title) { document.title = title; } + + async getHTML() { return ""; } + async run() { } +}; diff --git a/src/front/static/ts/views/Game.ts b/src/front/static/ts/views/Game.ts new file mode 100644 index 0000000..2b73777 --- /dev/null +++ b/src/front/static/ts/views/Game.ts @@ -0,0 +1,289 @@ +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 { + + running: boolean; + + constructor() + { + super(); + this.setTitle("pong (local match)"); + this.running = true; + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ pong_game.ts +
+ + + × +
+
+ + +
+
+
+ + +
+ +
+ +
+ `; + } + + async run() { + dragElement(document.getElementById("window")); + + let start: number = 0; + let elapsed: number; + + let game_playing: boolean = false; + let match_over: boolean = false; + let p1_score: number = 0; + let p2_score: number = 0; + let p1_name: string; + let p2_name: string; + + let countdown: number = 3; + let countdownTimer: number = 0; + + let canvas; + let ctx; + + const paddleOffset: number = 15; + const paddleHeight: number = 100; + const paddleWidth: number = 10; + const ballSize: number = 10; + + const paddleSpeed: number = 727 * 0.69; + let leftPaddleY: number; + let rightPaddleY: number; + let ballX: number; + let ballY: number; + let ballSpeed: number = 200; + let ballSpeedX: number = 300; + let ballSpeedY: number = 10; + + const keys: Record = {}; + + document.addEventListener("keydown", e => { keys[e.key] = true; }); + document.addEventListener("keyup", e => { keys[e.key] = false; }); + + function movePaddles() { + if ((keys["w"] || keys["W"]) && leftPaddleY > 0) + leftPaddleY -= paddleSpeed * elapsed; + if ((keys["s"] || keys["S"]) && leftPaddleY < canvas.height - paddleHeight) + leftPaddleY += paddleSpeed * elapsed; + if (keys["ArrowUp"] && rightPaddleY > 0) + rightPaddleY -= paddleSpeed * elapsed; + if (keys["ArrowDown"] && rightPaddleY < canvas.height - paddleHeight) + rightPaddleY += paddleSpeed * elapsed; + } + + function getBounceVelocity(paddleY) { + const speed = ballSpeed; + const paddleCenterY = paddleY + paddleHeight / 2; + + let n = (ballY - paddleCenterY) / (paddleHeight / 2); + n = Math.max(-1, Math.min(1, n)); + let theta = n * ((75 * Math.PI) / 180); + ballSpeedY = ballSpeed * Math.sin(theta); + } + + function moveBall() { + let length = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY); + let scale = ballSpeed / length; + ballX += (ballSpeedX * scale) * elapsed; + ballY += (ballSpeedY * scale) * elapsed; + + if (ballY <= 0 || ballY >= canvas.height - ballSize) + ballSpeedY *= -1; + + if (ballX <= paddleWidth + paddleOffset && ballX >= paddleOffset && + ballY > leftPaddleY && ballY < leftPaddleY + paddleHeight) + { + ballSpeedX *= -1; + ballX = paddleWidth + paddleOffset; + getBounceVelocity(leftPaddleY); + ballSpeed += 10; + } + + if (ballX >= canvas.width - paddleWidth - ballSize - paddleOffset && ballX <= canvas.width - ballSize - paddleOffset && + ballY > rightPaddleY && ballY < rightPaddleY + paddleHeight) + { + ballSpeedX *= -1; + ballX = canvas.width - paddleWidth - ballSize - paddleOffset; + getBounceVelocity(rightPaddleY); + ballSpeed += 10; + } + + // scoring + if (ballX < 0 || ballX > canvas.width - ballSize) + { + setOnekoState("default"); + game_playing = false; + if (ballX < 0) + p2_score++; + else + p1_score++; + + if (p1_score === 3 || p2_score === 3) + { + // ------------------------------------------------------------------------------------------------------------------------------------------ + // + // insert the fetch to the ScoreStore api here + // + // ------------------------------------------------------------------------------------------------------------------------------------------ + match_over = true; + } + else + { + countdown = 3; + countdownTimer = performance.now(); + } + + ballX = canvas.width / 2; + ballY = canvas.height / 2; + ballSpeed = 200; + ballSpeedX = 300 * ((ballSpeedX > 0) ? 1 : -1); + ballSpeedY = 10; + ballSpeedX = -ballSpeedX; + leftPaddleY = canvas.height / 2 - paddleHeight / 2; + rightPaddleY = canvas.height / 2 - paddleHeight / 2; + } + setBallPos(ballX, ballY); + } + + function draw() { + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = "white"; + ctx.beginPath(); + ctx.setLineDash([5, 10]); + ctx.moveTo(canvas.width / 2, 0); + ctx.lineTo(canvas.width / 2, canvas.height); + ctx.stroke(); + + ctx.fillStyle = "white"; + ctx.fillRect(paddleOffset, leftPaddleY, paddleWidth, paddleHeight); + ctx.fillRect(canvas.width - paddleWidth - paddleOffset, rightPaddleY, paddleWidth, paddleHeight); + + ctx.fillStyle = "white"; + if (game_playing) + ctx.fillRect(ballX, ballY, ballSize, ballSize); + + ctx.font = "24px Kubasta"; + let text_score = `${p1_score} - ${p2_score}`; + ctx.fillText(text_score, canvas.width / 2 - (ctx.measureText(text_score).width / 2), 25); + ctx.fillText(p1_name, canvas.width / 4 - (ctx.measureText(p1_name).width / 2), 45); + ctx.fillText(p2_name, (canvas.width / 4 * 3) - (ctx.measureText(p2_name).width / 2), 45); + + if (match_over) + { + ctx.font = "32px Kubasta"; + const winner = `${p1_score > p2_score ? p1_name : p2_name} won :D`; + ctx.fillText(winner, canvas.width / 2 - (ctx.measureText(winner).width / 2), canvas.height / 2 + 16); + document.getElementById("game-buttons").classList.remove("hidden"); + } + } + + function startCountdown() + { + const now = performance.now(); + if (countdown > 0) + { + if (now - countdownTimer >= 500) + { + countdown--; + countdownTimer = now; + } + ctx.font = "48px Kubasta"; + ctx.fillText(countdown.toString(), canvas.width / 2 - 10, canvas.height / 2 + 24); + } + else if (countdown === 0) + { + ctx.font = "48px Kubasta"; + ctx.fillText("Go!", canvas.width / 2 - 30, canvas.height / 2 + 24); + setTimeout(() => { + game_playing = true; + countdown = -1; + }, 500); + } + } + + const gameLoop = (timestamp: number) => { + elapsed = (timestamp - start) / 1000; + start = timestamp; + if (game_playing) + { + movePaddles(); + moveBall(); + } + draw(); + if (!game_playing) + startCountdown(); + if (this.running) + requestAnimationFrame(gameLoop); + }; + + + document.getElementById("game-retry")?.addEventListener("click", () => { + setOnekoState("pong"); + document.getElementById("game-buttons").classList.add("hidden"); + game_playing = false; + match_over = false; + p1_score = 0; + p2_score = 0; + + countdown = 3; + countdownTimer = performance.now(); + }); + let p1_input = document.getElementById("player1"); + let p2_input = document.getElementById("player2"); + + p2_input.value = "Player 2"; + if (await isLogged()) + p1_input.value = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + else + p1_input.value = "Player 1"; + + document.getElementById("game-start")?.addEventListener("click", () => { + p1_name = p1_input.value; + p2_name = p2_input.value; + document.getElementById("player-inputs").remove(); + + canvas = document.createElement("canvas"); + canvas.id = "gameCanvas"; + canvas.classList.add("reverse-border"); + + document.getElementById("main-div").prepend(canvas); + + ctx = canvas.getContext("2d"); + ctx.canvas.width = 600; + ctx.canvas.height = 600; + + leftPaddleY = canvas.height / 2 - paddleHeight / 2; + rightPaddleY = canvas.height / 2 - paddleHeight / 2; + 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 new file mode 100644 index 0000000..f1b04bd --- /dev/null +++ b/src/front/static/ts/views/LoginPage.ts @@ -0,0 +1,80 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.js" +import { setOnekoState } from "../oneko.ts" +import { isLogged, navigationManager } from "../main.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("login"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ login.ts +
+ + + × +
+
+ +
+

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; + + try { + const data_req = await fetch("http://localhost:3001/login", { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ user: username, password: password }), + }); + const data = await data_req.json(); + + if (data_req.status === 200) + { + isLogged(); + navigationManager("/"); + } + else if (data_req.status === 400) + { + document.getElementById("login-error-message").innerHTML = "error: " + data.error; + document.getElementById("login-error-message").classList.remove("hidden"); + } + else + { + throw new Error("invalid response"); + } + + } + catch (error) + { + console.error(error); + document.getElementById("login-error-message").innerHTML = "error: server error, try again later..."; + document.getElementById("login-error-message").classList.remove("hidden"); + } + }; + + document.getElementById("login-button")?.addEventListener("click", login); + } +} diff --git a/src/front/static/ts/views/MainMenu.ts b/src/front/static/ts/views/MainMenu.ts new file mode 100644 index 0000000..6451c2d --- /dev/null +++ b/src/front/static/ts/views/MainMenu.ts @@ -0,0 +1,34 @@ +import Aview from "./Aview.ts" +import { setOnekoState } from "../oneko.ts" + +export default class extends Aview { + + constructor() + { + 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 new file mode 100644 index 0000000..e7b3bc0 --- /dev/null +++ b/src/front/static/ts/views/PongMenu.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 ` +
+
+ pong_game.ts +
+ + + × +
+
+
+

welcome to pong!! Oo

+ +
+
+ `; + } + async run() { + dragElement(document.getElementById("window")); + } +} diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts new file mode 100644 index 0000000..502b15c --- /dev/null +++ b/src/front/static/ts/views/Profile.ts @@ -0,0 +1,105 @@ +import Aview from "./Aview.ts" +import { isLogged, navigationManager } from "../main.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("profile"); + } + + async getHTML() { + return ` +
+
+
+ +
girls kissing :3
+
+ +
+
+
+ +
+
+
+ `; + } + + open: boolean = false; + + async run() { + 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);*/ + + /*const main = document.getElementById("profile-profile"); + const nametag = main.appendChild(document.createElement("span")); + + nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`; + nametag.classList.add("text-neutral-900", "dark:text-white"); + + 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");*/ + //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 new file mode 100644 index 0000000..67c828f --- /dev/null +++ b/src/front/static/ts/views/RegisterPage.ts @@ -0,0 +1,87 @@ +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 { + + constructor() + { + super(); + this.setTitle("register"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ register.ts +
+ + + × +
+
+ +
+

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; + + try { + const data_req = await fetch("http://localhost:3001/register", { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ user: username, password: password }), + }); + const data = await data_req.json(); + + if (data_req.status === 200) + { + let uuid_req = await fetch("http://localhost:3001/me", { + method: "GET", + credentials: "include", + }); + let uuid = await uuid_req.json(); + document.cookie = `uuid=${uuid.user};max-ages=${60*60*24*7}`; + console.log(document.cookie); + isLogged(); + navigationManager("/"); + } + else if (data_req.status === 400) + { + document.getElementById("login-error-message").innerHTML = "error: " + data.error; + document.getElementById("login-error-message").classList.remove("hidden"); + } + else + { + throw new Error("invalid response"); + } + + } + catch (error) + { + console.error(error); + document.getElementById("login-error-message").innerHTML = "error: server error, try again later..."; + document.getElementById("login-error-message").classList.remove("hidden"); + } + }; + + document.getElementById("register-button")?.addEventListener("click", login); + } +} diff --git a/src/front/static/ts/views/Tetris.ts b/src/front/static/ts/views/Tetris.ts new file mode 100644 index 0000000..f01eb08 --- /dev/null +++ b/src/front/static/ts/views/Tetris.ts @@ -0,0 +1,706 @@ +import Aview from "./Aview.ts"; +import { dragElement } from "./drag.js"; + +export default class extends Aview { + running: boolean; + + constructor() { + super(); + this.setTitle("tetris (local match)"); + this.running = true; + } + + async getHTML() { + return ` +
+
+ pong_game.ts +
+ + + × +
+
+ + +
+
+ + + +
+ +
+
+ `; + } + + 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"); + } +} 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 new file mode 100644 index 0000000..1f798ae --- /dev/null +++ b/src/front/static/ts/views/TournamentMenu.ts @@ -0,0 +1,143 @@ +import Aview from "./Aview.ts" +import { setOnekoState, setBallPos } from "../oneko.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("Tournament"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+

how many players ?

+
+ + +
+
+
+ `; + } + + async run() { + const generateBracket = async (playerCount: number) => { + document.getElementById("bracket").innerHTML = ""; + + const rounds = Math.ceil(Math.log2(playerCount)); + const totalSlots = 2 ** rounds; + const byes = totalSlots - playerCount; + + let odd = 0; + if (playerCount % 2) + { + console.error("odd numbers are temporarily invalids"); + return ; + /*++odd; + --playerCount;*/ + } + + let notPowPlayersCount = 0; + + if ((playerCount & (playerCount - 1)) != 0) + notPowPlayersCount = playerCount - (2 ** Math.floor(Math.log2(playerCount))); + + + let initialPlayers = Array.from({ length: 2 ** Math.floor(Math.log2(playerCount))}, (_, i) => `Player ${i + 1}`); + playerCount = 2 ** Math.floor(Math.log2(playerCount)); + //let initialPlayers = Array.from({ length: playerCount }, (_, i) => `Player ${i + 1}`); + + const bracketWrapper = document.createElement("div"); + bracketWrapper.className = "flex space-x-8 overflow-x-auto"; + + // Round 0: Player input column + const playerInputColumn = document.createElement("div"); + playerInputColumn.className = `flex flex-col mt-${(notPowPlayersCount + odd) * 28} space-y-4`; + + initialPlayers.forEach((name, i) => { + const input = document.createElement("input"); + input.type = "text"; + input.id = `playerName${i}`; + input.value = ""; + input.placeholder = name; + input.className = + "w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200"; + playerInputColumn.appendChild(input); + }); + + bracketWrapper.appendChild(playerInputColumn); + + let currentRound = initialPlayers; + let previousPadding = 4; + for (let round = 1; round <= rounds; round++) + { + const roundColumn = document.createElement("div"); + previousPadding = previousPadding * 2 + 10 + roundColumn.className = `flex flex-col justify-center space-y-${previousPadding}`; + + const nextRound: string[] = []; + + if (!notPowPlayersCount) + { + if (odd) + { + const input = document.createElement("input"); + input.type = "text"; + input.id = `playerName${playerCount}`; + input.value = ""; + input.placeholder = `Player ${++playerCount}`; + input.className = + "w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200"; + roundColumn.appendChild(input); + odd--; + nextRound.push(""); + } + } + + while (notPowPlayersCount) + { + const input = document.createElement("input"); + input.type = "text"; + input.id = `playerName${playerCount}`; + input.value = ""; + input.placeholder = `Player ${++playerCount}`; + input.className = + "w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200"; + roundColumn.appendChild(input); + --notPowPlayersCount; + nextRound.push(""); + } + + for (let i = 0; i < currentRound.length; i += 2) + { + const p1 = currentRound[i]; + const p2 = currentRound[i + 1]; + + const matchDiv = document.createElement("div"); + matchDiv.className = + "w-32 h-10 flex items-center justify-center bg-white border rounded shadow text-center text-sm"; + + matchDiv.textContent = ""; + nextRound.push(""); + + roundColumn.appendChild(matchDiv); + } + + bracketWrapper.appendChild(roundColumn); + currentRound = nextRound; + } + + document.getElementById("bracket").appendChild(bracketWrapper); + }; + + document.getElementById("bracket-generate")?.addEventListener("click", () => { + const input: HTMLInputElement = document.getElementById("playerNumber") as HTMLInputElement; + generateBracket(+input.value); + }); + + + } +} 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; + } +} diff --git a/src/front/style.css b/src/front/style.css deleted file mode 100644 index b5c61c9..0000000 --- a/src/front/style.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tailwind.config.js b/tailwind.config.js index d2fdab4..9bd8368 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,5 @@ export default { - content: ['./src/front/**/*.{html,js}'], + content: ['./src/front/**/*.{html,js,ts,css}'], theme: { extend: {}, },