」 feat(src/front): taskbar to access profile and settings for user management is done :D and also its very pretty :3

This commit is contained in:
y-syo
2025-10-12 13:56:10 +02:00
parent 26b16749bb
commit 1141fe3159
7 changed files with 857 additions and 768 deletions

View File

@ -12,7 +12,7 @@
<!--body class="bg-gray-100 dark:bg-neutral-950 h-screen flex flex-col"-->
<body class="bg-neutral-950 dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col">
<body class="bg-[url(https://y-syo.me/res/bg.jpg)] dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col">
<!--script src="https://kanel.ovh/oneko.js"></script-->
<!--script src="./static/ts/oneko.js"></script-->
@ -35,15 +35,22 @@
<div id="app" class="flex-1 flex items-center justify-center">
</div>
<div id="taskbar-menu" class="absolute bottom-13 left-0"></div>
<div class="border-t-2 border-neutral-300 dark:border-neutral-800 sticky bottom-0">
<nav class="bg-neutral-200 dark:bg-neutral-900 shadow-md border-t-2 border-neutral-400 dark:border-neutral-700 px-4 sm:px-6 lg:px-8 flex justify-start h-12 items-center space-x-6 font-[Kubasta]">
<a id="profile-button" class="text-neutral-900 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-400" href="/login" data-link>login</a>
<a class="text-neutral-900 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-400" href="/" data-link>home</a>
<nav class="bg-neutral-200 dark:bg-neutral-900 shadow-md border-t-2 border-neutral-400 dark:border-neutral-700 flex justify-between h-12 items-center content-center space-x-6 font-[Kubasta]">
<div class="flex px-4 items-center content-center space-x-2">
<button id="profile-button" class="taskbar-button flex flex-row justify-center items-center"><img class="object-scale-down mr-2 h-5 w-5" src="https://api.kanel.ovh/id?id=65" /> start</button>
<div class="text-neutral-700 dark:text-neutral-400">|</div>
<a class="taskbar-button" href="https://rusty.42angouleme.fr/">rusty</a>
<a class="taskbar-button" href="/tetris" data-link>tetris</a>
</div>
<div class="reverse-border m-1.5 h-8/10 content-center">
<span id="taskbar-clock" class="text- neutral-900 dark:text-white px-4">12:37</span>
</div>
</nav>
</div>
<script type="module" src="/static/ts/main.ts"></script>
</body>
</html>

View File

@ -47,3 +47,32 @@
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
;
}

View File

