diff --git a/src/front/index.html b/src/front/index.html index 233ae24..6471bae 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -42,8 +42,8 @@
|
- rusty - tetris + rusty + tetris-guideline.pdf
12:37 diff --git a/src/front/static/css/style.css b/src/front/static/css/style.css index 35cd827..56bb708 100644 --- a/src/front/static/css/style.css +++ b/src/front/static/css/style.css @@ -1,4 +1,13 @@ @import "tailwindcss"; +@layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} @font-face { font-family: Kubasta; diff --git a/src/front/static/ts/main.ts b/src/front/static/ts/main.ts index 112b5a9..5301e4e 100644 --- a/src/front/static/ts/main.ts +++ b/src/front/static/ts/main.ts @@ -1,6 +1,6 @@ import { oneko } from "./oneko.ts"; -import Profile from "./views/Profile.ts"; -let profile_view = new Profile; +import ProfileMenu from "./views/ProfileMenu.ts"; +let profile_view = new ProfileMenu; export async function isLogged(): Promise { let uuid_req = await fetch("http://localhost:3001/me", { @@ -40,7 +40,9 @@ const routes = [ { path: "/login", view: () => import("./views/LoginPage.ts") }, { path: "/register", view: () => import("./views/RegisterPage.ts") }, - { path: "/friends", view: () => import("./views/Friends.ts") } + { path: "/friends", view: () => import("./views/Friends.ts") }, + { path: "/profile", view: () => import("./views/Profile.ts") }, + { path: "/settings", view: () => import("./views/Settings.ts") }, ]; const router = async () => { @@ -97,7 +99,8 @@ document.addEventListener("DOMContentLoaded", () => { oneko(); -function updateClock() { +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"); @@ -113,3 +116,5 @@ function updateClock() { setInterval(updateClock, 5000); updateClock(); + +oneko(); diff --git a/src/front/static/ts/oneko.ts b/src/front/static/ts/oneko.ts index 9c579d4..a2a159b 100644 --- a/src/front/static/ts/oneko.ts +++ b/src/front/static/ts/oneko.ts @@ -18,20 +18,23 @@ export function setOnekoState(state: string) { default: oneko_state = 0; } + return; } export function setOnekoOffset() { - if (oneko_state == 1) + if (oneko_state != 0) { offsetX = document.getElementById("window").offsetLeft + 44; offsetY = document.getElementById("window").offsetTop + 44 + 24; - console.log(offsetX, offsetY); } + return; } -export function setBallPos(x: number, y: number) { +export function setBallPos(x: number, y: number) +{ mousePosX = x + offsetX; mousePosY = y + offsetY; + return; } export function oneko() { @@ -39,7 +42,7 @@ export function oneko() { window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; - if (isReducedMotion) return; + if (isReducedMotion) return ; const nekoEl = document.createElement("div"); @@ -141,15 +144,15 @@ export function oneko() { { mousePosX = event.clientX; mousePosY = event.clientY; - } + } }); window.requestAnimationFrame(onAnimationFrame); } - let lastFrameTimestamp; + let lastFrameTimestamp: number; - function onAnimationFrame(timestamp) { + function onAnimationFrame(timestamp: number) { // Stops execution if the neko element is removed from DOM if (!nekoEl.isConnected) { return; diff --git a/src/front/static/ts/views/Friends.ts b/src/front/static/ts/views/Friends.ts index a47cf86..90ff80b 100644 --- a/src/front/static/ts/views/Friends.ts +++ b/src/front/static/ts/views/Friends.ts @@ -1,12 +1,13 @@ import Aview from "./Aview.ts" import { setOnekoState } from "../oneko.ts" import { dragElement } from "./drag.ts"; +import { isLogged, navigationManager } from "../main.ts" export default class extends Aview { constructor() { super(); - this.setTitle("Friends list"); + this.setTitle("friends list"); setOnekoState("default"); } @@ -26,22 +27,47 @@ export default class extends Aview {
-
- + +
+ +
`; } async run() { + if (!await isLogged()) + navigationManager("/"); + + dragElement(document.getElementById("window")); let uuid: String; uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + const n_friends = (document.getElementById("friends_n") as HTMLParagraphElement); + const friends_error_message = (document.getElementById("friends-error-message") as HTMLParagraphElement); + const friends_message = (document.getElementById("friends-msg") as HTMLParagraphElement) + const friends_list = (document.getElementById("friends_list") as HTMLUListElement); + const new_friend = (document.getElementById("new-friend") as HTMLInputElement); + const add_friend_err = (document.getElementById("add-friend-err") as HTMLParagraphElement); + const add_friend_msg = (document.getElementById("add-friend-msg") as HTMLParagraphElement); + + async function removeFriend(name) { + const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/" + name, { + method: "DELETE", + credentials: "include", + }); + if (data_req.status === 200) { + console.log("friends removed success fully"); + } else { + console.log("could not remove friend"); + } + } const list_friends = async () => { const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/count", { @@ -54,7 +80,7 @@ export default class extends Aview { let data = await data_req.json(); if (data.n_friends > 0) { - const list_req = await fetch("http://localhost:3002/users/" + uuid + "/friends?iStart=0&iEnd=20", { + const list_req = await fetch("http://localhost:3002/users/" + uuid + "/friends?iStart=0&iEnd=2147483647", { method: "GET", headers: { "Content-Type": "application/json", @@ -70,8 +96,21 @@ export default class extends Aview { let list = (await list_req.json()).friends as JSON; for (let i = 0; i < data.n_friends; i++) { - let new_friends = document.createElement('li') - new_friends.innerHTML = "- " + list[i].friendName; + let new_friends = document.createElement('li'); + + const span = document.createElement('span'); + span.textContent = list[i].friendName; + + const but = document.createElement('button'); + but.textContent = "-"; + but.className = "bg-red-500 text-white px-2 py-0 rounded hover:bg-red-600"; + but.onclick = function() { + removeFriend(list[i].friendName); + list_friends() + } + + new_friends.appendChild(span); + new_friends.appendChild(but); friends_list.appendChild(new_friends); } } else { @@ -80,16 +119,13 @@ export default class extends Aview { friends_error_message.classList.remove("hidden"); } } else { - friends_error_message.innerHTML = "failed to fetch friends"; - friends_error_message.classList.remove("hidden"); + friends_message.innerHTML = "you have no friends D:"; + friends_message.classList.remove("hidden"); } } + const add_friend = async () => { - const new_friend = (document.getElementById("new-friend") as HTMLInputElement); - const add_friend_err = (document.getElementById("add-friend-err") as HTMLParagraphElement); - const add_friend_msg = (document.getElementById("add-friend-msg") as HTMLParagraphElement); - const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/" + new_friend.value, { method: "POST", credentials: "include", @@ -105,39 +141,13 @@ export default class extends Aview { add_friend_err.classList.remove("hidden"); if (!add_friend_msg.classList.contains("hidden")) add_friend_msg.classList.add("hidden") + list_friends() + return } list_friends() + new_friend.value = ''; } - const rm_friend = async () => { - const new_friend = (document.getElementById("new-friend") as HTMLInputElement); - const add_friend_err = (document.getElementById("add-friend-err") as HTMLParagraphElement); - const add_friend_msg = (document.getElementById("add-friend-msg") as HTMLParagraphElement); - - const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/" + new_friend.value, { - method: "DELETE", - credentials: "include", - }); - let data = await data_req.json() - if (data_req.status === 200) { - add_friend_msg.innerHTML = data.msg; - add_friend_msg.classList.remove("hidden"); - if (!add_friend_err.classList.contains("hidden")) - add_friend_err.classList.add("hidden") - } else { - add_friend_err.innerHTML = data.error; - add_friend_err.classList.remove("hidden"); - if (!add_friend_msg.classList.contains("hidden")) - add_friend_msg.classList.add("hidden") - } - list_friends() - } - - dragElement(document.getElementById("window")); - const n_friends = (document.getElementById("friends_n") as HTMLParagraphElement); - const friends_error_message = (document.getElementById("friends-error-message") as HTMLParagraphElement); - const friends_list = (document.getElementById("friends_list") as HTMLUListElement); - try { const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/count", { method: "GET", @@ -158,6 +168,5 @@ export default class extends Aview { friends_error_message.classList.remove("hidden"); } document.getElementById("add-friends-button")?.addEventListener("click", add_friend); - document.getElementById("rm-friends-button")?.addEventListener("click", rm_friend); } } diff --git a/src/front/static/ts/views/Game.ts b/src/front/static/ts/views/Game.ts index 2b73777..5e29d7b 100644 --- a/src/front/static/ts/views/Game.ts +++ b/src/front/static/ts/views/Game.ts @@ -4,7 +4,7 @@ import { dragElement } from "./drag.js" import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts" export default class extends Aview { - + running: boolean; constructor() @@ -27,7 +27,7 @@ export default class extends Aview { - +
@@ -46,6 +46,7 @@ export default class extends Aview { async run() { dragElement(document.getElementById("window")); + let uuid: string; let start: number = 0; let elapsed: number; @@ -103,7 +104,7 @@ export default class extends Aview { ballSpeedY = ballSpeed * Math.sin(theta); } - function moveBall() { + async function moveBall() { let length = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY); let scale = ballSpeed / length; ballX += (ballSpeedX * scale) * elapsed; @@ -142,6 +143,17 @@ export default class extends Aview { if (p1_score === 3 || p2_score === 3) { + console.log(isLogged()); + if (await isLogged()) + { + let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + fetch(`http://localhost:3002/users/${uuid}/matchHistory`, { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ "opponent": p2_name, "myScore": p1_score, "opponentScore": p2_score }) + }); + } // ------------------------------------------------------------------------------------------------------------------------------------------ // // insert the fetch to the ScoreStore api here @@ -225,13 +237,13 @@ export default class extends Aview { } } - const gameLoop = (timestamp: number) => { + const gameLoop = async (timestamp: number) => { elapsed = (timestamp - start) / 1000; start = timestamp; if (game_playing) { movePaddles(); - moveBall(); + await moveBall(); } draw(); if (!game_playing) @@ -257,13 +269,26 @@ export default class extends Aview { p2_input.value = "Player 2"; if (await isLogged()) - p1_input.value = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + { + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + 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(); + p1_input.value = userdata.displayName; + } else p1_input.value = "Player 1"; document.getElementById("game-start")?.addEventListener("click", () => { - p1_name = p1_input.value; - p2_name = p2_input.value; + p1_name = p1_input.value.length > 16 ? p1_input.value.substring(0, 16) + "." : p1_input.value; + p2_name = p2_input.value.length > 16 ? p2_input.value.substring(0, 16) + "." : p2_input.value; document.getElementById("player-inputs").remove(); canvas = document.createElement("canvas"); @@ -272,7 +297,7 @@ export default class extends Aview { document.getElementById("main-div").prepend(canvas); - ctx = canvas.getContext("2d"); + ctx = canvas.getContext("2d", {alpha: false}); ctx.canvas.width = 600; ctx.canvas.height = 600; diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index f1b04bd..0311473 100644 --- a/src/front/static/ts/views/LoginPage.ts +++ b/src/front/static/ts/views/LoginPage.ts @@ -24,14 +24,30 @@ export default class extends Aview {
-
-

welcome back ! please login.

- - - -
- -
+
+
+

welcome back ! please login.

+ + + +
+ +
+ +
+ + +
+
`; } @@ -49,7 +65,6 @@ export default class extends Aview { credentials: "include", body: JSON.stringify({ user: username, password: password }), }); - const data = await data_req.json(); if (data_req.status === 200) { @@ -58,6 +73,7 @@ export default class extends Aview { } else if (data_req.status === 400) { + const data = await data_req.json(); document.getElementById("login-error-message").innerHTML = "error: " + data.error; document.getElementById("login-error-message").classList.remove("hidden"); } diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts index 502b15c..e233f52 100644 --- a/src/front/static/ts/views/Profile.ts +++ b/src/front/static/ts/views/Profile.ts @@ -1,29 +1,38 @@ import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; +import { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" + export default class extends Aview { constructor() { super(); this.setTitle("profile"); + setOnekoState("default"); } async getHTML() { return ` -
-
-
- -
girls kissing :3
+
+
+ profile.ts +
+ + + ×
- -
-
+
+
+
+
-
@@ -31,43 +40,15 @@ export default class extends Aview { `; } - open: boolean = false; + async run() { + if (!await isLogged()) + navigationManager("/"); - 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(); + dragElement(document.getElementById("window")); + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; - 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}`, { + const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { method: "GET", credentials: "include", }); @@ -78,28 +59,56 @@ export default class extends Aview { } let userdata = await userdata_req.json(); - console.log(userdata_req);*/ + const matchCount_req = await fetch(`http://localhost:3002/users/${uuid}/matchHistory/count`, { + method: "GET", + credentials: "include", + }); + const matchCount = await matchCount_req.json(); - /*const main = document.getElementById("profile-profile"); - const nametag = main.appendChild(document.createElement("span")); + const matches_req = await fetch(`http://localhost:3002/users/${uuid}/matchHistory?iStart=0&iEnd=${matchCount.n_matches}`, { + method: "GET", + credentials: "include", + }); + const matches = await matches_req.json(); - nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`; + const main = document.getElementById("profile-scorelist"); + if (!main) + return console.error("what"); + + console.log(matches); + if (matches.matchHistory) + { + for (let match of matches.matchHistory) + { + const newEntry = document.createElement("li"); + newEntry.classList.add("m-2", "default-button", "bg-neutral-200", "dark:bg-neutral-800", "text-neutral-900", "dark:text-white"); + newEntry.innerHTML = match.score.p1Score > match.score.p2Score ? `${match.score.p1} - winner` : `${match.score.p2} - winner`; + main.insertBefore(newEntry, main.firstChild); + console.log(match.tx); + } + } + + const profile = document.getElementById("profile-profile"); + if (!profile) return; + + const picture = profile.appendChild(document.createElement("img")); + picture.src = "https://api.kanel.ovh/pp"; + picture.classList.add("text-neutral-900", "dark:text-white", "center", "h-18", "w-18", "mx-3"); + + const nametag = profile.appendChild(document.createElement("div")); + nametag.innerHTML = ` +
Hi ${userdata.displayName} ! :D
+
${uuid}
+ `; nametag.classList.add("text-neutral-900", "dark:text-white"); - const winrate = main.appendChild(document.createElement("div")); + const winrate = profile.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"); - }); - } + winrate.innerHTML = ` +
wins: ${userdata.wins}
+
losses: ${userdata.losses}
+
winrate: ${Math.round(userdata.wins / (userdata.wins + userdata.losses) * 100)} %
+ `; + winrate.classList.add("text-neutral-900", "dark:text-white", "grow", "content-center"); + } } diff --git a/src/front/static/ts/views/ProfileMenu.ts b/src/front/static/ts/views/ProfileMenu.ts new file mode 100644 index 0000000..9409360 --- /dev/null +++ b/src/front/static/ts/views/ProfileMenu.ts @@ -0,0 +1,87 @@ +import Aview from "./Aview.ts" +import { isLogged, navigationManager } from "../main.ts" + +export default class extends Aview { + + 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]; + 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(); + + return ` + hi, ${ userdata.displayName.length > 8 ? userdata.displayName.substring(0, 8) + "." : userdata.displayName } ! +
+ profile + settings + friends + `; + } + + + document.getElementById("profile-items").innerHTML = await getMainHTML(); + + 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 67c828f..429cc09 100644 --- a/src/front/static/ts/views/RegisterPage.ts +++ b/src/front/static/ts/views/RegisterPage.ts @@ -1,7 +1,7 @@ import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; import { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" -import { dragElement } from "./drag.ts"; export default class extends Aview { @@ -24,14 +24,30 @@ export default class extends Aview {
-
-

welcome ! please register.

- - - -
- -
+
+
+

welcome ! please register.

+ + + +
+ +
+ +
+ + +
+
`; } diff --git a/src/front/static/ts/views/Settings.ts b/src/front/static/ts/views/Settings.ts new file mode 100644 index 0000000..fb5fbc6 --- /dev/null +++ b/src/front/static/ts/views/Settings.ts @@ -0,0 +1,84 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; +import { setOnekoState } from "../oneko.ts" +import { isLogged, navigationManager } from "../main.ts" + + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("profile"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ settings.ts +
+ + + × +
+
+
+ + + +
+
+ `; + } + + async run() { + if (!await isLogged()) + navigationManager("/"); + + dragElement(document.getElementById("window")); + + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + 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(); + + (document.getElementById("displayName-input") as HTMLInputElement).placeholder = userdata.displayName; + (document.getElementById("displayName-input") as HTMLInputElement).value = userdata.displayName; + + document.getElementById("displayName-button")?.addEventListener("click", async () => { + const changeDisplayName_req = await fetch(`http://localhost:3002/users/${uuid}/displayName`, { + method: "PATCH", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ displayName: (document.getElementById("displayName-input") as HTMLInputElement).value }) + }); + if (changeDisplayName_req.status == 200) { + // idk display success + } + else { + // display error ig, uuuh it's in await changeDisplayName.json().error + } + }); + + document.getElementById("deleteAccount-button")?.addEventListener("click", async () => { + const delete_req = await fetch(`http://localhost:3002/users/${uuid}`, { + method: "DELETE", + credentials: "include", + }); + + if (delete_req.status == 200) + navigationManager("/"); + else + console.error("xd"); // should never happen, wtf + }); + } +} diff --git a/src/front/static/ts/views/Tetris.ts b/src/front/static/ts/views/Tetris.ts index f01eb08..1fc2d2d 100644 --- a/src/front/static/ts/views/Tetris.ts +++ b/src/front/static/ts/views/Tetris.ts @@ -1,5 +1,6 @@ import Aview from "./Aview.ts"; import { dragElement } from "./drag.js"; +import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts" export default class extends Aview { running: boolean; @@ -7,6 +8,7 @@ export default class extends Aview { constructor() { super(); this.setTitle("tetris (local match)"); + setOnekoState("tetris"); this.running = true; } @@ -211,14 +213,14 @@ export default class extends Aview { }; 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 + [ "#000000", "#000000" ] , // placeholder for 0 + [ "#00d2e1", "#0080a8" ], // I - cyan + [ "#0092e9", "#001fbf" ], // J - blue + [ "#e79700", "#c75700" ], // L - orange + [ "#d8c800", "#8f7700" ], // O - yellow + [ "#59e000", "#038b00" ], // S - green + [ "#de1fdf", "#870087" ], // T - purple + [ "#f06600", "#c10d07" ], // Z - red ]; class Piece { @@ -274,33 +276,42 @@ export default class extends Aview { canvas: HTMLCanvasElement | null; holdCanvas: HTMLCanvasElement | null; queueCanvas: HTMLCanvasElement | null; - ctx: CanvasRenderingContext2D; - holdCtx: CanvasRenderingContext2D; - queueCtx: CanvasRenderingContext2D; + ctx: CanvasRenderingContext2D | null; + holdCtx: CanvasRenderingContext2D | null; + queueCtx: CanvasRenderingContext2D | null; 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; + score: number = 0; + level: number = 1; + lines: number = 0; + dropInterval: number = 1000; + lastDrop: number = 0; + isLocking: boolean = false; + lockRotationCount: number = 0; + lockLastRotationCount: number = 0; + isGameOver: boolean = false; + isPaused: boolean = false; constructor(canvasId: string) { const el = document.getElementById( canvasId, ) as HTMLCanvasElement | null; this.canvas = el; + if (!this.canvas) + throw console.error("no canvas :c"); this.canvas.width = COLS * BLOCK; this.canvas.height = ROWS * BLOCK; const ctx = this.canvas.getContext("2d"); this.ctx = ctx; + if (!this.ctx) + throw console.error("no ctx D:"); - this.holdCanvas = document.getElementById("hold"); - this.queueCanvas = document.getElementById("queue"); + this.holdCanvas = document.getElementById("hold") as HTMLCanvasElement; + this.queueCanvas = document.getElementById("queue") as HTMLCanvasElement; + if (!this.holdCanvas || !this.queueCanvas) + throw console.error("no canvas :c"); this.holdCtx = this.holdCanvas.getContext("2d"); this.queueCtx = this.queueCanvas.getContext("2d"); @@ -337,6 +348,7 @@ export default class extends Aview { [this.piece, this.holdPiece] = [this.holdPiece, this.piece]; if (!this.piece) this.spawnPiece(); + if (!this.piece) return; this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2); this.piece.y = -2; @@ -352,7 +364,6 @@ export default class extends Aview { 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; } @@ -374,12 +385,11 @@ export default class extends Aview { 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; + return y - 1; } y++; @@ -388,11 +398,12 @@ export default class extends Aview { lockPiece() { if (!this.piece) return; + this.isLocking = false; 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 (cell.y > 0) isValid = true; } if (!isValid) this.isGameOver = true; @@ -427,6 +438,8 @@ export default class extends Aview { rotatePiece(dir: "cw" | "ccw") { if (!this.piece) return; + if (this.isLocking && this.lockRotationCount < 15) + this.lockRotationCount++; // Try rotation with wall kicks const originalIndex = this.piece.rotationIndex; if (dir === "cw") this.piece.rotateCW(); @@ -446,6 +459,7 @@ export default class extends Aview { 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; @@ -470,6 +484,26 @@ export default class extends Aview { } keys: Record = {}; + direction: number = 0; + inputDelay = 200; + inputTimestamp = Date.now(); + move: boolean = false; + + inputManager() { + if (this.move || Date.now() > this.inputTimestamp + this.inputDelay) + { + if (this.keys["ArrowLeft"] && !this.keys["ArrowRight"]) + this.movePiece(-1, 0); + else if (!this.keys["ArrowLeft"] && this.keys["ArrowRight"]) + this.movePiece(1, 0); + else if (this.keys["ArrowLeft"] && this.keys["ArrowRight"]) + this.movePiece(this.direction, 0); + this.move = false; + } + + /*if (this.keys["ArrowDown"]) + this.softDrop();*/ + } registerListeners() { window.addEventListener("keydown", (e) => { @@ -482,8 +516,18 @@ export default class extends Aview { if (this.isPaused) return; - if (e.key === "ArrowLeft") this.movePiece(-1, 0); - else if (e.key === "ArrowRight") this.movePiece(1, 0); + if (e.key === "ArrowLeft") + { + this.inputTimestamp = Date.now(); + this.direction = -1;//this.movePiece(-1, 0); + this.move = true; + } + else if (e.key === "ArrowRight") + { + this.inputTimestamp = Date.now(); + this.direction = 1;//this.movePiece(1, 0); + this.move = true; + } else if (e.key === "ArrowDown") this.softDrop(); else if (e.code === "Space") { e.preventDefault(); @@ -504,18 +548,38 @@ export default class extends Aview { 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(); + if (!this.isPaused) + { + this.inputManager(); + if (this.isLocking ? timestamp - this.lastDrop > 500 : timestamp - this.lastDrop > this.dropInterval) + { + if (this.isLocking && this.lockRotationCount == this.lockLastRotationCount) + this.lockPiece(); + this.lockLastRotationCount = this.lockRotationCount; + if (!this.movePiece(0, 1)) + { + if (!this.isLocking) + { + this.lockRotationCount = 0; + this.lockLastRotationCount = 0; + this.isLocking = true; + } + } + else if (this.isLocking) + this.lockRotationCount = 0; this.lastDrop = timestamp; } } this.draw(); + if (this.isGameOver) + return; requestAnimationFrame(this.loop.bind(this)); } drawGrid() { const ctx = this.ctx; + if (!ctx || !this.canvas) + return; ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.strokeStyle = "#222"; for (let r = 0; r <= ROWS; r++) { @@ -537,7 +601,7 @@ export default class extends Aview { 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]); + if (val) this.fillBlock(c, r, COLORS[val], this.ctx); else this.clearBlock(c, r); } } @@ -547,16 +611,16 @@ export default class extends Aview { if (!this.piece) return; for (const cell of this.piece.getCells()) - if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val]); + if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val], this.ctx); let offset: number = this.getGhostOffset(this.piece); for (const cell of this.piece.getCells()) - if (cell.y + offset >= 0) + if (cell.y + offset >= 0 && offset > 0) this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]); } drawHold() { - if (!this.holdPiece) return; + if (!this.holdPiece || !this.holdCtx) return; this.holdCtx.clearRect(0, 0, 200, 200); let y: number = 0; @@ -564,9 +628,8 @@ export default class extends Aview { let x: number = 0; for (const val of row) { if (val) { - this.holdCtx.fillStyle = this.canHold - ? COLORS[this.holdPiece.findColorIndex()] - : "gray"; + this.fillBlock(x, y, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : ["#8c8c84", "#393934"], this.holdCtx); + /*this.holdCtx.fillStyle = ; this.holdCtx.fillRect( x * BLOCK + 1 + @@ -575,7 +638,7 @@ export default class extends Aview { y * BLOCK + 1 + 20, BLOCK - 2, BLOCK - 2, - ); + );*/ } x++; } @@ -584,9 +647,9 @@ export default class extends Aview { } drawQueue() { + if (!this.queueCtx) return ; 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]) { @@ -596,7 +659,7 @@ export default class extends Aview { this.queueCtx.fillStyle = COLORS[ ["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1 - ]; + ][0]; this.queueCtx.fillRect( x * BLOCK + 1 + @@ -619,23 +682,87 @@ export default class extends Aview { } } - 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); + adjustColor(hex: string, amount: number): string { + let color = hex.startsWith('#') ? hex.slice(1) : hex; + const num = parseInt(color, 16); + let r = (num >> 16) + amount; + let g = ((num >> 8) & 0x00FF) + amount; + let b = (num & 0x0000FF) + amount; + r = Math.max(Math.min(255, r), 0); + g = Math.max(Math.min(255, g), 0); + b = Math.max(Math.min(255, b), 0); + return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`; } - fillGhostBlock(x: number, y: number, color: string) { + + fillBlock(x: number, y: number, color: string[], ctx) { + if (!ctx) return; + //const ctx = this.ctx; + //ctx.fillStyle = color; + const grad = ctx.createLinearGradient(x * BLOCK, y * BLOCK, x * BLOCK, y * BLOCK + BLOCK); + grad.addColorStop(0, color[0]); + grad.addColorStop(1, color[1]); + ctx.fillStyle = grad; + ctx.fillRect(x * BLOCK + 4, y * BLOCK + 4, BLOCK - 4, BLOCK - 4); + const X = x * BLOCK; + const Y = y * BLOCK; + const W = BLOCK; + const H = BLOCK; + const S = 4; + + ctx.lineWidth = S; + ctx.beginPath(); + ctx.strokeStyle = color[0]; + ctx.moveTo(X, Y + S / 2); + ctx.lineTo(X + W, Y + S / 2); + ctx.moveTo(X + S / 2, Y); + ctx.lineTo(X + S / 2, Y + H); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[1], -20); + ctx.moveTo(X, Y + H - S / 2); + ctx.lineTo(X + W, Y + H - S / 2); + ctx.moveTo(X + W - S / 2, Y); + ctx.lineTo(X + W - S / 2, Y + H); + ctx.stroke(); + } + fillGhostBlock(x: number, y: number, color: string[]) { + if (!this.ctx) return; const ctx = this.ctx; - ctx.strokeStyle = color; - ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + + const X = x * BLOCK; + const Y = y * BLOCK; + const W = BLOCK; + const H = BLOCK; + const S = 4; + + ctx.lineWidth = S; + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[0], -40); + ctx.moveTo(X, Y + S / 2); + ctx.lineTo(X + W, Y + S / 2); + ctx.moveTo(X + S / 2, Y); + ctx.lineTo(X + S / 2, Y + H); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[1], -60); + ctx.moveTo(X, Y + H - S / 2); + ctx.lineTo(X + W, Y + H - S / 2); + ctx.moveTo(X + W - S / 2, Y); + ctx.lineTo(X + W - S / 2, Y + H); + ctx.stroke(); + //ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); } clearBlock(x: number, y: number) { + if (!this.ctx) return; const ctx = this.ctx; ctx.clearRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); } drawHUD() { + if (!this.ctx || !this.canvas) return; const ctx = this.ctx; ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(4, 4, 120, 60); @@ -675,6 +802,7 @@ export default class extends Aview { } draw() { + if (!this.ctx || !this.canvas) return; // clear everything this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); @@ -698,6 +826,7 @@ export default class extends Aview { this.drawBoard(); this.drawPiece(); this.drawHUD(); + this.drawQueue(); } }