ft_transcendence/frontend/src/pages/pong/pong.ts

597 lines
17 KiB
TypeScript

import {
addRoute,
navigateTo,
setTitle,
type RouteHandlerParams,
type RouteHandlerReturn,
} from "@app/routing";
import authHtml from "./pong.html?raw";
import tourScoresHtml from "./tourTable.html?raw";
import io from "socket.io-client";
import {
JoinRes,
type CSocket,
type GameMove,
type GameUpdate,
type TourInfo,
} from "./socket";
import { showError, showInfo, showSuccess } from "@app/toast";
import { getUser as getSelfUser, type User } from "@app/auth";
import { isNullish } from "@app/utils";
import client from "@app/api";
import "./pong.css";
import { quitChat } from "@app/chat/chatHelperFunctions/quitChat";
declare module "ft_state" {
interface State {
pongSock?: CSocket;
pongKeepAliveInterval?: ReturnType<typeof setInterval>;
}
}
enum QueueState {
InQueu = "In Queue",
InGame = "In Game",
Ready = "Ready-ing",
Iddle = "Queue Up",
In_local = "In Local",
}
enum ReadyState {
readyUp = "ready ok",
readyDown = "not ready",
}
enum TourBtnState {
Joined = "Joined",
Started = "Started",
AbleToJoin = "Join Tournament",
AbleToCreate = "Create Tournament",
AbleToStart = "Start Tournament",
AbeToProcreate = "He would be proud",
}
enum TourInfoState {
Running = "🟢",
Owner = "👑",
Registered = "✅",
NotRegisted = "❌",
NoTournament = "⚪️",
}
document.addEventListener("ft:pageChange", (newUrl) => {
if (window.__state.pongSock !== undefined) window.__state.pongSock.close();
if (window.__state.pongKeepAliveInterval !== undefined)
clearInterval(window.__state.pongKeepAliveInterval);
window.__state.pongSock = undefined;
window.__state.pongKeepAliveInterval = undefined;
});
export function getSocket(): CSocket {
if (window.__state.pongSock === undefined) {
window.__state.pongSock = io(window.location.host, {
path: "/api/pong/socket.io/",
}) as any as CSocket;
}
if (window.__state.pongKeepAliveInterval === undefined) {
window.__state.pongKeepAliveInterval = setInterval(() => {
window.__state.pongSock?.emit("hello");
}, 100);
}
return window.__state.pongSock;
}
function pongClient(
_url: string,
_args: RouteHandlerParams,
): RouteHandlerReturn {
setTitle("Pong Game");
const urlParams = new URLSearchParams(window.location.search);
let game_req_join = urlParams.get("game");
// todo:
// [ ] shape sock
// - [ ] joinGame (guid) -> ["ok"|"no, dont ever talk to my kid or me ever again you creep"];
// - [ ] launch newgame evt?
let inTournament = false;
return {
html: authHtml,
postInsert: async (app) => {
const DEFAULT_COLOR = "white";
const SELF_COLOR = "red";
const user = getSelfUser();
let currentGame: {
game: GameUpdate;
spectating: boolean;
playerL: { id: string; name: string; self: boolean };
playerR: { id: string; name: string; self: boolean };
} | null = null;
const rdy_btn =
document.querySelector<HTMLButtonElement>("#readyup-btn");
const batLeft = document.querySelector<HTMLDivElement>("#batleft");
const batRight =
document.querySelector<HTMLDivElement>("#batright");
const ball = document.querySelector<HTMLDivElement>("#ball");
const score =
document.querySelector<HTMLDivElement>("#score-board");
const playerL =
document.querySelector<HTMLDivElement>("#player-left");
const playerR =
document.querySelector<HTMLDivElement>("#player-right");
const queueBtn =
document.querySelector<HTMLButtonElement>("#QueueBtn");
const LocalGameBtn =
document.querySelector<HTMLButtonElement>("#LocalBtn");
const gameBoard =
document.querySelector<HTMLDivElement>("#pongbox");
const queue_infos =
document.querySelector<HTMLSpanElement>("#queue-info");
const how_to_play_btn =
document.querySelector<HTMLButtonElement>("#play-info");
const protips =
document.querySelector<HTMLDivElement>("#protips-box");
const end_scr =
document.querySelector<HTMLDivElement>("#pong-end-screen");
const tournamentBtn =
document.querySelector<HTMLButtonElement>("#TourBtn");
const tour_infos =
document.querySelector<HTMLButtonElement>("#tour-info");
const tour_scores =
document.querySelector<HTMLDivElement>("#tourscore-box");
let socket = getSocket();
if (isNullish(user)) {
// if no user (no loggin / other) : GTFO
navigateTo("/app");
return;
}
if (
!batLeft ||
!batRight ||
!ball ||
!score ||
!queueBtn ||
!playerL ||
!playerR ||
!gameBoard ||
!queue_infos ||
!LocalGameBtn ||
!rdy_btn ||
!end_scr ||
!tournamentBtn ||
!tour_infos ||
!tour_scores ||
!how_to_play_btn ||
!protips
)
// sanity check
return showError("fatal error");
tournamentBtn.addEventListener("click", () => {
showInfo(`Button State: ${tournamentBtn.innerText}`);
switch (tournamentBtn.innerText) {
case TourBtnState.AbleToStart:
socket.emit("tourStart");
break;
case TourBtnState.AbleToJoin:
socket.emit("tourRegister");
break;
case TourBtnState.AbleToCreate:
socket.emit("tourCreate");
break;
case TourBtnState.AbeToProcreate:
showError("We are developpers, this is impossible !");
break;
case TourBtnState.Joined:
socket.emit("tourUnregister");
break;
case TourBtnState.Started:
break;
}
});
// ---
// keys handler
// ---
const keys: Record<string, boolean> = {};
if (how_to_play_btn && protips)
how_to_play_btn.addEventListener("click", () => {
protips.classList.toggle("hidden");
how_to_play_btn.innerText =
how_to_play_btn.innerText === "?" ? "x" : "?";
});
document.addEventListener("keydown", (e) => {
keys[e.key.toLowerCase()] = true;
});
document.addEventListener("keyup", (e) => {
keys[e.key.toLowerCase()] = false;
});
tour_infos.addEventListener("click", () => {
tour_scores.classList.toggle("hidden");
});
setInterval(() => {
// key sender
if (keys["escape"] === true) {
protips.classList.add("hidden");
tour_scores.classList.add("hidden");
how_to_play_btn.innerText = "?";
}
if (queueBtn.innerText !== QueueState.InGame)
//we're in game ? continue | gtfo
return;
// we are not in a game OR we are spectating a game => gtfo
if (currentGame === null || currentGame.spectating) return;
let packet: GameMove = {
move: null,
moveRight: null,
};
if (queueBtn.innerText !== QueueState.InGame)
//we're in game ? continue | gtfo
return;
if (currentGame === null) return;
if (keys["w"] !== keys["s"])
packet.move = keys["w"] ? "up" : "down";
if (currentGame.game.local && keys["o"] !== keys["l"])
packet.moveRight = keys["o"] ? "up" : "down";
socket.emit("gameMove", packet);
}, 1000 / 60);
// ---
// keys end
// ---
// ---
// join game
// ---
if (game_req_join != null) {
socket.emit("joinGame", game_req_join, (res: JoinRes) => {
switch (res) {
case JoinRes.yes:
showInfo("JoinRes = yes");
quitChat();
break;
case JoinRes.no:
showInfo("JoinRes = no");
break;
default:
showError("JoinRes switch fail:" + res);
}
});
game_req_join = null;
}
// ---
// join game end
// ---
// ---
// position logic (client)
// ---
const DEFAULT_POSITIONS: GameUpdate = {
gameId: "",
ball: { size: 16, x: 800 / 2, y: 450 / 2 },
left: {
id: "",
paddle: { x: 40, y: 185, width: 12, height: 80 },
score: 0,
},
right: {
id: "",
paddle: { x: 748, y: 185, width: 12, height: 80 },
score: 0,
},
local: false,
};
function resetBoard(
batLeft: HTMLDivElement,
batRight: HTMLDivElement,
playerL: HTMLDivElement,
playerR: HTMLDivElement,
) {
render(DEFAULT_POSITIONS);
batLeft.style.backgroundColor = DEFAULT_COLOR;
batRight.style.backgroundColor = DEFAULT_COLOR;
playerR.style.color = "";
playerL.style.color = "";
playerR.innerText = "";
playerL.innerText = "";
currentGame = null;
}
let render_tour_score_once = false;
const renderTournamentScores = (info: TourInfo) => {
let players = info.players.sort((l, r) => r.score - l.score);
const medals = ["🥇", "🥈", "🥉"];
if (!render_tour_score_once) {
tour_scores.innerHTML = tourScoresHtml;
render_tour_score_once = true;
}
let table = tour_scores.querySelector("#tour-score-body");
let table_shadow = document.createElement("tbody");
if (table) {
table_shadow.innerHTML = players
.map(
(player, idx) =>
`<tr class="${player.id === user.id ? "bg-amber-400 hover:bg-amber-500" : "hover:bg-gray-50"}" data-id="${player.id}">
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-semibold min-w-100px"><span class="font-lg medal">${idx < medals.length ? medals[idx] : ""}</span>${player.name}</td>
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-bold min-w-100px">${player.score}</td>
</tr>`,
)
.join("");
if (table_shadow.innerHTML !== table.innerHTML) {
table.innerHTML = table_shadow.innerHTML;
}
}
};
// TODO: REMOVE THIS
// const makePlayer = (n: number) => ({ name: `user${n}`, id: `${n}`, score: n })
//
// renderTournamentScores({
// state: "playing",
// ownerId: "ownerID",
// remainingMatches: null,
// players: [...Array.from(Array(10).keys()).map(makePlayer), { id: user.id, name: user.name, score: 99 }],
// });
const render = (state: GameUpdate) => {
batLeft.style.top = `${state.left.paddle.y}px`;
batLeft.style.left = `${state.left.paddle.x}px`;
batLeft.style.width = `${state.left.paddle.width}px`;
batLeft.style.height = `${state.left.paddle.height}px`;
batRight.style.top = `${state.right.paddle.y}px`;
batRight.style.left = `${state.right.paddle.x}px`;
batRight.style.width = `${state.right.paddle.width}px`;
batRight.style.height = `${state.right.paddle.height}px`;
ball.style.transform = `translateX(${state.ball.x - state.ball.size}px) translateY(${state.ball.y - state.ball.size}px)`;
ball.style.height = `${state.ball.size * 2}px`;
ball.style.width = `${state.ball.size * 2}px`;
score.innerText = `${state.left.score} | ${state.right.score}`;
};
socket.on("gameUpdate", (state: GameUpdate) => {
updateCurrentGame(state);
render(state);
});
socket.on("tourEnding", (ending) => {
inTournament = false;
showInfo(ending);
});
// ---
// position logic (client) end
// ---
// ---
// queue evt
// ---
// utils
async function getUser(
user: string,
): Promise<{ id: string; name: string | null }> {
let t = await client.getUser({ user });
if (t.kind === "success")
return { id: user, name: t.payload.name };
return { id: user, name: null };
}
// btn setup
queueBtn.addEventListener("click", () => {
if (inTournament) {
showError("You can't queue up currently !");
return;
}
if (queueBtn.innerText !== QueueState.Iddle) {
if (queueBtn.innerText === QueueState.InQueu) {
socket.emit("dequeue");
queueBtn.innerText = QueueState.Iddle;
}
return;
}
queueBtn.innerText = QueueState.InQueu;
socket.emit("enqueue");
});
LocalGameBtn.addEventListener("click", () => {
if (
queueBtn.innerText !== QueueState.Iddle ||
currentGame !== null ||
inTournament
) {
showError("cant launch a game currently");
return;
}
socket.emit("localGame");
queueBtn.innerText = QueueState.In_local;
LocalGameBtn.innerText = "playing";
});
rdy_btn.addEventListener("click", () => {
showInfo("rdy-evt");
switch (rdy_btn.innerText) {
case ReadyState.readyDown:
socket.emit("readyUp");
rdy_btn.innerText = ReadyState.readyUp;
rdy_btn.classList.remove("text-red-600");
rdy_btn.classList.add("text-green-600");
break;
case ReadyState.readyUp:
socket.emit("readyDown");
rdy_btn.innerText = ReadyState.readyDown;
rdy_btn.classList.remove("text-green-600");
rdy_btn.classList.add("text-red-600");
break;
default:
showError("error on ready btn");
}
});
const updateCurrentGame = async (state: GameUpdate) => {
const normalizeUser = (
u: { id: string; name: string | null },
d: string,
) => {
return {
id: u.id,
name: u.name ?? d,
self: u.id === user.id,
};
};
if (currentGame === null)
currentGame = {
spectating: !(
state.left.id === user.id ||
state.right.id === user.id
),
game: state,
playerL: normalizeUser(
await getUser(state.left.id),
"left",
),
playerR: normalizeUser(
await getUser(state.right.id),
"right",
),
};
else currentGame.game = state;
if (
(currentGame && currentGame?.game.local) ||
currentGame?.playerL.self
) {
batLeft!.style.backgroundColor = SELF_COLOR;
playerL!.style.color = SELF_COLOR;
}
if (
currentGame &&
!currentGame?.game.local &&
currentGame?.playerR.self
) {
batRight!.style.backgroundColor = SELF_COLOR;
playerR!.style.color = SELF_COLOR;
}
playerL!.innerText = currentGame!.playerL.name;
playerR!.innerText = currentGame!.playerR.name;
};
socket.on("newGame", async (state) => {
currentGame = null;
updateCurrentGame(state);
render(state);
tour_scores.classList.add("hidden");
queueBtn.innerText = QueueState.InGame;
queueBtn.style.color = "red";
batLeft.style.backgroundColor = DEFAULT_COLOR;
batRight.style.backgroundColor = DEFAULT_COLOR;
rdy_btn.classList.remove("hidden");
rdy_btn.classList.add("text-red-600");
rdy_btn.innerText = ReadyState.readyDown;
});
socket.on("rdyEnd", () => {
rdy_btn.classList.remove("text-green-600");
rdy_btn.classList.remove("text-red-600");
rdy_btn.classList.add("hidden");
});
socket.on("gameEnd", (winner) => {
rdy_btn.classList.add("hidden");
queueBtn.innerHTML = QueueState.Iddle;
queueBtn.style.color = "white";
if (!isNullish(currentGame)) {
let end_txt: string = "";
if (
(user.id === currentGame.game.left.id &&
winner === "left") ||
(user.id === currentGame.game.right.id &&
winner === "right")
)
end_txt = "you won! #yippe";
else end_txt = "you lost #sadge";
if (currentGame.spectating)
end_txt = `${winner === "left" ? currentGame.playerL.name : currentGame.playerR.name} won #gg`;
end_scr.innerText = end_txt;
end_scr.classList.remove("hidden");
setTimeout(() => {
end_scr.classList.add("hidden");
}, 3 * 1000);
if (currentGame.game.local) {
LocalGameBtn.innerText = "Local Game";
}
}
resetBoard(batLeft, batRight, playerL, playerR);
});
socket.on("updateInformation", (e) => {
queue_infos.innerText = `${e.totalUser}👤 ${e.inQueue}${e.totalGames}▮•▮`;
});
socket.on("queueEvent", (e) => {
if (e === "registered") queueBtn.innerText = QueueState.InQueu;
else if (e === "unregistered")
queueBtn.innerText = QueueState.Iddle;
showInfo(`QueueEvent: ${e}`);
}); // MAYBE: play a sound? to notify user that smthing happend
// ---
// queue evt end
// ---
socket.on("tournamentInfo", (s) => {
// no tournament => we can create it !
if (s === null) {
tournamentBtn.innerText = TourBtnState.AbleToCreate;
// create tournament
tour_infos.innerText = `${TourInfoState.NoTournament} 0👤 0▮•▮`;
return;
}
let weIn = s.players.some((p) => p.id === user.id);
let imOwner = s.ownerId === user.id;
switch (s.state) {
case "ended":
inTournament = false;
tournamentBtn.innerText = TourBtnState.AbleToCreate;
break;
case "playing":
inTournament = weIn;
tournamentBtn.innerText = TourBtnState.Started;
tour_infos.innerText = `${TourInfoState.Running} ${s.players.length}👤 ${s.remainingMatches ?? "?"}▮•▮`;
break;
case "prestart":
inTournament = weIn;
tour_infos.innerText = `${imOwner ? TourInfoState.Owner : weIn ? TourInfoState.Registered : TourInfoState.NotRegisted} ${s.players.length}👤 ?▮•▮`;
if (imOwner) {
tournamentBtn.innerText = TourBtnState.AbleToStart;
} else {
tournamentBtn.innerText = weIn
? TourBtnState.Joined
: TourBtnState.AbleToJoin;
}
break;
}
renderTournamentScores(s);
});
socket.on("tournamentRegister", ({ kind, msg }) => {
if (kind === "success") showSuccess(msg ?? "Success");
if (kind === "failure") showError(msg ?? "An error Occured");
});
// init
rdy_btn.classList.add("hidden");
queueBtn.innerText = QueueState.Iddle;
rdy_btn.innerText = ReadyState.readyUp;
resetBoard(batLeft, batRight, playerL, playerR);
},
};
}
addRoute("/pong", pongClient);