@ -1,6 +1,8 @@
import { oneko } from "./oneko.ts";
import Profile from "./views/Profile.ts";
let profile_view = new Profile;
export async function isLogged(): boolean {
export async function isLogged(): Promise<boolean> {
let uuid_req = await fetch("http://localhost:3001/me", {
method: "GET",
credentials: "include",
@ -9,63 +11,11 @@ export async function isLogged(): boolean {
{
let uuid = await uuid_req.json();
document.cookie = `uuid=${uuid.user};max-age=${60*60*24*7}`;
const old_button = document.getElementById("profile-button");
const dropdown = document.createElement("div");
dropdown.classList.add("relative", "inline-block", "group");
dropdown.id = "profile-button";
const button_dropdown = dropdown.appendChild(document.createElement("button"));
button_dropdown.innerHTML = uuid.user;
button_dropdown.classList.add("text-neutral-900", "group-hover:text-neutral-700", "dark:text-white", "dark:group-hover:text-neutral-400");
const menu_div = dropdown.appendChild(document.createElement("div"));
menu_div.classList.add("float:right", "hidden", "absolute", "left-0", "bottom-full", "dark:bg-neutral-800", "dark:text-white", "min-w-[160px]", "shadow-lg", "z-10", "group-hover:block");
const profile_a = menu_div.appendChild(document.createElement("a"));
const settings_a = menu_div.appendChild(document.createElement("a"));
const logout_button = menu_div.appendChild(document.createElement("button"));
profile_a.text = "profile";
profile_a.classList.add("block", "no-underline", "px-4", "py-3");
profile_a.href = "/profile";
profile_a.setAttribute("data-link", "");
settings_a.text = "settings";
settings_a.classList.add("block", "no-underline", "px-4", "py-3");
settings_a.href = "/settings";
settings_a.setAttribute("data-link", "");
logout_button.innerHTML = "logout";
logout_button.classList.add("block", "no-underline", "px-4", "py-3");
logout_button.id = "logout-button";
//document.getElementById("logout-button")?.addEventListener("click", async () => {
logout_button.addEventListener("click", async () => {
let req = await fetch("http://localhost:3001/logout", {
method: "GET",
credentials: "include",
});
if (req.status === 200)
isLogged();
else
console.error("logout failed");
});
old_button.replaceWith(dropdown);
return true;
}
else // 401
{
document.cookie = `uuid=;max-age=0`;
const old_button = document.getElementById("profile-button");
const login_button = document.createElement("a");
login_button.id = "profile-button";
login_button.text = "login";
login_button.classList.add("text-neutral-900", "hover:text-neutral-700", "dark:text-white", "dark:hover:text-neutral-400");
login_button.href = "/login";
login_button.setAttribute("data-link", "");
old_button.replaceWith(login_button);
return false;
}
}
@ -90,8 +40,6 @@ const routes = [
{ path: "/login", view: () => import("./views/LoginPage.ts") },
{ path: "/register", view: () => import("./views/RegisterPage.ts") },
{ path: "/profile", view: () => import("./views/Profile.ts") },
];
const router = async () => {
@ -117,12 +65,18 @@ const router = async () => {
view.run();
};
document.getElementById("profile-button")?.addEventListener("click", () => {profile_view.run();});
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
isLogged();
document.body.addEventListener("click", e=> {
if (!e.target.closest("#taskbar-menu") && !e.target.matches("#profile-button"))
{
profile_view.open = false;
document.getElementById("taskbar-menu").innerHTML = "";
}
if (e.target.matches("[data-link]"))
{
e.preventDefault();
@ -144,3 +98,21 @@ document.addEventListener("DOMContentLoaded", () => {
});
oneko();
function updateClock()
{
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const clock = document.getElementById("taskbar-clock");
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
hours = hours < 10 ? "0" + hours : hours;
minutes = minutes < 10 ? "0" + minutes : minutes;
clock.innerHTML = `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]} ` + hours + ":" + minutes;
}
setInterval(updateClock, 5000);
updateClock();

View File

@ -1,4 +1,5 @@
import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
@ -13,22 +14,30 @@ export default class extends Aview {
async getHTML() {
return `
<form method="dialog" class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4 flex flex-col">
<h1 class="text-4xl font-bold text-blue-600">login</h1>
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">login.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 border rounded-md w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 border w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required></input>
<form method="dialog" class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome back ! please login.</h1>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<button id="login-button" type="submit" class="bg-blue-600 text-white hover:bg-blue-500 w-full py-2 rounded-md transition-colors">login</button>
<a class="text-gray-400 dark:text-gray-600 underline" href="/register" data-link>
register
</a>
</br>
<button id="login-button" type="submit" class="default-button w-full">login</button>
</form>
</div>
`;
}
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;

View File

@ -6,36 +6,81 @@ export default class extends Aview {
constructor()
{
super();
if (!isLogged())
navigationManager("/login");
this.setTitle("profile");
}
async getHTML() {
return `
<div id="main-window" class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4">
<div id="main-window" class="default-border shadow-2x1 bg-neutral-200 dark:bg-neutral-800">
<div class="flex flex-row items-stretch">
<div class="inline-block bg-linear-to-b from-orange-200 to-orange-300 min-h-84 w-6 relative">
<!--div class="absolute bottom-1 left-full whitespace-nowrap origin-bottom-left -rotate-90 font-bold">knl_meowscendence</div-->
<div class="absolute bottom-1 left-full whitespace-nowrap origin-bottom-left -rotate-90 font-bold">girls kissing :3</div>
</div>
<div class="flex flex-col items-center">
<div id="profile-items" class="flex flex-col items-center">
</div>
<div id="menu-bottom-div" class="hidden mt-auto flex flex-col items-center">
<hr class="my-2 w-32 reverse-border">
<button id="menu-logout" class="menu-default-button">logout</button>
</div>
</div>
</div>
</div>
`;
}
open: boolean = false;
async run() {
const uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, {
let uuid: String;
if (this.open)
{
this.open = false;
document.getElementById("taskbar-menu").innerHTML = "";
return ;
}
this.open = true;
document.getElementById("taskbar-menu").innerHTML = await this.getHTML();
async function getMainHTML() {
if (!(await isLogged()))
{
document.getElementById("menu-bottom-div").classList.add("hidden");
return `
<a class="menu-default-button inline-flex items-center justify-center" href="/login" data-link>login</a>
<a class="menu-default-button inline-flex items-center justify-center" href="/register" data-link>register</a>
`;
}
document.getElementById("menu-bottom-div").classList.remove("hidden");
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
return `
<span class="menu-default-label inline-flex items-center justify-center">hi, ${uuid} !</span>
<hr class="my-2 w-32 reverse-border">
<button class="menu-default-button">profile</button>
<button class="menu-default-button">settings</button>
`;
}
document.getElementById("profile-items").innerHTML = await getMainHTML();
/*const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, {
method: "GET",
credentials: "include",
});
if (userdata_req.status == 404)
{
console.error("invalid user");
return ;
}
let userdata = await userdata_req.json();
console.log(userdata_req);
console.log(userdata_req);*/
const main = document.getElementById("main-window");
/*const main = document.getElementById("profile-profile");
const nametag = main.appendChild(document.createElement("span"));
nametag.innerHTML = `Hiiiiii ${userdata.displayName} ! :D`;
@ -44,6 +89,17 @@ export default class extends Aview {
const winrate = main.appendChild(document.createElement("div"));
winrate.innerHTML = `wins: ${userdata.wins} | losses: ${userdata.losses} | winrate: ${userdata.wins / (userdata.wins + userdata.losses)}`;
winrate.classList.add("text-neutral-900", "dark:text-white");
winrate.classList.add("text-neutral-900", "dark:text-white");*/
//console.log(document.getElementById("menu-logout"));
document.getElementById("menu-logout").addEventListener("click", async () => {
let req = await fetch("http://localhost:3001/logout", {
method: "GET",
credentials: "include",
});
if (req.status === 200)
this.run();
else
console.error("logout failed");
});
}
}

View File

@ -1,6 +1,7 @@
import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
import { dragElement } from "./drag.ts";
export default class extends Aview {
@ -13,22 +14,30 @@ export default class extends Aview {
async getHTML() {
return `
<form method="dialog" class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4 flex flex-col">
<h1 class="text-4xl font-bold text-blue-600">register</h1>
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">register.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 border rounded-md w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 border w-full px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required></input>
<form method="dialog" class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome ! please register.</p>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<button id="register-button" type="submit" class="bg-blue-600 text-white hover:bg-blue-500 w-full py-2 rounded-md transition-colors">register</button>
<a class="text-gray-400 dark:text-gray-600 underline" href="/login" data-link>
i already have an account
</a>
</br>
<button id="register-button" type="submit" class="default-button w-full">register</button>
</form>
</div>
`;
}
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;

View File

@ -1,11 +1,10 @@
import Aview from "./Aview.ts"
import Aview from "./Aview.ts";
import { dragElement } from "./drag.js";
export default class extends Aview {
running: boolean;
constructor()
{
constructor() {
super();
this.setTitle("tetris (local match)");
this.running = true;
@ -40,6 +39,7 @@ export default class extends Aview {
}
async run() {
dragElement(document.getElementById("window"));
const COLS = 10;
const ROWS = 20;
const BLOCK = 30; // pixels per block
@ -49,127 +49,176 @@ export default class extends Aview {
// 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, 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, 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, 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]],
[
[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]],
[
[2, 0, 0],
[2, 2, 2],
[0, 0, 0],
],
[[0,2,2]
,[0,2,0]
,[0,2,0]],
[
[0, 2, 2],
[0, 2, 0],
[0, 2, 0],
],
[[0,0,0]
,[2,2,2]
,[0,0,2]],
[
[0, 0, 0],
[2, 2, 2],
[0, 0, 2],
],
[[0,2,0]
,[0,2,0]
,[2,2,0]],
[
[0, 2, 0],
[0, 2, 0],
[2, 2, 0],
],
],
L: [
[[0,0,3]
,[3,3,3]
,[0,0,0]],
[
[0, 0, 3],
[3, 3, 3],
[0, 0, 0],
],
[[0,3,0]
,[0,3,0]
,[0,3,3]],
[
[0, 3, 0],
[0, 3, 0],
[0, 3, 3],
],
[[0,0,0]
,[3,3,3]
,[3,0,0]],
[
[0, 0, 0],
[3, 3, 3],
[3, 0, 0],
],
[[3,3,0]
,[0,3,0]
,[0,3,0]],
[
[3, 3, 0],
[0, 3, 0],
[0, 3, 0],
],
],
O: [
[[4,4]
,[4,4]],
[
[4, 4],
[4, 4],
],
],
S: [
[[0,5,5]
,[5,5,0]
,[0,0,0]],
[
[0, 5, 5],
[5, 5, 0],
[0, 0, 0],
],
[[0,5,0]
,[0,5,5]
,[0,0,5]],
[
[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]],
[
[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],
[6, 6, 6],
[0, 0, 0],
],
[[0,6,0]
,[0,6,6]
,[0,6,0]],
[
[0, 6, 0],
[0, 6, 6],
[0, 6, 0],
],
[[0,0,0]
,[6,6,6]
,[0,6,0]],
[
[0, 0, 0],
[6, 6, 6],
[0, 6, 0],
],
[[0,6,0]
,[6,6,0]
,[0,6,0]],
[
[0, 6, 0],
[6, 6, 0],
[0, 6, 0],
],
],
Z: [
[[7,7,0]
,[0,7,7]
,[0,0,0]],
[
[7, 7, 0],
[0, 7, 7],
[0, 0, 0],
],
[[0,0,7]
,[0,7,7]
,[0,7,0]],
[
[0, 0, 7],
[0, 7, 7],
[0, 7, 0],
],
[[0,0,0]
,[7,7,0]
,[0,7,7]],
[
[0, 0, 0],
[7, 7, 0],
[0, 7, 7],
],
[[0,7,0]
,[7,7,0]
,[7,0,0]],
[
[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
"#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 {
@ -191,10 +240,7 @@ export default class extends Aview {
}
findColorIndex() {
for (const row of this.shape)
for (const v of row)
if (v)
return v;
for (const row of this.shape) for (const v of row) if (v) return v;
return 1;
}
@ -204,20 +250,19 @@ export default class extends Aview {
}
rotateCCW() {
this.rotationIndex = (this.rotationIndex - 1 + this.rotations.length) % this.rotations.length;
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++)
{
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 });
if (val) cells.push({ x: this.x + c, y: this.y + r, val });
}
}
return cells;
@ -226,9 +271,9 @@ export default class extends Aview {
class Game {
board: Cell[][];
canvas: HTMLCanvasElement;
holdCanvas: HTMLCanvasElement;
queueCanvas: HTMLCanvasElement;
canvas: HTMLCanvasElement | null;
holdCanvas: HTMLCanvasElement | null;
queueCanvas: HTMLCanvasElement | null;
ctx: CanvasRenderingContext2D;
holdCtx: CanvasRenderingContext2D;
queueCtx: CanvasRenderingContext2D;
@ -245,17 +290,19 @@ export default class extends Aview {
isPaused = false;
constructor(canvasId: string) {
const el = document.getElementById(canvasId);
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');
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.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();
@ -267,8 +314,7 @@ export default class extends Aview {
createEmptyBoard(): Cell[][] {
const b: Cell[][] = [];
for (let r = 0; r < ROWS; r++)
{
for (let r = 0; r < ROWS; r++) {
const row: Cell[] = new Array(COLS).fill(0);
b.push(row);
}
@ -279,8 +325,7 @@ export default class extends Aview {
// classic 7-bag randomizer
const pieces = Object.keys(TETROMINOES);
const bag = [...pieces];
for (let i = bag.length - 1; i > 0; i--)
{
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]];
}
@ -288,12 +333,10 @@ export default class extends Aview {
}
hold() {
if (!this.canHold)
return;
if (!this.canHold) return;
[this.piece, this.holdPiece] = [this.holdPiece, this.piece];
if (!this.piece)
this.spawnPiece();
if (!this.piece) this.spawnPiece();
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
this.piece.y = -2;
@ -306,13 +349,11 @@ export default class extends Aview {
spawnPiece() {
this.canHold = true;
if (this.nextQueue.length < 7)
this.fillBag();
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))
{
if (this.collides(this.piece)) {
this.isGameOver = true;
}
@ -321,26 +362,23 @@ export default class extends Aview {
}
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;
for (const cell of piece.getCells()) {
if (cell.y >= ROWS) return true;
if (cell.x < 0 || cell.x >= COLS) return true;
if (cell.y >= 0 && this.board[cell.y][cell.x]) return true;
}
return false;
}
getGhostOffset(piece: Piece): boolean {
getGhostOffset(piece: Piece): number {
let y: number = 0;
while (true)
{
for (const cell of piece.getCells())
{
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]))
if (
cell.y + y >= ROWS ||
(cell.y + y >= 0 && this.board[cell.y + y][cell.x])
)
return y - 1;
}
@ -349,18 +387,14 @@ export default class extends Aview {
}
lockPiece() {
if (!this.piece)
return;
if (!this.piece) return;
let isValid: boolean = false;
for (const cell of this.piece.getCells())
{
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 < 20) isValid = true;
}
if (!isValid)
this.isGameOver = true;
if (!isValid) this.isGameOver = true;
this.clearLines();
this.spawnPiece();
@ -368,11 +402,8 @@ export default class extends Aview {
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;
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));
@ -380,36 +411,30 @@ export default class extends Aview {
r++;
}
if (linesCleared > 0)
{
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)
{
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;
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();
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;
if (!this.collides(this.piece)) return;
this.piece.x -= k;
}
// no valid kick, revert
@ -418,12 +443,10 @@ export default class extends Aview {
}
movePiece(dx: number, dy: number) {
if (!this.piece)
return;
if (!this.piece) return;
this.piece.x += dx;
this.piece.y += dy;
if (this.collides(this.piece))
{
if (this.collides(this.piece)) {
this.piece.x -= dx;
this.piece.y -= dy;
return false;
@ -432,69 +455,58 @@ export default class extends Aview {
}
hardDrop() {
if (!this.piece)
return;
if (!this.piece) return;
let dropped = 0;
while (this.movePiece(0, 1))
dropped++;
while (this.movePiece(0, 1)) dropped++;
this.score += dropped * 2;
this.lockPiece();
}
softDrop() {
if (!this.piece)
return;
if (!this.movePiece(0, 1))
return;
if (!this.piece) return;
if (!this.movePiece(0, 1)) return;
//this.lockPiece();
else
this.score += 1;
else this.score += 1;
}
keys: Record<string, boolean> = {};
registerListeners() {
window.addEventListener('keydown', (e) => {
window.addEventListener("keydown", (e) => {
this.keys[e.key] = true;
if (this.isGameOver) return;
if (e.key === 'ArrowLeft')
this.movePiece(-1, 0);
else if (e.key === 'ArrowRight')
this.movePiece(1, 0);
else if (e.key === 'ArrowDown')
this.softDrop();
else if (e.code === 'Space')
{
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')
{
} 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;
} 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; });
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();
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;
}
}
@ -505,17 +517,15 @@ export default class extends Aview {
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++)
{
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++)
{
for (let c = 0; c <= COLS; c++) {
ctx.beginPath();
ctx.moveTo(c * BLOCK, 0);
ctx.lineTo(c * BLOCK, ROWS * BLOCK);
@ -524,26 +534,20 @@ export default class extends Aview {
}
drawBoard() {
for (let r = 0; r < ROWS; r++)
{
for (let c = 0; c < COLS; c++)
{
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);
if (val) this.fillBlock(c, r, COLORS[val]);
else this.clearBlock(c, r);
}
}
}
drawPiece() {
if (!this.piece)
return;
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]);
let offset: number = this.getGhostOffset(this.piece);
for (const cell of this.piece.getCells())
@ -552,23 +556,26 @@ export default class extends Aview {
}
drawHold() {
if (!this.holdPiece)
return ;
if (!this.holdPiece) return;
this.holdCtx.clearRect(0, 0, 200, 200)
this.holdCtx.clearRect(0, 0, 200, 200);
let y: number = 0;
for (const row of this.holdPiece.rotations[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";
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,
x * BLOCK +
1 +
(4 - this.holdPiece.rotations[0].length) * 15 +
10,
y * BLOCK + 1 + 20,
BLOCK - 2, BLOCK - 2);
BLOCK - 2,
BLOCK - 2,
);
}
x++;
}
@ -576,26 +583,33 @@ export default class extends Aview {
}
}
drawQueue() {
this.queueCtx.clearRect(0, 0, 500, 500)
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))
{
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])
{
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];
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 * BLOCK +
1 +
(4 - TETROMINOES[nextPiece][0].length) * 15 +
10,
y * BLOCK +
1 +
placement * 80 +
20 -
(nextPiece === "I" ? 15 : 0),
BLOCK - 2,
BLOCK - 2,
);
}
x++;
}
@ -623,34 +637,40 @@ export default class extends Aview {
drawHUD() {
const ctx = this.ctx;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(4, 4, 120, 60);
ctx.fillStyle = '#fff';
ctx.font = '12px Kubasta';
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.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';
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";
}
}
@ -658,19 +678,17 @@ export default class extends Aview {
// clear everything
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = '#000';
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.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++)
{
for (let c = 0; c <= COLS; c++) {
this.ctx.beginPath();
this.ctx.moveTo(c * BLOCK, 0);
this.ctx.lineTo(c * BLOCK, ROWS * BLOCK);
@ -683,17 +701,6 @@ export default class extends Aview {
}
}
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);
}
});
const game = new Game("board");
}
}