mirror of
https://github.com/KeyZox71/knl_meowscendence.git
synced 2025-12-31 21:56:41 +01:00
554 lines
19 KiB
TypeScript
554 lines
19 KiB
TypeScript
import Aview from "./Aview.ts"
|
||
import { isLogged, user_api } from "../main.js"
|
||
import { dragElement } from "./drag.ts";
|
||
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts"
|
||
|
||
export default class extends Aview {
|
||
running: boolean;
|
||
|
||
constructor()
|
||
{
|
||
super();
|
||
this.setTitle("Tournament");
|
||
setOnekoState("default");
|
||
this.running = true;
|
||
}
|
||
|
||
async getHTML() {
|
||
return `
|
||
<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]">pong_game.ts</span>
|
||
<div>
|
||
<button> - </button>
|
||
<button> □ </button>
|
||
<a href="/" data-link> × </a>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 pt-5 space-y-4 reverse-border">
|
||
<div id="tournament-id">
|
||
<p class="text-neutral-900 dark:text-white text-lg font-bold pb-4">how many players ?</p>
|
||
<div class="flex flex-col space-y-4">
|
||
<select id="playerNumber" class="bg-white text-shadow-neutral-900 p-2 input-border">
|
||
<option value="">-- player number --</option>
|
||
<option value="2">2 players</option>
|
||
<option value="3">3 players</option>
|
||
<option value="4">4 players</option>
|
||
<option value="6">6 players</option>
|
||
<option value="8">8 players</option>
|
||
</select>
|
||
<button type="submit" id="bracket-generate" class="default-button">create the bracket</button>
|
||
<div id="bracket" class="flex flex-col space-y-6 items-center"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="announcement" class="hidden flex flex-col space-y-8">
|
||
<div id="bracket-announcement" class="flex flex-col space-y-6 items-center">
|
||
</div>
|
||
<span id="announcement-text" class="text-lg font-bold text-neutral-900 dark:text-white"></span>
|
||
<button type="submit" id="tournament-continue" class="default-button">let's go</button>
|
||
</div>
|
||
|
||
<div id="winner-div" class="hidden flex flex-col items-center space-y-8">
|
||
<img src="https://api.kanel.ovh/pp?id=3" class="w-25 h-25 default-border" \>
|
||
<span id="winner-text" class="text-2x1 text-neutral-900 dark:text-white"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async runGame(p1_id: number, p2_id: number, players: string[]): Promise<number> {
|
||
return new Promise<number>(async (resolve) => {
|
||
//console.log(p1_id, p2_id, players, players[p1_id], players[p2_id]);
|
||
let p1_name = players[p1_id];
|
||
let p2_name = players[p2_id];
|
||
|
||
let uuid: string;
|
||
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_displayName: string;
|
||
let p2_displayName: string;
|
||
|
||
let countdown: number = 3;
|
||
let countdownTimer: number = 0;
|
||
|
||
let canvas: HTMLCanvasElement;
|
||
let ctx: CanvasRenderingContext2D;
|
||
|
||
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<string, boolean> = {};
|
||
|
||
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: number) {
|
||
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);
|
||
}
|
||
|
||
async 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) {
|
||
if (await isLogged()) {
|
||
let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||
fetch(`${user_api}/users/${uuid}/matchHistory?game=pong`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", },
|
||
credentials: "include",
|
||
body: JSON.stringify({
|
||
"game": "pong",
|
||
"opponent": p2_name,
|
||
"myScore": p1_score,
|
||
"opponentScore": p2_score,
|
||
"date": Date.now(),
|
||
}),
|
||
});
|
||
}
|
||
match_over = true;
|
||
resolve(p1_score == 3 ? p1_id : p2_id);
|
||
}
|
||
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_displayName, canvas.width / 4 - (ctx.measureText(p1_name).width / 2), 45);
|
||
ctx.fillText(p2_displayName, (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 = async (timestamp: number) => {
|
||
elapsed = (timestamp - start) / 1000;
|
||
start = timestamp;
|
||
if (game_playing) {
|
||
movePaddles();
|
||
await 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_isvalid = true;
|
||
let p2_isvalid = true;
|
||
if (await isLogged()) {
|
||
const p1_req = await fetch(`${user_api}/users/${p1_name}`, {
|
||
method: "GET",
|
||
credentials: "include",
|
||
});
|
||
const p2_req = await fetch(`${user_api}/users/${p2_name}`, {
|
||
method: "GET",
|
||
credentials: "include",
|
||
});
|
||
|
||
if (p1_req.status == 200)
|
||
p1_displayName = (await p1_req.json()).displayName;
|
||
else
|
||
p1_displayName = p1_name;
|
||
|
||
if (p2_req.status == 200)
|
||
p2_displayName = (await p2_req.json()).displayName;
|
||
else
|
||
p2_displayName = p2_name;
|
||
}
|
||
else
|
||
{
|
||
p1_displayName = p1_name;
|
||
p2_displayName = p2_name;
|
||
}
|
||
|
||
p1_displayName = p1_displayName.length > 16 ? p1_displayName.substring(0, 16) + "." : p1_displayName;
|
||
p2_displayName = p2_displayName.length > 16 ? p2_displayName.substring(0, 16) + "." : p2_displayName;
|
||
p1_name = p1_name.length > 16 ? p1_name.substring(0, 16) + "." : p1_name;
|
||
p2_name = p2_name.length > 16 ? p2_name.substring(0, 16) + "." : p2_name;
|
||
document.getElementById("tournament-ui")?.classList.add("hidden");
|
||
|
||
canvas = document.createElement("canvas");
|
||
canvas.id = "gameCanvas";
|
||
canvas.classList.add("reverse-border");
|
||
|
||
document.getElementById("main-div")?.prepend(canvas);
|
||
|
||
ctx = canvas.getContext("2d", { alpha: false }) as CanvasRenderingContext2D;
|
||
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);
|
||
});
|
||
}
|
||
|
||
waitForUserClick(buttonId: string): Promise<void> {
|
||
return new Promise((resolve) => {
|
||
const button = document.getElementById(buttonId);
|
||
if (!button) return resolve(); // failsafe if no button
|
||
|
||
const handler = () => {
|
||
button.removeEventListener("click", handler);
|
||
resolve();
|
||
};
|
||
button.addEventListener("click", handler);
|
||
});
|
||
}
|
||
|
||
tournament_state: number[][];
|
||
i: number = 0;
|
||
space: number;
|
||
|
||
updateBracketDisplay(tournament: number[][], players: string[]) {
|
||
for (let i of Array(tournament[0].length).keys())
|
||
this.tournament_state[this.i][i] = tournament[0][i];
|
||
for (let i of Array(tournament[1].length).keys())
|
||
{
|
||
console.log(this.tournament_state, this.i, i);
|
||
this.tournament_state[this.i + 1][i] = tournament[1][i];
|
||
}
|
||
this.i++;
|
||
const container = document.getElementById("bracket-announcement");
|
||
if (!container) return;
|
||
container.innerHTML = ""; // clear old bracket
|
||
|
||
const bracketWrapper = document.createElement("div");
|
||
bracketWrapper.className = "flex space-x-8 overflow-x-auto";
|
||
|
||
// replicate generateBracket() spacing logic
|
||
let previousPadding = 4;
|
||
|
||
for (let round = 0; round < this.tournament_state.length; round++) {
|
||
const roundColumn = document.createElement("div");
|
||
|
||
if (round === 0) {
|
||
roundColumn.className = `flex flex-col mt-${this.space} space-y-4`;
|
||
} else {
|
||
previousPadding = previousPadding * 2 + 10;
|
||
roundColumn.className = `flex flex-col justify-center space-y-${previousPadding}`;
|
||
}
|
||
|
||
// each player slot or winner
|
||
for (let i = 0; i < this.tournament_state[round].length; i++) {
|
||
const playerIndex = this.tournament_state[round][i];
|
||
const name =
|
||
playerIndex !== undefined && playerIndex !== null
|
||
? players[playerIndex]
|
||
: "";
|
||
|
||
const cell = document.createElement("div");
|
||
cell.className =
|
||
"w-32 h-10 flex items-center justify-center bg-white text-center text-sm input-border";
|
||
cell.textContent = name || "";
|
||
roundColumn.appendChild(cell);
|
||
}
|
||
|
||
bracketWrapper.appendChild(roundColumn);
|
||
}
|
||
|
||
container.appendChild(bracketWrapper);
|
||
}
|
||
|
||
async run() {
|
||
dragElement(document.getElementById("window"));
|
||
const generateBracket = async (playerCount: number) => {
|
||
this.tournament_state = [];
|
||
let initPlayerCount = playerCount;
|
||
document.getElementById("bracket").innerHTML = "";
|
||
|
||
const rounds: number = Math.ceil(Math.log2(playerCount));
|
||
const totalSlots: number = 2 ** rounds;
|
||
let odd: number = 0;
|
||
let notPowPlayersCount: number = 0;
|
||
let tournament: number[][] = [];
|
||
|
||
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));
|
||
|
||
const bracketWrapper = document.createElement("div");
|
||
bracketWrapper.className = "flex space-x-8 overflow-x-auto";
|
||
|
||
// Round 0: Player input column
|
||
const playerInputColumn = document.createElement("div");
|
||
this.space = (notPowPlayersCount + odd) * 28;
|
||
playerInputColumn.className = `flex flex-col mt-${(notPowPlayersCount + odd) * 28} space-y-4`;
|
||
|
||
tournament.push([]);
|
||
this.tournament_state.push([]);
|
||
initialPlayers.forEach((name, i) => {
|
||
const input = document.createElement("input");
|
||
input.type = "text";
|
||
input.id = `playerName${i}`;
|
||
input.value = name;
|
||
input.placeholder = name;
|
||
if (i == 0)
|
||
{
|
||
isLogged().then((value) => {
|
||
if (value) {
|
||
let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||
input.value = uuid;
|
||
input.readOnly = true;
|
||
}
|
||
});
|
||
}
|
||
input.className = "w-32 h-10 p-2 text-sm bg-white disabled:bg-gray-200 input-border";
|
||
playerInputColumn.appendChild(input);
|
||
tournament[0].push(i);
|
||
this.tournament_state[0].push(-1);
|
||
});
|
||
|
||
bracketWrapper.appendChild(playerInputColumn);
|
||
|
||
let currentRound = initialPlayers;
|
||
let previousPadding = 4;
|
||
tournament.push([]);
|
||
for (let round = 1; round <= rounds; round++)
|
||
{
|
||
this.tournament_state.push([]);
|
||
const roundColumn = document.createElement("div");
|
||
previousPadding = previousPadding * 2 + 10
|
||
roundColumn.className = `flex flex-col justify-center space-y-${previousPadding}`;
|
||
|
||
const nextRound: string[] = [];
|
||
|
||
while (notPowPlayersCount) {
|
||
tournament[1].push(playerCount);
|
||
this.tournament_state[1].push(-1);
|
||
const input = document.createElement("input");
|
||
input.type = "text";
|
||
input.id = `playerName${playerCount}`;
|
||
input.value = `player ${playerCount + 1}`;
|
||
input.placeholder = `player ${++playerCount}`;
|
||
input.className =
|
||
"w-32 h-10 p-2 text-sm bg-white disabled:bg-gray-200 input-border";
|
||
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 text-center text-sm input-border";
|
||
|
||
matchDiv.textContent = "";
|
||
nextRound.push("");
|
||
|
||
roundColumn.appendChild(matchDiv);
|
||
this.tournament_state[round].push(-1);
|
||
}
|
||
|
||
bracketWrapper.appendChild(roundColumn);
|
||
currentRound = nextRound;
|
||
}
|
||
|
||
document.getElementById("bracket")?.appendChild(document.createElement("hr")).classList.add("my-4", "mb-8", "w-64", "reverse-border");
|
||
document.getElementById("bracket")?.appendChild(bracketWrapper);
|
||
const btn = document.getElementById("bracket")?.appendChild(document.createElement("button"));
|
||
if (!btn) return;
|
||
btn.classList.add("default-button", "w-full");
|
||
btn.id = "tournament-play";
|
||
btn.onclick = async () => {
|
||
document.getElementById("tournament-id")?.classList.add("hidden");
|
||
let players: string[] = [];
|
||
let players_displayName: string[] = [];
|
||
for (let i of Array(initPlayerCount).keys()) {
|
||
players.push((document.getElementById(`playerName${i}`) as HTMLInputElement).value);
|
||
const name_req = await fetch(`${user_api}/users/${players.at(-1)}`, {
|
||
method: "GET",
|
||
credentials: "include",
|
||
});
|
||
if (name_req.status === 200)
|
||
players_displayName.push((await name_req.json()).displayName);
|
||
else
|
||
players_displayName.push(players.at(-1));
|
||
}
|
||
|
||
while (tournament[0].length > 1)
|
||
{
|
||
this.updateBracketDisplay(tournament, players_displayName);
|
||
while(tournament[0].length > 0)
|
||
{
|
||
const p1 = tournament[0].shift() as number;
|
||
const p2 = tournament[0].shift() as number;
|
||
|
||
document.getElementById("announcement-text").innerText = `${players_displayName[p1]} vs ${players_displayName[p2]}`;
|
||
document.getElementById("announcement")?.classList.remove("hidden");
|
||
await this.waitForUserClick("tournament-continue");
|
||
document.getElementById("announcement")?.classList.add("hidden");
|
||
const result = await this.runGame(p1, p2, players);
|
||
document.getElementById("gameCanvas")?.remove();
|
||
tournament[1].push(result);
|
||
}
|
||
tournament[0] = tournament[1];
|
||
tournament[1] = [];
|
||
}
|
||
document.getElementById("winner-div")?.classList.remove("hidden");
|
||
document.getElementById("winner-text").innerText = `${players_displayName[tournament[0][0]]} won the tournament !! ggs :D`;
|
||
};
|
||
btn.innerText = "start tournament !!";
|
||
|
||
};
|
||
|
||
document.getElementById("bracket-generate")?.addEventListener("click", () => {
|
||
const input: HTMLInputElement = document.getElementById("playerNumber") as HTMLInputElement;
|
||
if (input.value == "")
|
||
return;
|
||
generateBracket(+input.value);
|
||
});
|
||
}
|
||
}
|