mirror of
https://github.com/KeyZox71/knl_meowscendence.git
synced 2025-12-31 21:56:41 +01:00
10
.env.example
10
.env.example
@ -11,7 +11,6 @@ GRAPH_PORT=3000
|
|||||||
|
|
||||||
ELK_PORT=5601
|
ELK_PORT=5601
|
||||||
|
|
||||||
GOOGLE_CALLBACK_URL=https://localhost:8443/api/v1
|
|
||||||
# the url to which the user will be redirected when it logs with google
|
# the url to which the user will be redirected when it logs with google
|
||||||
CALLBACK_REDIR=http://localhost:3000
|
CALLBACK_REDIR=http://localhost:3000
|
||||||
GOOGLE_CLIENT_SECRET=susAF
|
GOOGLE_CLIENT_SECRET=susAF
|
||||||
@ -27,5 +26,10 @@ SMTP_AUTH_USERNAME=<smtp-user>
|
|||||||
SMTP_AUTH_PASSWORD=<smtp pass>
|
SMTP_AUTH_PASSWORD=<smtp pass>
|
||||||
EMAIL_TO=<mail to send to>
|
EMAIL_TO=<mail to send to>
|
||||||
|
|
||||||
USER_URL=<the url to the user api>
|
USER_URL=<the internal url to the user api>
|
||||||
AUTH_URL=<the url to the auth api>
|
AUTH_URL=<the internal url to the auth api>
|
||||||
|
SCORE_URL=<the internal url to the score store api>
|
||||||
|
|
||||||
|
CORS_ORIGIN=<the external url of origin for cors>
|
||||||
|
VITE_USER_API=<the external url of the user api>
|
||||||
|
VITE_AUTH_API=<the external url of the auth api>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ services:
|
|||||||
- API_TARGET=user
|
- API_TARGET=user
|
||||||
- LOG_FILE_PATH=/var/log/log.log
|
- LOG_FILE_PATH=/var/log/log.log
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- CORS_ORIGIN=${CORS_ORIGIN}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
auth-api:
|
auth-api:
|
||||||
container_name: transcendence-api-auth
|
container_name: transcendence-api-auth
|
||||||
@ -29,12 +30,13 @@ services:
|
|||||||
- back
|
- back
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
- GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL}
|
- GOOGLE_CALLBACK_URL=${AUTH_URL}
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||||
- API_TARGET=auth
|
- API_TARGET=auth
|
||||||
- LOG_FILE_PATH=/var/log/log.log
|
- LOG_FILE_PATH=/var/log/log.log
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- CORS_ORIGIN=${CORS_ORIGIN}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
scorestore-api:
|
scorestore-api:
|
||||||
container_name: transcendence-api-scoreStore
|
container_name: transcendence-api-scoreStore
|
||||||
|
|||||||
@ -13,6 +13,9 @@ RUN cd /build \
|
|||||||
|
|
||||||
FROM node:lts-alpine AS builder-vite
|
FROM node:lts-alpine AS builder-vite
|
||||||
|
|
||||||
|
ARG VITE_USER_URL
|
||||||
|
ARG VITE_AUTH_URL
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -24,8 +27,8 @@ RUN pnpm install --frozen-lockfile
|
|||||||
COPY vite.config.js tailwind.config.js ./
|
COPY vite.config.js tailwind.config.js ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
RUN pnpm vite build
|
RUN VITE_USER_URL=${VITE_USER_URL} VITE_AUTH_URL=${VITE_AUTH_URL}\
|
||||||
|
pnpm vite build
|
||||||
|
|
||||||
FROM alpine:3.22
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,9 @@ services:
|
|||||||
build:
|
build:
|
||||||
dockerfile: docker/front/Dockerfile
|
dockerfile: docker/front/Dockerfile
|
||||||
context: ../../
|
context: ../../
|
||||||
|
args:
|
||||||
|
- VITE_USER_URL=${VITE_USER_URL}
|
||||||
|
- VITE_AUTH_URL=${VITE_AUTH_URL}
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753250450,
|
"lastModified": 1756542300,
|
||||||
"narHash": "sha256-i+CQV2rPmP8wHxj0aq4siYyohHwVlsh40kV89f3nw1s=",
|
"narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fc02ee70efb805d3b2865908a13ddd4474557ecf",
|
"rev": "d7600c775f877cd87b4f5a831c28aa94137377aa",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
nodejs_22
|
nodejs_22
|
||||||
pnpm
|
pnpm
|
||||||
just
|
just
|
||||||
foundry
|
foundry
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
if [ ! -d node_modules/ ]; then
|
if [ ! -d node_modules/ ]; then
|
||||||
@ -50,6 +50,7 @@
|
|||||||
echo Installing foundry env
|
echo Installing foundry env
|
||||||
forge i
|
forge i
|
||||||
fi
|
fi
|
||||||
|
alias jarvis=just
|
||||||
export PATH+=:$(pwd)/node_modules/.bin
|
export PATH+=:$(pwd)/node_modules/.bin
|
||||||
echo entering ft_trans env
|
echo entering ft_trans env
|
||||||
'';
|
'';
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@avalabs/avalanchejs": "^5.0.0",
|
"@avalabs/avalanchejs": "^5.0.0",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
"@fastify/cors": "^11.1.0",
|
||||||
"@fastify/env": "^5.0.2",
|
"@fastify/env": "^5.0.2",
|
||||||
"@fastify/jwt": "^9.1.0",
|
"@fastify/jwt": "^9.1.0",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -14,6 +14,9 @@ importers:
|
|||||||
'@fastify/cookie':
|
'@fastify/cookie':
|
||||||
specifier: ^11.0.2
|
specifier: ^11.0.2
|
||||||
version: 11.0.2
|
version: 11.0.2
|
||||||
|
'@fastify/cors':
|
||||||
|
specifier: ^11.1.0
|
||||||
|
version: 11.1.0
|
||||||
'@fastify/env':
|
'@fastify/env':
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
@ -261,6 +264,9 @@ packages:
|
|||||||
'@fastify/cookie@11.0.2':
|
'@fastify/cookie@11.0.2':
|
||||||
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
|
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
|
||||||
|
|
||||||
|
'@fastify/cors@11.1.0':
|
||||||
|
resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==}
|
||||||
|
|
||||||
'@fastify/deepmerge@2.0.2':
|
'@fastify/deepmerge@2.0.2':
|
||||||
resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==}
|
resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==}
|
||||||
|
|
||||||
@ -1903,6 +1909,11 @@ snapshots:
|
|||||||
cookie: 1.0.2
|
cookie: 1.0.2
|
||||||
fastify-plugin: 5.0.1
|
fastify-plugin: 5.0.1
|
||||||
|
|
||||||
|
'@fastify/cors@11.1.0':
|
||||||
|
dependencies:
|
||||||
|
fastify-plugin: 5.0.1
|
||||||
|
toad-cache: 3.7.0
|
||||||
|
|
||||||
'@fastify/deepmerge@2.0.2': {}
|
'@fastify/deepmerge@2.0.2': {}
|
||||||
|
|
||||||
'@fastify/env@5.0.2':
|
'@fastify/env@5.0.2':
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import fastifyJWT from '@fastify/jwt';
|
import fastifyJWT from '@fastify/jwt';
|
||||||
import fastifyCookie from '@fastify/cookie';
|
import fastifyCookie from '@fastify/cookie';
|
||||||
|
import cors from '@fastify/cors'
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import cors from '@fastify/cors';
|
import cors from '@fastify/cors';
|
||||||
|
|
||||||
@ -148,6 +149,13 @@ const querySchemaMatchHistoryGame = { type: 'object', required: ['game'], proper
|
|||||||
* @param {import('fastify').FastifyPluginOptions} options
|
* @param {import('fastify').FastifyPluginOptions} options
|
||||||
*/
|
*/
|
||||||
export default async function(fastify, options) {
|
export default async function(fastify, options) {
|
||||||
|
|
||||||
|
fastify.register(cors, {
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||||
|
credentials: true,
|
||||||
|
methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ]
|
||||||
|
});
|
||||||
|
|
||||||
fastify.register(fastifyJWT, {
|
fastify.register(fastifyJWT, {
|
||||||
secret: process.env.JWT_SECRET || '123456789101112131415161718192021',
|
secret: process.env.JWT_SECRET || '123456789101112131415161718192021',
|
||||||
cookie: {
|
cookie: {
|
||||||
|
|||||||
@ -1,19 +1,57 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite + Tailwind Test</title>
|
|
||||||
<link href="/style.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
|
|
||||||
<div class="text-center p-10 bg-white rounded-xl shadow space-y-4">
|
|
||||||
<h1 class="text-4xl font-bold text-blue-600">Vite + Tailwind</h1>
|
|
||||||
<p class="text-gray-700 text-lg">🚀 Looks like it's working!</p>
|
|
||||||
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
|
|
||||||
Click Me
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link href="/static/assets/favicon.ico" rel="icon" type="image/x-icon" >
|
||||||
|
<title>Vite + Tailwind Test</title>
|
||||||
|
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<body class="bg-[url(https://api.kanel.ovh/random)] dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col font-[Lexend]">
|
||||||
|
|
||||||
|
<div class="absolute flex flex-col items-center space-y-5 top-4 left-5">
|
||||||
|
<!--a class="absolute flex flex-col items-center top-4 left-5" href="/pong" data-icon-->
|
||||||
|
<a class="flex flex-col items-center" href="/pong" data-icon>
|
||||||
|
<img src="./static/assets/pong.svg" width=32 height=32 />
|
||||||
|
<span class="text-white font-[Kubasta]">pong_game.ts</span>
|
||||||
|
</a>
|
||||||
|
<a class="flex flex-col items-center" href="/tetris" data-icon>
|
||||||
|
<img src="./static/assets/tetrio.svg" width=32 height=32 />
|
||||||
|
<span class="text-white font-[Kubasta]">tetris_game.ts</span>
|
||||||
|
</a>
|
||||||
|
<!--a class="flex flex-col items-center" href="https://tetr.io/">
|
||||||
|
<img src="./static/assets/tetrio.svg" width=32 height=32 />
|
||||||
|
<span class="text-white font-[Kubasta]">tetr.io</span>
|
||||||
|
</a-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="flex-1 flex items-center justify-center">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="taskbar-menu" class="absolute bottom-13 left-0"></div>
|
||||||
|
<div id="friends-menu" class="absolute bottom-13 right-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 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 id="start-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 target="_blank" class="taskbar-button" href="https://rusty.42angouleme.fr/">rusty</a>
|
||||||
|
<a target="_blank" class="taskbar-button" href="https://dn720004.ca.archive.org/0/items/2009-tetris-variant-concepts_202201/2009%20Tetris%20Design%20Guideline.pdf">tetris-guideline.pdf</a>
|
||||||
|
</div>
|
||||||
|
<div id="taskbar-trail" class="flex px-4 items-center content-center space-x-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/static/ts/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
BIN
src/front/static/assets/favicon.ico
Normal file
BIN
src/front/static/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
src/front/static/assets/fonts/Kubasta.otf
Normal file
BIN
src/front/static/assets/fonts/Kubasta.otf
Normal file
Binary file not shown.
BIN
src/front/static/assets/fonts/Kubasta.ttf
Normal file
BIN
src/front/static/assets/fonts/Kubasta.ttf
Normal file
Binary file not shown.
17
src/front/static/assets/pong.svg
Normal file
17
src/front/static/assets/pong.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080" width="1080" height="1080">
|
||||||
|
<defs>
|
||||||
|
<image width="1080" height="1080" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4AQMAAADW3v7MAAAAAXNSR0IB2cksfwAAAANQTFRFAAAAp3o92gAAA0hJREFUeJztzjEBACAMwDDm3zTPniiAo1GQOX+Y14HVQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQxfkswQ5pU9WdQAAAABJRU5ErkJggg=="/>
|
||||||
|
<image width="97" height="97" id="img2" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAQMAAAD8yF3gAAAAAXNSR0IB2cksfwAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/v/Rru6GAAAAHUlEQVR4nGOsb2CAg0bGUd4ob5Q3yhvlEctjQAYA62mQYokW47wAAAAASUVORK5CYII="/>
|
||||||
|
<image width="97" height="97" id="img3" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAQMAAAD8yF3gAAAAAXNSR0IB2cksfwAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAAHUlEQVR4nGNkQAaMo7xR3ihvpPL+M/xnGuVRwAMA3qIuYqMkUaQAAAAASUVORK5CYII="/>
|
||||||
|
<image width="97" height="97" id="img4" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAgMAAAC7aCcwAAAAAXNSR0IB2cksfwAAAAlQTFRF////////////jkrl/gAAAAN0Uk5T/v8A1ghndgAAACtJREFUeJxjFHVgwApeM47KjMqMyozKjMqMyozK0FJmFYj6v2ZUZlSGejIAqSynoiv80kgAAAAASUVORK5CYII="/>
|
||||||
|
</defs>
|
||||||
|
<style>
|
||||||
|
.s0 { fill: #ffffff }
|
||||||
|
</style>
|
||||||
|
<use id="Background" href="#img1" x="0" y="0"/>
|
||||||
|
<path id="Layer 1" fill-rule="evenodd" class="s0" d="m1008 746v97h-937v-97z"/>
|
||||||
|
<path id="Shape 1" fill-rule="evenodd" class="s0" d="m729 459v97h-97v-97z"/>
|
||||||
|
<use id="Shape 1 copy" style="opacity: .7" href="#img2" x="706" y="385"/>
|
||||||
|
<use id="Shape 1 copy 2" style="opacity: .4" href="#img3" x="780" y="311"/>
|
||||||
|
<use id="Shape 1 copy 3" style="opacity: .2" href="#img4" x="854" y="237"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
114
src/front/static/assets/tetrio.svg
Normal file
114
src/front/static/assets/tetrio.svg
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="60mm"
|
||||||
|
height="60mm"
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
version="1.1"
|
||||||
|
id="svg9638"
|
||||||
|
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||||
|
sodipodi:docname="tetrio.svg"
|
||||||
|
inkscape:export-filename="D:\Projects\tetrio\client\res\tetriox256.png"
|
||||||
|
inkscape:export-xdpi="108.37334"
|
||||||
|
inkscape:export-ydpi="108.37334">
|
||||||
|
<defs
|
||||||
|
id="defs9632" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1.979899"
|
||||||
|
inkscape:cx="121.84373"
|
||||||
|
inkscape:cy="101.26992"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="1676"
|
||||||
|
inkscape:window-y="-4"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata9635">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-237)">
|
||||||
|
<rect
|
||||||
|
style="opacity:0.07000002;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.29149818;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect14257"
|
||||||
|
width="55"
|
||||||
|
height="55"
|
||||||
|
x="2.05"
|
||||||
|
y="239.5" />
|
||||||
|
<rect
|
||||||
|
style="opacity:0.07000002;fill:#000000;fill-opacity:1;stroke:none;stroke-width:20.26499748;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect14257-7"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
x="5"
|
||||||
|
y="242" />
|
||||||
|
<rect
|
||||||
|
style="opacity:0.07000002;fill:#0e0b0e;fill-opacity:1;stroke:none;stroke-width:18.23849869;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect14257-7-6"
|
||||||
|
width="45"
|
||||||
|
height="45"
|
||||||
|
x="7.5"
|
||||||
|
y="244.5" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#df4eaa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 0,237 v 10 h 10 v 10 h 10 v -10 h 10 v -10 z"
|
||||||
|
id="path14118"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccccc" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#c040aa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect14120"
|
||||||
|
width="10"
|
||||||
|
height="40"
|
||||||
|
x="10"
|
||||||
|
y="257" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#7e5fe3;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect14120-0"
|
||||||
|
width="10"
|
||||||
|
height="40"
|
||||||
|
x="20"
|
||||||
|
y="257" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#2f51aa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 30,287 H 50 V 277 H 40 V 257 H 30 Z"
|
||||||
|
id="path14118-8"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#15919d;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 60,247 H 40 v 10 h 10 v 20 h 10 z"
|
||||||
|
id="path14118-8-3"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
88
src/front/static/css/style.css
Normal file
88
src/front/static/css/style.css
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
@layer utilities {
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Kubasta;
|
||||||
|
src: url('../assets/fonts/Kubasta.otf') format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@source inline("space-y-{18,46,102,214,438,886,1782,3574,7158,14326,28662,57334,114678,229366,458742,917494}");
|
||||||
|
@source inline("mt-{28,56,84,112}");
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--shadow-2x1: 2px 2px 0px black;
|
||||||
|
/*
|
||||||
|
--color-kanel-700: #ac5c24;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-border {
|
||||||
|
@apply border-2
|
||||||
|
border-t-neutral-100 border-l-neutral-100 border-r-neutral-400 border-b-neutral-400
|
||||||
|
dark:border-t-neutral-500 dark:border-l-neutral-500 dark:border-r-neutral-700 dark:border-b-neutral-700
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reverse-border {
|
||||||
|
@apply border-2
|
||||||
|
border-t-neutral-400 border-l-neutral-400 border-r-neutral-100 border-b-neutral-100
|
||||||
|
dark:border-t-neutral-700 dark:border-l-neutral-700 dark:border-r-neutral-500 dark:border-b-neutral-500
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-border {
|
||||||
|
@apply border-2
|
||||||
|
border-t-neutral-950 border-l-neutral-950 border-r-neutral-200 border-b-neutral-200
|
||||||
|
dark:border-t-neutral-950 dark:border-l-neutral-950 dark:border-r-neutral-600 dark:border-b-neutral-600
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-button {
|
||||||
|
@apply shadow-2x1
|
||||||
|
bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700
|
||||||
|
text-neutral-900 dark:text-white
|
||||||
|
px-4 py-2
|
||||||
|
delay-0 duration-150 transition-colors
|
||||||
|
|
||||||
|
border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700
|
||||||
|
active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-button {
|
||||||
|
@apply shadow-2x1
|
||||||
|
bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700
|
||||||
|
text-neutral-900 dark:text-white
|
||||||
|
px-4 py-0.5
|
||||||
|
content-center text-center
|
||||||
|
delay-0 duration-150 transition-colors
|
||||||
|
|
||||||
|
border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700
|
||||||
|
active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-default-button {
|
||||||
|
@apply w-46 h-12
|
||||||
|
text-neutral-900 dark:text-white
|
||||||
|
bg-neutral-200 hover:bg-neutral-300
|
||||||
|
dark:bg-neutral-800 dark:hover:bg-neutral-700
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-default-label {
|
||||||
|
@apply w-46 h-12
|
||||||
|
text-neutral-900 dark:text-white
|
||||||
|
bg-neutral-200
|
||||||
|
dark:bg-neutral-800
|
||||||
|
;
|
||||||
|
}
|
||||||
174
src/front/static/ts/main.ts
Normal file
174
src/front/static/ts/main.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { oneko } from "./oneko.ts";
|
||||||
|
import ProfileMenu from "./views/ProfileMenu.ts";
|
||||||
|
import FriendsMenu from "./views/Friends.ts";
|
||||||
|
let profile_view = new ProfileMenu;
|
||||||
|
let friends_view = new FriendsMenu;
|
||||||
|
|
||||||
|
export const user_api = import.meta.env.VITE_USER_API as String;
|
||||||
|
export const auth_api = import.meta.env.VITE_AUTH_API as String;
|
||||||
|
|
||||||
|
export async function isLogged(): Promise<boolean> {
|
||||||
|
let uuid_req = await fetch(auth_api + "/me", {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (uuid_req.status === 200) {
|
||||||
|
let uuid = await uuid_req.json();
|
||||||
|
document.cookie = `uuid=${uuid.user};max-age=${60 * 60 * 24 * 7}`;
|
||||||
|
if (!document.getElementById("friends-btn"))
|
||||||
|
{
|
||||||
|
const btn: HTMLButtonElement = document.createElement("button") as HTMLButtonElement;
|
||||||
|
btn.id = "friends-btn";
|
||||||
|
btn?.classList.add("taskbar-button");
|
||||||
|
btn.innerText = "friends";
|
||||||
|
document.getElementById("taskbar-trail")?.prepend(btn);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else // 401
|
||||||
|
{
|
||||||
|
document.cookie = `uuid=;max-age=0`;
|
||||||
|
const btn = document.getElementById("friends-btn") as HTMLButtonElement;
|
||||||
|
if (btn) btn.remove();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const navigationManager = url => {
|
||||||
|
history.pushState(null, null, url);
|
||||||
|
router();
|
||||||
|
};
|
||||||
|
|
||||||
|
let view;
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: "/", view: () => import("./views/MainMenu.ts") },
|
||||||
|
|
||||||
|
{ path: "/pong", view: () => import("./views/PongMenu.ts") },
|
||||||
|
{ path: "/pong/local", view: () => import("./views/Pong.ts") },
|
||||||
|
{ path: "/pong/tournament", view: () => import("./views/TournamentMenu.ts") },
|
||||||
|
|
||||||
|
{ path: "/tetris", view: () => import("./views/TetrisMenu.ts") },
|
||||||
|
{ path: "/tetris/solo", view: () => import("./views/Tetris.ts") },
|
||||||
|
{ path: "/tetris/versus", view: () => import("./views/TetrisVersus.ts") },
|
||||||
|
|
||||||
|
{ path: "/login", view: () => import("./views/LoginPage.ts") },
|
||||||
|
{ path: "/register", view: () => import("./views/RegisterPage.ts") },
|
||||||
|
|
||||||
|
{ path: "/profile", view: () => import("./views/Profile.ts") },
|
||||||
|
{ path: "/settings", view: () => import("./views/Settings.ts") },
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = async () => {
|
||||||
|
|
||||||
|
const routesMap = routes.map(route => {
|
||||||
|
return { route: route, isMatch: location.pathname === route.path };
|
||||||
|
});
|
||||||
|
|
||||||
|
let match = routesMap.find(routeMap => routeMap.isMatch);
|
||||||
|
|
||||||
|
if (!match)
|
||||||
|
match = { route: routes[0], isMatch: true };
|
||||||
|
|
||||||
|
if (view)
|
||||||
|
view.running = false;
|
||||||
|
|
||||||
|
//console.log(match);
|
||||||
|
|
||||||
|
const module = await match.route.view();
|
||||||
|
view = new module.default();
|
||||||
|
|
||||||
|
document.querySelector("#app").innerHTML = await view.getHTML();
|
||||||
|
view.run();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("profile-button")?.addEventListener("click", () => { profile_view.run(); });
|
||||||
|
window.addEventListener("popstate", router);
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
isLogged();
|
||||||
|
|
||||||
|
document.body.addEventListener("click", e => {
|
||||||
|
if (!e.target.closest("#taskbar-menu") && !e.target.matches("#profile-button")) {
|
||||||
|
profile_view.open = false;
|
||||||
|
document.getElementById("taskbar-menu").innerHTML = "";
|
||||||
|
}
|
||||||
|
if (e.target.matches("#friends-btn")) {
|
||||||
|
friends_view.open = !friends_view.open;
|
||||||
|
friends_view.run();
|
||||||
|
}
|
||||||
|
if (e.target.matches("[data-link]")) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigationManager(e.target.href);
|
||||||
|
}
|
||||||
|
if (e.target.closest("[data-icon]"))
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("dblclick", e => {
|
||||||
|
if (e.target.closest("[data-icon]")) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigationManager(e.target.closest("[data-icon]").href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router();
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pingClock() {
|
||||||
|
if (await isLogged()) {
|
||||||
|
fetch(user_api + "/ping", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateClock, 5000);
|
||||||
|
updateClock();
|
||||||
|
|
||||||
|
setInterval(pingClock, 30000);
|
||||||
|
|
||||||
|
oneko();
|
||||||
|
|
||||||
|
async function startMenuPP() {
|
||||||
|
const profileButton = document.getElementById("start-img") as HTMLImageElement;
|
||||||
|
try {
|
||||||
|
if(document.cookie.match(new RegExp('(^| )' + "token" + '=([^;]+)'))) {
|
||||||
|
throw "not today, thank you";
|
||||||
|
}
|
||||||
|
let uuid: String;
|
||||||
|
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
|
||||||
|
|
||||||
|
const a = await fetch(`http://localhost:3002/users/${uuid}/avatar`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
profileButton.src = a.status === 200
|
||||||
|
? `http://localhost:3002/users/${uuid}/avatar?t=${Date.now()}`
|
||||||
|
: "https://api.kanel.ovh/pp";
|
||||||
|
} catch (err){
|
||||||
|
// console.log("not yet logged, going default for start icon...");
|
||||||
|
profileButton.src = "https://api.kanel.ovh/id?id=65";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(startMenuPP, 3000);
|
||||||
|
startMenuPP();
|
||||||
284
src/front/static/ts/oneko.ts
Normal file
284
src/front/static/ts/oneko.ts
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
// oneko.js: https://github.com/adryd325/oneko.js
|
||||||
|
// edited by yosyo specificely for knl_meowscendence.
|
||||||
|
|
||||||
|
let oneko_state: number = 0; // 0 = normal, 1 = pong, 2 = tetris
|
||||||
|
let mousePosX: number = 0;
|
||||||
|
let mousePosY: number = 0;
|
||||||
|
let offsetX: number = 0;
|
||||||
|
let offsetY: number = 0;
|
||||||
|
|
||||||
|
export function setOnekoState(state: string) {
|
||||||
|
switch (state) {
|
||||||
|
case "pong":
|
||||||
|
oneko_state = 1;
|
||||||
|
break;
|
||||||
|
case "tetris":
|
||||||
|
oneko_state = 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
oneko_state = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOnekoOffset() {
|
||||||
|
if (oneko_state === 1)
|
||||||
|
{
|
||||||
|
offsetX = document.getElementById("window").offsetLeft + 44;
|
||||||
|
offsetY = document.getElementById("window").offsetTop + 44 + 24;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSleepPos() {
|
||||||
|
mousePosX = document.getElementById("window")?.offsetLeft + 120;
|
||||||
|
mousePosY = document.getElementById("window")?.offsetTop + 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBallPos(x: number, y: number)
|
||||||
|
{
|
||||||
|
mousePosX = x + offsetX;
|
||||||
|
mousePosY = y + offsetY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oneko() {
|
||||||
|
const isReducedMotion =
|
||||||
|
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
|
||||||
|
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
|
||||||
|
|
||||||
|
if (isReducedMotion) return ;
|
||||||
|
|
||||||
|
const nekoEl = document.createElement("div");
|
||||||
|
|
||||||
|
let nekoPosX = 256;
|
||||||
|
let nekoPosY = 256;
|
||||||
|
|
||||||
|
let frameCount = 0;
|
||||||
|
let idleTime = 0;
|
||||||
|
let idleAnimation = null;
|
||||||
|
let idleAnimationFrame = 0;
|
||||||
|
|
||||||
|
const nekoSpeed = 10;
|
||||||
|
const spriteSets = {
|
||||||
|
idle: [[-3, -3]],
|
||||||
|
alert: [[-7, -3]],
|
||||||
|
scratchSelf: [
|
||||||
|
[-5, 0],
|
||||||
|
[-6, 0],
|
||||||
|
[-7, 0],
|
||||||
|
],
|
||||||
|
scratchWallN: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -1],
|
||||||
|
],
|
||||||
|
scratchWallS: [
|
||||||
|
[-7, -1],
|
||||||
|
[-6, -2],
|
||||||
|
],
|
||||||
|
scratchWallE: [
|
||||||
|
[-2, -2],
|
||||||
|
[-2, -3],
|
||||||
|
],
|
||||||
|
scratchWallW: [
|
||||||
|
[-4, 0],
|
||||||
|
[-4, -1],
|
||||||
|
],
|
||||||
|
tired: [[-3, -2]],
|
||||||
|
sleeping: [
|
||||||
|
[-2, 0],
|
||||||
|
[-2, -1],
|
||||||
|
],
|
||||||
|
N: [
|
||||||
|
[-1, -2],
|
||||||
|
[-1, -3],
|
||||||
|
],
|
||||||
|
NE: [
|
||||||
|
[0, -2],
|
||||||
|
[0, -3],
|
||||||
|
],
|
||||||
|
E: [
|
||||||
|
[-3, 0],
|
||||||
|
[-3, -1],
|
||||||
|
],
|
||||||
|
SE: [
|
||||||
|
[-5, -1],
|
||||||
|
[-5, -2],
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[-6, -3],
|
||||||
|
[-7, -2],
|
||||||
|
],
|
||||||
|
SW: [
|
||||||
|
[-5, -3],
|
||||||
|
[-6, -1],
|
||||||
|
],
|
||||||
|
W: [
|
||||||
|
[-4, -2],
|
||||||
|
[-4, -3],
|
||||||
|
],
|
||||||
|
NW: [
|
||||||
|
[-1, 0],
|
||||||
|
[-1, -1],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
nekoEl.id = "oneko";
|
||||||
|
nekoEl.ariaHidden = true;
|
||||||
|
nekoEl.style.width = "32px";
|
||||||
|
nekoEl.style.height = "32px";
|
||||||
|
nekoEl.style.position = "fixed";
|
||||||
|
nekoEl.style.pointerEvents = "none";
|
||||||
|
nekoEl.style.imageRendering = "pixelated";
|
||||||
|
nekoEl.style.left = `${nekoPosX - 16}px`;
|
||||||
|
nekoEl.style.top = `${nekoPosY - 16}px`;
|
||||||
|
nekoEl.style.zIndex = 2147483647;
|
||||||
|
|
||||||
|
let nekoFile = "https://kanel.ovh/assets/oneko.gif"
|
||||||
|
const curScript = document.currentScript
|
||||||
|
if (curScript && curScript.dataset.cat) {
|
||||||
|
nekoFile = curScript.dataset.cat
|
||||||
|
}
|
||||||
|
nekoEl.style.backgroundImage = `url(${nekoFile})`;
|
||||||
|
|
||||||
|
document.body.appendChild(nekoEl);
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", function (event) {
|
||||||
|
if (oneko_state == 0)
|
||||||
|
{
|
||||||
|
mousePosX = event.clientX;
|
||||||
|
mousePosY = event.clientY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.requestAnimationFrame(onAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastFrameTimestamp: number;
|
||||||
|
|
||||||
|
function onAnimationFrame(timestamp: number) {
|
||||||
|
// Stops execution if the neko element is removed from DOM
|
||||||
|
if (!nekoEl.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!lastFrameTimestamp) {
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
if (timestamp - lastFrameTimestamp > 100) {
|
||||||
|
lastFrameTimestamp = timestamp
|
||||||
|
frame()
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(onAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSprite(name, frame) {
|
||||||
|
const sprite = spriteSets[name][frame % spriteSets[name].length];
|
||||||
|
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetIdleAnimation() {
|
||||||
|
idleAnimation = null;
|
||||||
|
idleAnimationFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function idle() {
|
||||||
|
idleTime += 1;
|
||||||
|
|
||||||
|
// every ~ 20 seconds
|
||||||
|
if (oneko_state === 2) {
|
||||||
|
idleAnimation = "sleeping";
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
idleTime > 10 &&
|
||||||
|
Math.floor(Math.random() * 200) == 0 &&
|
||||||
|
idleAnimation == null
|
||||||
|
) {
|
||||||
|
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
|
||||||
|
if (nekoPosX < 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallW");
|
||||||
|
}
|
||||||
|
if (nekoPosY < 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallN");
|
||||||
|
}
|
||||||
|
if (nekoPosX > window.innerWidth - 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallE");
|
||||||
|
}
|
||||||
|
if (nekoPosY > window.innerHeight - 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallS");
|
||||||
|
}
|
||||||
|
idleAnimation =
|
||||||
|
avalibleIdleAnimations[
|
||||||
|
Math.floor(Math.random() * avalibleIdleAnimations.length)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (idleAnimation) {
|
||||||
|
case "sleeping":
|
||||||
|
if (idleAnimationFrame < 8) {
|
||||||
|
setSprite("tired", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
|
||||||
|
if (idleAnimationFrame > 192) {
|
||||||
|
resetIdleAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "scratchWallN":
|
||||||
|
case "scratchWallS":
|
||||||
|
case "scratchWallE":
|
||||||
|
case "scratchWallW":
|
||||||
|
case "scratchSelf":
|
||||||
|
setSprite(idleAnimation, idleAnimationFrame);
|
||||||
|
if (idleAnimationFrame > 9) {
|
||||||
|
resetIdleAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setSprite("idle", 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
idleAnimationFrame += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function frame() {
|
||||||
|
frameCount += 1;
|
||||||
|
const diffX = nekoPosX - mousePosX;
|
||||||
|
const diffY = nekoPosY - mousePosY;
|
||||||
|
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
||||||
|
|
||||||
|
if (distance < nekoSpeed || distance < 48) {
|
||||||
|
idle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
idleAnimation = null;
|
||||||
|
idleAnimationFrame = 0;
|
||||||
|
|
||||||
|
if (idleTime > 1) {
|
||||||
|
setSprite("alert", 0);
|
||||||
|
// count down after being alerted before moving
|
||||||
|
idleTime = Math.min(idleTime, 7);
|
||||||
|
idleTime -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction;
|
||||||
|
direction = diffY / distance > 0.5 ? "N" : "";
|
||||||
|
direction += diffY / distance < -0.5 ? "S" : "";
|
||||||
|
direction += diffX / distance > 0.5 ? "W" : "";
|
||||||
|
direction += diffX / distance < -0.5 ? "E" : "";
|
||||||
|
setSprite(direction, frameCount);
|
||||||
|
|
||||||
|
nekoPosX -= (diffX / distance) * nekoSpeed;
|
||||||
|
nekoPosY -= (diffY / distance) * nekoSpeed;
|
||||||
|
|
||||||
|
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
|
||||||
|
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
|
||||||
|
|
||||||
|
nekoEl.style.left = `${nekoPosX - 16}px`;
|
||||||
|
nekoEl.style.top = `${nekoPosY - 16}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
}
|
||||||
10
src/front/static/ts/views/Aview.ts
Normal file
10
src/front/static/ts/views/Aview.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default class {
|
||||||
|
contructor()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle(title) { document.title = title; }
|
||||||
|
|
||||||
|
async getHTML() { return ""; }
|
||||||
|
async run() { }
|
||||||
|
};
|
||||||
200
src/front/static/ts/views/Friends.ts
Normal file
200
src/front/static/ts/views/Friends.ts
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.ts";
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
import { isLogged, navigationManager, user_api, auth_api } from "../main.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
open: Boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.setTitle("friends list");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<div class="relative b-0 default-border bg-neutral-200">
|
||||||
|
<div class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-40 justify-between px-2">
|
||||||
|
<span class="font-[Kubasta]">friends.ts</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-200 pb-5 dark:bg-neutral-800 justify-center text-center reverse-border">
|
||||||
|
<form method="dialog" class="justify-center bg-neutral-200 dark:bg-neutral-800 space-y-4 space-x-2 px-4 pt-4">
|
||||||
|
<input type="text" id="new-friend" placeholder="new friend" class="bg-white text-neutral-900 input-border" required></input>
|
||||||
|
<button id="add-friends-button" type="submit" class="default-button text-center mx-0 my-0">add friend</button>
|
||||||
|
<p id="add-friend-err" class="hidden text-red-700 dark:text-red-500"></p>
|
||||||
|
<p id="add-friend-msg" class="hidden text-gray-900 dark:text-white text-lg"></p>
|
||||||
|
</form>
|
||||||
|
<p id="friends-error-message" class="hidden text-red-700 dark:text-red-500"></p>
|
||||||
|
<p id="friend-msg" class="hidden text-gray-900 dark:text-white text-lg"></p>
|
||||||
|
<div class="flex flex-row space-x-4 w-full min-w-60 px-4 py-2">
|
||||||
|
<ul id="friends_list" class="bg-neutral-300 dark:bg-neutral-900 reverse-border space-y-2 hidden text-gray-900 dark:text-white overflow-scroll h-48 w-full">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
if (!await isLogged())
|
||||||
|
navigationManager("/");
|
||||||
|
|
||||||
|
if (this.open === true) {
|
||||||
|
document.getElementById("friends-menu").innerHTML = await this.getHTML();
|
||||||
|
} else {
|
||||||
|
document.getElementById("friends-menu").innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uuid: String;
|
||||||
|
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
const friends_error_message = (document.getElementById("friends-error-message") 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: String) {
|
||||||
|
const data_req = await fetch(user_api + "/users/" + uuid + "/friends/" + name, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (data_req.status === 200) {
|
||||||
|
console.log("friends removed successfully");
|
||||||
|
} else {
|
||||||
|
console.log("could not remove friend");
|
||||||
|
}
|
||||||
|
list_friends();
|
||||||
|
list_friends();
|
||||||
|
list_friends();
|
||||||
|
list_friends();
|
||||||
|
list_friends();
|
||||||
|
list_friends();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isFriendLogged(name: string): Promise<Boolean> {
|
||||||
|
const data_req = await fetch(user_api + "/ping/" + name, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data_req.status === 404)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (await data_req.json()).isLogged
|
||||||
|
}
|
||||||
|
|
||||||
|
const list_friends = async () => {
|
||||||
|
const data_req = await fetch(user_api + "/users/" + uuid + "/friends/count", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (data_req.status === 404) {
|
||||||
|
}
|
||||||
|
let data = await data_req.json();
|
||||||
|
while (friends_list.firstChild) {
|
||||||
|
friends_list.removeChild(friends_list.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.n_friends > 0) {
|
||||||
|
const list_req = await fetch(user_api + "/users/" + uuid + "/friends?iStart=0&iEnd=2147483647", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (list_req.status == 404) {
|
||||||
|
friends_list.classList.add("hidden")
|
||||||
|
return;
|
||||||
|
} else if (list_req.status === 200) {
|
||||||
|
friends_list.classList.remove("hidden")
|
||||||
|
|
||||||
|
let list = (await list_req.json()).friends as JSON;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.n_friends; i++) {
|
||||||
|
let new_friends = document.createElement('li');
|
||||||
|
|
||||||
|
const activitySpan = document.createElement('span');
|
||||||
|
const isLogged = await isFriendLogged(list[i].friendName)
|
||||||
|
activitySpan.textContent = "•";
|
||||||
|
if (isLogged == true)
|
||||||
|
activitySpan.className = "px-0 text-green-500";
|
||||||
|
else
|
||||||
|
activitySpan.className = "px-0 text-red-500";
|
||||||
|
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = "px-3";
|
||||||
|
span.textContent = list[i].friendName;
|
||||||
|
|
||||||
|
const but = document.createElement('button');
|
||||||
|
but.textContent = "-";
|
||||||
|
but.classList.add("px-0", "py-0", "taskbar-button");
|
||||||
|
but.onclick = function() {
|
||||||
|
removeFriend(list[i].friendName);
|
||||||
|
};
|
||||||
|
|
||||||
|
new_friends.appendChild(activitySpan);
|
||||||
|
new_friends.appendChild(span);
|
||||||
|
new_friends.appendChild(but);
|
||||||
|
friends_list.appendChild(new_friends);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
friends_error_message.innerHTML = (await list_req.json()).error;
|
||||||
|
friends_error_message.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
friends_list.classList.add("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const add_friend = async () => {
|
||||||
|
const data_req = await fetch(user_api + "/users/" + uuid + "/friends/" + new_friend.value, {
|
||||||
|
method: "POST",
|
||||||
|
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()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list_friends()
|
||||||
|
new_friend.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data_req = await fetch(user_api + "/users/" + uuid + "/friends/count", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (data_req.status === 200) {
|
||||||
|
// let data = await data_req.json();
|
||||||
|
list_friends()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
friends_error_message.innerHTML = "failed to fetch friends";
|
||||||
|
friends_error_message.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
document.getElementById("add-friends-button")?.addEventListener("click", add_friend);
|
||||||
|
setInterval(list_friends, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
197
src/front/static/ts/views/LoginPage.ts
Normal file
197
src/front/static/ts/views/LoginPage.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.ts"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
import { isLogged, navigationManager, user_api, auth_api } from "../main.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("login");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
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]">login.ts</span>
|
||||||
|
<div>
|
||||||
|
<button> - </button>
|
||||||
|
<button> □ </button>
|
||||||
|
<a href="/" data-link> × </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 reverse-border flex flex-col items-center">
|
||||||
|
<form method="dialog" class="space-y-4">
|
||||||
|
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome back ! please login.</h1>
|
||||||
|
<div class="flex flex-row justify-between space-x-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<button id="login-button" type="submit" class="default-button w-full">login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
|
||||||
|
|
||||||
|
<hr class="my-4 w-64 reverse-border">
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-4 w-full">
|
||||||
|
<a target="_blank" id="login-google" class="default-button inline-flex items-center justify-center w-full">
|
||||||
|
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" height=20 width=20 class="mr-2 justify-self-start" />
|
||||||
|
login with google
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href="https://rusty.42angouleme.fr/issues/all" class="default-button inline-flex items-center justify-center w-full">
|
||||||
|
<img src="https://rusty.42angouleme.fr/assets/favicon-bb06adc80c8495db.ico" height=20 width=20 class="mr-2 justify-self-start" />
|
||||||
|
login with rusty
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
document.getElementById("login-google").href = `${auth_api}/login/google`;
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
|
||||||
|
const totpVerify = async () => {
|
||||||
|
const username = (document.getElementById("username") as HTMLInputElement).value;
|
||||||
|
const password = (document.getElementById("password") as HTMLInputElement).value;
|
||||||
|
const totpPin = (document.getElementById('totpPin') as HTMLInputElement).value;
|
||||||
|
const idWindow = (document.getElementById('2fa-popup-content') as HTMLInputElement);
|
||||||
|
try {
|
||||||
|
const data_req = await fetch(auth_api + "/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ user: username, password: password, token: totpPin }),
|
||||||
|
});
|
||||||
|
if (data_req.status === 200) {
|
||||||
|
isLogged();
|
||||||
|
navigationManager("/");
|
||||||
|
} else if (data_req.status === 401) {
|
||||||
|
const data = await data_req.json();
|
||||||
|
|
||||||
|
if (!document.getElementById("error-totp")) {
|
||||||
|
const error = document.createElement("p");
|
||||||
|
error.innerHTML = data.error;
|
||||||
|
error.classList.add("text-red-700", "dark:text-red-500");
|
||||||
|
|
||||||
|
idWindow.appendChild(error);
|
||||||
|
} else {
|
||||||
|
const error = document.getElementById("error-totp") as HTMLParagraphElement;
|
||||||
|
error.innerHTML = data.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log(data_req.status)
|
||||||
|
console.log(await data_req.json())
|
||||||
|
// throw new Error("invalid response");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
const username = (document.getElementById("username") as HTMLInputElement).value;
|
||||||
|
const password = (document.getElementById("password") as HTMLInputElement).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data_req = await fetch(auth_api + "/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ user: username, password: password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data_req.status === 200)
|
||||||
|
{
|
||||||
|
isLogged();
|
||||||
|
navigationManager("/");
|
||||||
|
}
|
||||||
|
else if (data_req.status === 402) {
|
||||||
|
const popup: HTMLDivElement = document.createElement("div");
|
||||||
|
popup.id = "2fa-popup";
|
||||||
|
popup.classList.add("z-10", "absolute", "default-border");
|
||||||
|
const header = popup.appendChild(document.createElement("div"));;
|
||||||
|
header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2");
|
||||||
|
header.id = "2fa-header";
|
||||||
|
header.appendChild(document.createElement("span")).innerText = "2fa.ts";
|
||||||
|
const btn = header.appendChild(document.createElement("button"));
|
||||||
|
btn.innerText = " × ";
|
||||||
|
btn.onclick = () => { document.getElementById("2fa-popup").remove(); };
|
||||||
|
|
||||||
|
const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div"));
|
||||||
|
popup_content.id = "2fa-popup-content";
|
||||||
|
popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4");
|
||||||
|
|
||||||
|
const tokenInput = document.createElement("input");
|
||||||
|
tokenInput.type = "tel";
|
||||||
|
tokenInput.id = "totpPin";
|
||||||
|
tokenInput.name = "totpPin";
|
||||||
|
tokenInput.placeholder = "TOTP code";
|
||||||
|
tokenInput.required = true;
|
||||||
|
tokenInput.autocomplete = "off";
|
||||||
|
tokenInput.pattern = "[0-9]*";
|
||||||
|
tokenInput.setAttribute("inputmode", "numeric");
|
||||||
|
tokenInput.classList.add("bg-white", "text-neutral-900","w-full", "px-4", "py-2", "input-border");
|
||||||
|
|
||||||
|
const tokenSubmit = document.createElement("button");
|
||||||
|
tokenSubmit.type = "submit";
|
||||||
|
tokenSubmit.classList.add("default-button", "w-full");
|
||||||
|
tokenSubmit.id = "totp-submit";
|
||||||
|
tokenSubmit.innerHTML = "submit";
|
||||||
|
|
||||||
|
const tokenTitle = document.createElement("h1");
|
||||||
|
tokenTitle.innerHTML = `hey ${username}, please submit your 2fa code below :`;
|
||||||
|
tokenTitle.classList.add("text-gray-900", "dark:text-white", "text-lg", "pt-0", "pb-4", "justify-center");
|
||||||
|
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.method = "dialog";
|
||||||
|
form.classList.add("space-y-4");
|
||||||
|
form.appendChild(tokenTitle);
|
||||||
|
form.appendChild(tokenInput);
|
||||||
|
form.appendChild(tokenSubmit);
|
||||||
|
|
||||||
|
popup_content.appendChild(form);
|
||||||
|
|
||||||
|
const uu = document.getElementById("username") as HTMLInputElement;
|
||||||
|
const pass = document.getElementById("password") as HTMLInputElement;
|
||||||
|
|
||||||
|
uu.disabled = true;
|
||||||
|
pass.disabled = true;
|
||||||
|
|
||||||
|
document.getElementById("app")?.appendChild(popup);
|
||||||
|
tokenInput.focus();
|
||||||
|
dragElement(document.getElementById("2fa-popup"));
|
||||||
|
|
||||||
|
document.getElementById("totp-submit")?.addEventListener("click", totpVerify);
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Error("invalid response");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
document.getElementById("login-error-message").innerHTML = "error: server error, try again later...";
|
||||||
|
document.getElementById("login-error-message").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("login-button")?.addEventListener("click", login);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/front/static/ts/views/MainMenu.ts
Normal file
35
src/front/static/ts/views/MainMenu.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { isLogged} from "../main.ts"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("knl is trans(cendence)");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
// <div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4"-->
|
||||||
|
return `
|
||||||
|
<!--div class="default-border">
|
||||||
|
<div 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]">knl_meowscendence</span>
|
||||||
|
<div>
|
||||||
|
<button> - </button>
|
||||||
|
<button> □ </button>
|
||||||
|
<a href="/" data-link> × </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
|
||||||
|
<p class="text-gray-900 dark:text-white text-lg pb-4">i like pong</p>
|
||||||
|
<a class="default-button" href="/pong" data-link>
|
||||||
|
Pong
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div-->
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
343
src/front/static/ts/views/Pong.ts
Normal file
343
src/front/static/ts/views/Pong.ts
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { isLogged, user_api, auth_api } from "../main.js"
|
||||||
|
import { dragElement } from "./drag.js"
|
||||||
|
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
running: boolean;
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("pong (local match)");
|
||||||
|
this.running = true;
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<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-5 space-y-4 reverse-border">
|
||||||
|
<div id="player-inputs" class="flex flex-col space-y-4">
|
||||||
|
<h1 class="text-lg text-neutral-900 dark:text-white font-bold mt-2">enter the users ids/names</h1>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<span class="reverse-border w-full ml-2"><input type="text" id="player1" placeholder="Player 1" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input></span>
|
||||||
|
<span class="reverse-border w-full ml-2"><input type="text" id="player2" placeholder="Player 2" class="bg-white text-neutral-900 px-4 py-2 w-full input-border" required></input></span>
|
||||||
|
</div>
|
||||||
|
<button id="game-start" class="default-button">play</button>
|
||||||
|
</div>
|
||||||
|
<div id="game-buttons" class="hidden flex mt-4">
|
||||||
|
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
|
||||||
|
<a id="game-back" class="default-button w-full mx-4 py-2" href="/pong" data-link>back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
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_name: string;
|
||||||
|
let p2_name: string;
|
||||||
|
|
||||||
|
let p1_displayName: string;
|
||||||
|
let p2_displayName: string;
|
||||||
|
|
||||||
|
let countdown: number = 3;
|
||||||
|
let countdownTimer: number = 0;
|
||||||
|
|
||||||
|
let canvas;
|
||||||
|
let ctx;
|
||||||
|
|
||||||
|
const paddleOffset: number = 15;
|
||||||
|
const paddleHeight: number = 100;
|
||||||
|
const paddleWidth: number = 10;
|
||||||
|
const ballSize: number = 10;
|
||||||
|
|
||||||
|
const paddleSpeed: number = 727 * 0.69;
|
||||||
|
let leftPaddleY: number;
|
||||||
|
let rightPaddleY: number;
|
||||||
|
let ballX: number;
|
||||||
|
let ballY: number;
|
||||||
|
let ballSpeed: number = 200;
|
||||||
|
let ballSpeedX: number = 300;
|
||||||
|
let ballSpeedY: number = 10;
|
||||||
|
|
||||||
|
const keys: Record<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) {
|
||||||
|
const speed = ballSpeed;
|
||||||
|
const paddleCenterY = paddleY + paddleHeight / 2;
|
||||||
|
|
||||||
|
let n = (ballY - paddleCenterY) / (paddleHeight / 2);
|
||||||
|
n = Math.max(-1, Math.min(1, n));
|
||||||
|
let theta = n * ((75 * Math.PI) / 180);
|
||||||
|
ballSpeedY = ballSpeed * Math.sin(theta);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
console.log(isLogged());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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_input: HTMLInputElement = document.getElementById("player1") as HTMLInputElement;
|
||||||
|
let p2_input: HTMLInputElement = document.getElementById("player2") as HTMLInputElement;
|
||||||
|
|
||||||
|
p2_input.value = "player 2";
|
||||||
|
if (await isLogged())
|
||||||
|
{
|
||||||
|
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
|
||||||
|
p1_input.value = uuid;
|
||||||
|
p1_input.readOnly = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
p1_input.value = "player 1";
|
||||||
|
|
||||||
|
document.getElementById("game-start")?.addEventListener("click", async () => {
|
||||||
|
let p1_isvalid = true;
|
||||||
|
let p2_isvalid = true;
|
||||||
|
if (await isLogged()) {
|
||||||
|
const p1_req = await fetch(`${user_api}/users/${p1_input.value}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const p2_req = await fetch(`${user_api}/users/${p2_input.value}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (p1_req.status != 200)
|
||||||
|
p1_isvalid = false;
|
||||||
|
else
|
||||||
|
p1_displayName = (await p1_req.json()).displayName;
|
||||||
|
|
||||||
|
if (p2_req.status != 200)
|
||||||
|
p2_isvalid = false;
|
||||||
|
else
|
||||||
|
p2_displayName = (await p2_req.json()).displayName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
p1_isvalid = p2_isvalid = false;
|
||||||
|
p1_name = p1_input.value;
|
||||||
|
p2_name = p2_input.value;
|
||||||
|
if (!p1_isvalid)
|
||||||
|
p1_displayName = p1_name;
|
||||||
|
if (!p2_isvalid)
|
||||||
|
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_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");
|
||||||
|
canvas.id = "gameCanvas";
|
||||||
|
canvas.classList.add("reverse-border");
|
||||||
|
|
||||||
|
document.getElementById("main-div").prepend(canvas);
|
||||||
|
|
||||||
|
ctx = canvas.getContext("2d", {alpha: false});
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/front/static/ts/views/PongMenu.ts
Normal file
42
src/front/static/ts/views/PongMenu.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.js"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("knl is trans(cendence)");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<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 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 to pong!! Oo</p>
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<a class="default-button" href="/pong/local" data-link>
|
||||||
|
local match
|
||||||
|
</a>
|
||||||
|
<a class="default-button" href="/pong/tournament" data-link>
|
||||||
|
local tournament
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async run() {
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/front/static/ts/views/Profile.ts
Normal file
226
src/front/static/ts/views/Profile.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.ts";
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
import { isLogged, navigationManager, user_api, auth_api } from "../main.ts"
|
||||||
|
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("profile");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
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]">profile.ts</span>
|
||||||
|
<div>
|
||||||
|
<button> - </button>
|
||||||
|
<button> □ </button>
|
||||||
|
<a href="/" data-link> × </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
|
||||||
|
<div class="flex flex-col space-y-4 w-full">
|
||||||
|
<div id="profile-profile" class="default-border h-24 flex flex-row place-content-stretch content-center items-center space-x-6 pr-4">
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row space-x-4 w-full min-w-175">
|
||||||
|
<ul id="profile-pong-scorelist" class="reverse-border bg-neutral-300 dark:bg-neutral-900 h-48 w-full overflow-scroll no-scrollbar">
|
||||||
|
</ul>
|
||||||
|
<ul id="profile-tetris-scorelist" class="reverse-border bg-neutral-300 dark:bg-neutral-900 h-48 w-full overflow-scroll no-scrollbar">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
if (!await isLogged())
|
||||||
|
navigationManager("/");
|
||||||
|
|
||||||
|
let pc: number = 0;
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
let uuid: String;
|
||||||
|
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
|
||||||
|
const userdata_req = await fetch(`${user_api}/users/${uuid}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (userdata_req.status == 404) {
|
||||||
|
console.error("invalid user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let userdata = await userdata_req.json();
|
||||||
|
|
||||||
|
let matchCount_req = await fetch(`${user_api}/users/${uuid}/matchHistory/count?game=pong`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
let matchCount = await matchCount_req.json();
|
||||||
|
pc += matchCount.n_matches;
|
||||||
|
|
||||||
|
let matches_req = await fetch(`${user_api}/users/${uuid}/matchHistory?game=pong&iStart=0&iEnd=${matchCount.n_matches}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
let matches = await matches_req.json();
|
||||||
|
|
||||||
|
let main = document.getElementById("profile-pong-scorelist");
|
||||||
|
if (!main)
|
||||||
|
return console.error("what");
|
||||||
|
|
||||||
|
if (matches.matchHistory) {
|
||||||
|
for (let match of matches.matchHistory) {
|
||||||
|
const p2_req = await fetch(`${user_api}/users/${match.score.p2}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
match.score.p1 = userdata.displayName;
|
||||||
|
match.score.p2 = (await p2_req.json()).displayName;
|
||||||
|
const newEntry = document.createElement("li");
|
||||||
|
newEntry.classList.add("m-1", "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);
|
||||||
|
|
||||||
|
const popup: HTMLDivElement = document.createElement("div");
|
||||||
|
const id: number = Math.floor(Math.random() * 100000000000);
|
||||||
|
popup.id = `${id}`;
|
||||||
|
popup.classList.add("z-10", "absolute", "default-border");
|
||||||
|
const header = popup.appendChild(document.createElement("div"));
|
||||||
|
header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2");
|
||||||
|
header.id = `${id}-header`;
|
||||||
|
const title = header.appendChild(document.createElement("span"));
|
||||||
|
title.classList.add("font-[Kubasta]");
|
||||||
|
title.innerText = "score-pong.ts";
|
||||||
|
const btn = header.appendChild(document.createElement("button"));
|
||||||
|
btn.innerText = " × ";
|
||||||
|
btn.onclick = () => { document.getElementById(`${id}`).remove(); };
|
||||||
|
|
||||||
|
const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div"));
|
||||||
|
popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4");
|
||||||
|
const date = new Date(match.score.date);
|
||||||
|
popup_content.appendChild(document.createElement("span")).innerText = `${date.toDateString()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||||
|
const score = popup_content.appendChild(document.createElement("span"));
|
||||||
|
score.classList.add();
|
||||||
|
score.innerText = `${match.score.p1} : ${match.score.p1Score} - ${match.score.p2Score} : ${match.score.p2}`;
|
||||||
|
const tx = popup_content.appendChild(document.createElement("a"));
|
||||||
|
tx.href = `https://testnet.snowscan.xyz/tx/${match.tx}`;
|
||||||
|
tx.innerText = "transaction proof";
|
||||||
|
tx.target = "_blank";
|
||||||
|
tx.classList.add("default-button", "items-center", "justify-center", "text-center");
|
||||||
|
|
||||||
|
newEntry.onclick = () => { document.getElementById("app")?.appendChild(popup); dragElement(document.getElementById(`${id}`)); };
|
||||||
|
console.log(match.tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchCount_req = await fetch(`${user_api}/users/${uuid}/matchHistory/count?game=tetris`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
matchCount = await matchCount_req.json();
|
||||||
|
pc += matchCount.n_matches;
|
||||||
|
|
||||||
|
matches_req = await fetch(`${user_api}/users/${uuid}/matchHistory?game=tetris&iStart=0&iEnd=${matchCount.n_matches}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
matches = await matches_req.json();
|
||||||
|
|
||||||
|
main = document.getElementById("profile-tetris-scorelist");
|
||||||
|
if (!main)
|
||||||
|
return console.error("what");
|
||||||
|
|
||||||
|
// don't read this shit for you mental health
|
||||||
|
if (matches.matchHistory) {
|
||||||
|
for (let match of matches.matchHistory) {
|
||||||
|
if (match.score.p2 != undefined)
|
||||||
|
{
|
||||||
|
const p2_req = await fetch(`${user_api}/users/${match.score.p2}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
match.score.p2 = (await p2_req.json()).displayName;
|
||||||
|
}
|
||||||
|
match.score.p1 = userdata.displayName;
|
||||||
|
const newEntry = document.createElement("li");
|
||||||
|
newEntry.classList.add("m-1", "default-button", "bg-neutral-200", "dark:bg-neutral-800", "text-neutral-900", "dark:text-white");
|
||||||
|
newEntry.innerHTML = match.score.p2 != undefined ?
|
||||||
|
(match.score.p1Score > match.score.p2Score ? `${match.score.p1} - winner` : `${match.score.p2} - winner`)
|
||||||
|
:
|
||||||
|
(`solo game - ${match.score.p1Score}`)
|
||||||
|
;
|
||||||
|
main.insertBefore(newEntry, main.firstChild);
|
||||||
|
|
||||||
|
const popup: HTMLDivElement = document.createElement("div");
|
||||||
|
const id: number = Math.floor(Math.random() * 100000000000);
|
||||||
|
popup.id = `${id}`;
|
||||||
|
popup.classList.add("z-10", "absolute", "default-border");
|
||||||
|
const header = popup.appendChild(document.createElement("div"));
|
||||||
|
header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2");
|
||||||
|
header.id = `${id}-header`;
|
||||||
|
const title = header.appendChild(document.createElement("span"));
|
||||||
|
title.classList.add("font-[Kubasta]");
|
||||||
|
title.innerText = "score-tetris.ts";
|
||||||
|
const btn = header.appendChild(document.createElement("button"));
|
||||||
|
btn.innerText = " × ";
|
||||||
|
btn.onclick = () => { document.getElementById(`${id}`).remove(); };
|
||||||
|
|
||||||
|
const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div"));
|
||||||
|
popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4");
|
||||||
|
const date = new Date(match.score.date);
|
||||||
|
popup_content.appendChild(document.createElement("span")).innerText = `${date.toDateString()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||||
|
const score = popup_content.appendChild(document.createElement("span"));
|
||||||
|
score.classList.add();
|
||||||
|
score.innerText = match.score.p2 != undefined ?
|
||||||
|
(`${match.score.p1} : ${match.score.p1Score} - ${match.score.p2Score} : ${match.score.p2}`)
|
||||||
|
:
|
||||||
|
(`${match.score.p1} : ${match.score.p1Score}`)
|
||||||
|
;
|
||||||
|
const tx = popup_content.appendChild(document.createElement("a"));
|
||||||
|
tx.href = `https://testnet.snowscan.xyz/tx/${match.tx}`;
|
||||||
|
tx.innerText = "transaction proof";
|
||||||
|
tx.target = "_blank";
|
||||||
|
tx.classList.add("default-button", "items-center", "justify-center", "text-center");
|
||||||
|
|
||||||
|
newEntry.onclick = () => { document.getElementById("app")?.appendChild(popup); dragElement(document.getElementById(`${id}`)); };
|
||||||
|
console.log(match.tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = document.getElementById("profile-profile");
|
||||||
|
if (!profile) return;
|
||||||
|
|
||||||
|
const picture = profile.appendChild(document.createElement("img"));
|
||||||
|
const a = await fetch(`${user_api}/users/${uuid}/avatar`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
picture.src = a.status === 200
|
||||||
|
? `${user_api}/users/${uuid}/avatar?t=${Date.now()}`
|
||||||
|
: "https://api.kanel.ovh/pp";
|
||||||
|
picture.classList.add("text-neutral-900", "dark:text-white", "center", "h-18", "w-18", "mx-3", "reverse-border");
|
||||||
|
|
||||||
|
const nametag = profile.appendChild(document.createElement("div"));
|
||||||
|
nametag.innerHTML = `
|
||||||
|
<div class="text-lg">Hi ${userdata.displayName} ! :D</div>
|
||||||
|
<div class="italic">${uuid}<div>
|
||||||
|
`;
|
||||||
|
nametag.classList.add("text-neutral-900", "dark:text-white");
|
||||||
|
|
||||||
|
const winrate = profile.appendChild(document.createElement("div"));
|
||||||
|
winrate.innerHTML = `
|
||||||
|
<div> total playcount: ${pc} </div>
|
||||||
|
<div> pong winrate: ${ (userdata.pong.wins == 0 && userdata.pong.losses == 0) ? "-" : Math.round(userdata.pong.wins / (userdata.pong.wins + userdata.pong.losses) * 100) + " %" } </div>
|
||||||
|
<div> tetris winrate: ${ (userdata.tetris.wins == 0 && userdata.tetris.losses == 0) ? "-" : Math.round(userdata.tetris.wins / (userdata.tetris.wins + userdata.tetris.losses) * 100) + " %" } </div>
|
||||||
|
`;
|
||||||
|
winrate.classList.add("text-neutral-900", "dark:text-white", "grow", "content-center");
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/front/static/ts/views/ProfileMenu.ts
Normal file
87
src/front/static/ts/views/ProfileMenu.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { isLogged, user_api, auth_api } from "../main.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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() {
|
||||||
|
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];
|
||||||
|
const userdata_req = await fetch(`${user_api}/users/${uuid}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (userdata_req.status == 404)
|
||||||
|
{
|
||||||
|
console.error("invalid user");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
let userdata = await userdata_req.json();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<span class="menu-default-label inline-flex items-center justify-center">hi, ${ userdata.displayName.length > 8 ? userdata.displayName.substring(0, 8) + "." : userdata.displayName } !</span>
|
||||||
|
<hr class="my-2 w-32 reverse-border">
|
||||||
|
<a class="menu-default-button inline-flex items-center justify-center" href="/profile" data-link>profile</a>
|
||||||
|
<a class="menu-default-button inline-flex items-center justify-center" href="/settings" data-link>settings</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("profile-items").innerHTML = await getMainHTML();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.getElementById("menu-logout").addEventListener("click", async () => {
|
||||||
|
let req = fetch(`${auth_api}/logout`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
req.then((res) => {
|
||||||
|
isLogged();
|
||||||
|
if (res.status === 200)
|
||||||
|
this.run();
|
||||||
|
else
|
||||||
|
console.error("logout failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/front/static/ts/views/RegisterPage.ts
Normal file
104
src/front/static/ts/views/RegisterPage.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.ts";
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
import { isLogged, navigationManager, user_api, auth_api } from "../main.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.setTitle("register");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
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]">register.ts</span>
|
||||||
|
<div>
|
||||||
|
<button> - </button>
|
||||||
|
<button> □ </button>
|
||||||
|
<a href="/" data-link> × </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 reverse-border flex flex-col items-center">
|
||||||
|
<form method="dialog" class="space-y-4">
|
||||||
|
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome ! please register.</h1>
|
||||||
|
<div class="flex flex-row justify-between space-x-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<button id="register-button" type="submit" class="default-button w-full">register</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500 pt-4"></p>
|
||||||
|
|
||||||
|
<hr class="my-4 w-64 reverse-border">
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-4 w-full">
|
||||||
|
<a target="_blank" id="register-google" class="default-button inline-flex items-center justify-center w-full">
|
||||||
|
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" height=20 width=20 class="mr-2 justify-self-start" />
|
||||||
|
register with google
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href="https://rusty.42angouleme.fr/issues/all" class="default-button inline-flex items-center justify-center w-full">
|
||||||
|
<img src="https://rusty.42angouleme.fr/assets/favicon-bb06adc80c8495db.ico" height=20 width=20 class="mr-2 justify-self-start" />
|
||||||
|
register with rusty
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
document.getElementById("register-google").href = `${auth_api}/register/google`;
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
const login = async () => {
|
||||||
|
const username = (document.getElementById("username") as HTMLInputElement).value;
|
||||||
|
const password = (document.getElementById("password") as HTMLInputElement).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data_req = await fetch(auth_api + "/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ user: username, password: password }),
|
||||||
|
});
|
||||||
|
const data = await data_req.json();
|
||||||
|
|
||||||
|
if (data_req.status === 200) {
|
||||||
|
let uuid_req = await fetch(auth_api + "/me", {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
let uuid = await uuid_req.json();
|
||||||
|
document.cookie = `uuid=${uuid.user};max-ages=${60 * 60 * 24 * 7}`;
|
||||||
|
isLogged();
|
||||||
|
navigationManager("/");
|
||||||
|
}
|
||||||
|
else if (data_req.status === 400) {
|
||||||
|
if (document.getElementById("login-error-message")) {
|
||||||
|
document.getElementById("login-error-message").innerHTML = "error: " + data.error;
|
||||||
|
document.getElementById("login-error-message")?.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("invalid response");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (document.getElementById("login-error-message")) {
|
||||||
|
document.getElementById("login-error-message").innerHTML = "error: server error, try again later...";
|
||||||
|
document.getElementById("login-error-message")?.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("register-button")?.addEventListener("click", login);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/front/static/ts/views/Settings.ts
Normal file
181
src/front/static/ts/views/Settings.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.ts";
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
import { totpEnablePopup } from "./TotpEnable.ts";
|
||||||
|
import { totpVerify } from "../../../../api/auth/totpVerify.js";
|
||||||
|
import { isLogged, navigationManager, user_api, auth_api } from "../main.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.setTitle("profile");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
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]">settings.ts</span>
|
||||||
|
<div>
|
||||||
|
<button> - </button>
|
||||||
|
<button> □ </button>
|
||||||
|
<a href="/" data-link> × </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
|
||||||
|
<div class="flex flex-row items-center place-items-center space-x-4">
|
||||||
|
<input type="text" id="displayName-input" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
|
||||||
|
<button id="displayName-button" type="submit" class="default-button w-full">change display name</button>
|
||||||
|
</div>
|
||||||
|
<div id="upload" class="flex flex-row items-center place-items-center space-x-8">
|
||||||
|
<div id="upload-preview" class="hidden flex flex-col items-center place-items-center space-y-4">
|
||||||
|
<img id="upload-preview-img" class="w-20 h-20" />
|
||||||
|
<button id="upload-submit" type="submit" class="default-button">change avatar</button>
|
||||||
|
</div>
|
||||||
|
<label for="upload-file" class="default-button">select an avatar...</label><input type="file" id="upload-file" class="hidden" accept="image/*" />
|
||||||
|
</div>
|
||||||
|
<button id="deleteAccount-button" type="submit" class="default-button w-full">delete your account</button>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<hr class="w-50 reverse-border">
|
||||||
|
</div>
|
||||||
|
<button id="2fa-button" type="submit" class="default-button w-full">2fa</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
if (!await isLogged())
|
||||||
|
navigationManager("/");
|
||||||
|
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
|
||||||
|
const isTOTPEnabled = async () => {
|
||||||
|
const totpVerify_req = await fetch(auth_api + '/2fa', {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
|
||||||
|
if (totpVerify_req.status === 200) {
|
||||||
|
const totpVerify_data = await totpVerify_req.json();
|
||||||
|
if (totpVerify_data.totp == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let uuid: String;
|
||||||
|
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
const userdata_req = await fetch(`${user_api}/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(`${user_api}/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(auth_api + "/", {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (delete_req.status == 200)
|
||||||
|
navigationManager("/");
|
||||||
|
else
|
||||||
|
console.error("xd"); // xd?????????????
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = document.getElementById("upload-file") as HTMLInputElement;
|
||||||
|
upload.addEventListener("change", () => {
|
||||||
|
const fileList: FileList | null = upload.files;
|
||||||
|
if (!fileList)
|
||||||
|
return console.error("empty");
|
||||||
|
if (!fileList[0].type.startsWith("image/")) {
|
||||||
|
console.error("invalid file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("upload-preview")?.classList.remove("hidden");
|
||||||
|
const img = document.getElementById("upload-preview-img") as HTMLImageElement;
|
||||||
|
img.classList.remove("hidden");
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (!e.target)
|
||||||
|
return;
|
||||||
|
img.src = e.target.result as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(fileList[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
(document.getElementById("upload-submit") as HTMLButtonElement).onclick = async () => {
|
||||||
|
const up_req = await fetch(`${user_api}/users/${uuid}/avatar`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": upload.files[0].type } ,
|
||||||
|
credentials: "include",
|
||||||
|
body: upload.files[0], //upload uuuh whatever i have to upload
|
||||||
|
});
|
||||||
|
console.log(up_req.status);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totpButton = document.getElementById("2fa-button") as HTMLButtonElement;
|
||||||
|
|
||||||
|
if ((await isTOTPEnabled()) === true) {
|
||||||
|
totpButton.innerHTML = "disable 2fa";
|
||||||
|
|
||||||
|
document.getElementById("2fa-button")?.addEventListener("click", async () => {
|
||||||
|
const totp_req = await fetch(`${user_api}/2fa`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
if (totp_req.status === 200) {
|
||||||
|
console.log("working")
|
||||||
|
navigationManager("/settings")
|
||||||
|
} else {
|
||||||
|
console.log("wut")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
totpButton.innerHTML = "enable 2fa";
|
||||||
|
|
||||||
|
document.getElementById("2fa-button")?.addEventListener("click", async () => {
|
||||||
|
const totp_req = await fetch(`${user_api}/2fa`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
if (totp_req.status === 200) {
|
||||||
|
console.log("working")
|
||||||
|
const totp_data = await totp_req.json();
|
||||||
|
totpEnablePopup(uuid, totp_data.secret, totp_data.otpauthUrl);
|
||||||
|
} else {
|
||||||
|
console.log("wut")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
885
src/front/static/ts/views/Tetris.ts
Normal file
885
src/front/static/ts/views/Tetris.ts
Normal file
@ -0,0 +1,885 @@
|
|||||||
|
import Aview from "./Aview.ts";
|
||||||
|
import { dragElement } from "./drag.js";
|
||||||
|
import { setOnekoState, setBallPos, setOnekoOffset, setSleepPos } from "../oneko.ts";
|
||||||
|
import { isLogged , user_api, auth_api } from "../main.js";
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
running: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.setTitle("tetris (local match)");
|
||||||
|
setOnekoState("tetris");
|
||||||
|
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]">tetris_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 space-y-4 reverse-border">
|
||||||
|
<div class="flex flex-row justify-center items-start space-x-4">
|
||||||
|
<canvas id="hold" class="reverse-border" width="140" height="100"></canvas>
|
||||||
|
<canvas id="board" class="reverse-border" width="300" height="600"></canvas>
|
||||||
|
<canvas id="queue" class="reverse-border" width="140" height="420"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="game-buttons" class="hidden flex mt-4">
|
||||||
|
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
|
||||||
|
<a id="game-back" class="default-button w-full mx-4 py-2" href="/tetris" data-link>back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
setSleepPos();
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
const COLS = 10;
|
||||||
|
const ROWS = 20;
|
||||||
|
const BLOCK = 30; // pixels per block
|
||||||
|
|
||||||
|
const view = this;
|
||||||
|
|
||||||
|
type Cell = number; // 0 empty, >0 occupied (color index)
|
||||||
|
|
||||||
|
// Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix
|
||||||
|
const TETROMINOES: { [key: string]: number[][][] } = {
|
||||||
|
I: [
|
||||||
|
[
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
J: [
|
||||||
|
[
|
||||||
|
[2, 0, 0],
|
||||||
|
[2, 2, 2],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 2, 2],
|
||||||
|
[0, 2, 0],
|
||||||
|
[0, 2, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[2, 2, 2],
|
||||||
|
[0, 0, 2],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 2, 0],
|
||||||
|
[0, 2, 0],
|
||||||
|
[2, 2, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
L: [
|
||||||
|
[
|
||||||
|
[0, 0, 3],
|
||||||
|
[3, 3, 3],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 3, 0],
|
||||||
|
[0, 3, 0],
|
||||||
|
[0, 3, 3],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[3, 3, 3],
|
||||||
|
[3, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[3, 3, 0],
|
||||||
|
[0, 3, 0],
|
||||||
|
[0, 3, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
O: [
|
||||||
|
[
|
||||||
|
[4, 4],
|
||||||
|
[4, 4],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[
|
||||||
|
[0, 5, 5],
|
||||||
|
[5, 5, 0],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 5, 0],
|
||||||
|
[0, 5, 5],
|
||||||
|
[0, 0, 5],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 5, 5],
|
||||||
|
[5, 5, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[5, 0, 0],
|
||||||
|
[5, 5, 0],
|
||||||
|
[0, 5, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
T: [
|
||||||
|
[
|
||||||
|
[0, 6, 0],
|
||||||
|
[6, 6, 6],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 6, 0],
|
||||||
|
[0, 6, 6],
|
||||||
|
[0, 6, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[6, 6, 6],
|
||||||
|
[0, 6, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 6, 0],
|
||||||
|
[6, 6, 0],
|
||||||
|
[0, 6, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Z: [
|
||||||
|
[
|
||||||
|
[7, 7, 0],
|
||||||
|
[0, 7, 7],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 7],
|
||||||
|
[0, 7, 7],
|
||||||
|
[0, 7, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[7, 7, 0],
|
||||||
|
[0, 7, 7],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 7, 0],
|
||||||
|
[7, 7, 0],
|
||||||
|
[7, 0, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
[ "#000000", "#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 {
|
||||||
|
shape: number[][];
|
||||||
|
rotations: number[][][];
|
||||||
|
rotationIndex: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
colorIndex: number;
|
||||||
|
|
||||||
|
constructor(public type: string) {
|
||||||
|
this.rotations = TETROMINOES[type];
|
||||||
|
this.rotationIndex = 0;
|
||||||
|
this.shape = this.rotations[this.rotationIndex];
|
||||||
|
this.colorIndex = this.findColorIndex();
|
||||||
|
|
||||||
|
this.x = Math.floor((COLS - this.shape[0].length) / 2);
|
||||||
|
this.y = -2; //start on tiles 21 and 22
|
||||||
|
}
|
||||||
|
|
||||||
|
findColorIndex() {
|
||||||
|
for (const row of this.shape)
|
||||||
|
for (const v of row)
|
||||||
|
if (v)
|
||||||
|
return v;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCW() {
|
||||||
|
this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length;
|
||||||
|
this.shape = this.rotations[this.rotationIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCCW() {
|
||||||
|
this.rotationIndex =
|
||||||
|
(this.rotationIndex - 1 + this.rotations.length) %
|
||||||
|
this.rotations.length;
|
||||||
|
this.shape = this.rotations[this.rotationIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
getCells(): { x: number; y: number; val: number }[] {
|
||||||
|
const cells: { x: number; y: number; val: number }[] = [];
|
||||||
|
|
||||||
|
for (let r = 0; r < this.shape.length; r++) {
|
||||||
|
for (let c = 0; c < this.shape[r].length; c++) {
|
||||||
|
const val = this.shape[r][c];
|
||||||
|
if (val) cells.push({ x: this.x + c, y: this.y + r, val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Game {
|
||||||
|
board: Cell[][];
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
holdCanvas: HTMLCanvasElement | null;
|
||||||
|
queueCanvas: HTMLCanvasElement | null;
|
||||||
|
ctx: CanvasRenderingContext2D | null;
|
||||||
|
holdCtx: CanvasRenderingContext2D | null;
|
||||||
|
queueCtx: CanvasRenderingContext2D | null;
|
||||||
|
piece: Piece | null = null;
|
||||||
|
holdPiece: Piece | null = null;
|
||||||
|
canHold: boolean = true;
|
||||||
|
nextQueue: string[] = [];
|
||||||
|
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") 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");
|
||||||
|
if (!this.holdCtx || !this.queueCtx)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.holdCtx.clearRect(0, 0, 200, 200);
|
||||||
|
this.queueCtx.clearRect(0, 0, 500, 500);
|
||||||
|
|
||||||
|
this.board = this.createEmptyBoard();
|
||||||
|
this.fillBag();
|
||||||
|
|
||||||
|
this.spawnPiece();
|
||||||
|
this.registerListeners();
|
||||||
|
requestAnimationFrame(this.loop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmptyBoard(): Cell[][] {
|
||||||
|
const b: Cell[][] = [];
|
||||||
|
for (let r = 0; r < ROWS; r++) {
|
||||||
|
const row: Cell[] = new Array(COLS).fill(0);
|
||||||
|
b.push(row);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillBag() {
|
||||||
|
// classic 7-bag randomizer
|
||||||
|
const pieces = Object.keys(TETROMINOES);
|
||||||
|
const bag = [...pieces];
|
||||||
|
for (let i = bag.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[bag[i], bag[j]] = [bag[j], bag[i]];
|
||||||
|
}
|
||||||
|
this.nextQueue.push(...bag);
|
||||||
|
}
|
||||||
|
|
||||||
|
hold() {
|
||||||
|
if (!this.canHold) return;
|
||||||
|
|
||||||
|
[this.piece, this.holdPiece] = [this.holdPiece, this.piece];
|
||||||
|
if (!this.piece) this.spawnPiece();
|
||||||
|
if (!this.piece) return;
|
||||||
|
|
||||||
|
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
|
||||||
|
this.piece.y = -2;
|
||||||
|
this.piece.rotationIndex = 0;
|
||||||
|
this.piece.shape = this.piece.rotations[this.piece.rotationIndex];
|
||||||
|
|
||||||
|
this.canHold = false;
|
||||||
|
this.drawHold();
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnPiece() {
|
||||||
|
this.canHold = true;
|
||||||
|
if (this.nextQueue.length < 7) this.fillBag();
|
||||||
|
const type = this.nextQueue.shift()!;
|
||||||
|
this.piece = new Piece(type);
|
||||||
|
if (this.collides(this.piece)) {
|
||||||
|
this.isGameOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawHold();
|
||||||
|
this.drawQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
collides(piece: Piece): boolean {
|
||||||
|
for (const cell of piece.getCells()) {
|
||||||
|
if (cell.y >= ROWS) return true;
|
||||||
|
if (cell.x < 0 || cell.x >= COLS) return true;
|
||||||
|
if (cell.y >= 0 && this.board[cell.y][cell.x]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGhostOffset(piece: Piece): number {
|
||||||
|
let y: number = 0;
|
||||||
|
while (true) {
|
||||||
|
for (const cell of piece.getCells()) {
|
||||||
|
if (
|
||||||
|
cell.y + y >= ROWS ||
|
||||||
|
(cell.y + y >= 0 && this.board[cell.y + y][cell.x])
|
||||||
|
)
|
||||||
|
return y - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lockPiece() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
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 > 0) isValid = true;
|
||||||
|
}
|
||||||
|
if (!isValid) this.isGameOver = true;
|
||||||
|
|
||||||
|
this.clearLines();
|
||||||
|
this.spawnPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLines() {
|
||||||
|
let linesCleared = 0;
|
||||||
|
outer: for (let r = ROWS - 1; r >= 0; r--) {
|
||||||
|
for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer;
|
||||||
|
|
||||||
|
this.board.splice(r, 1);
|
||||||
|
this.board.unshift(new Array(COLS).fill(0));
|
||||||
|
linesCleared++;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linesCleared > 0) {
|
||||||
|
this.lines += linesCleared;
|
||||||
|
const points = [0, 40, 100, 300, 1200];
|
||||||
|
this.score += (points[linesCleared] || 0) * this.level;
|
||||||
|
// level up every 10 lines (Fixed Goal System)
|
||||||
|
const newLevel = Math.floor(this.lines / 10) + 1;
|
||||||
|
if (newLevel > this.level) {
|
||||||
|
this.level = newLevel;
|
||||||
|
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rotatePiece(dir: "cw" | "ccw") {
|
||||||
|
if (!this.piece) return;
|
||||||
|
if (this.isLocking && this.lockRotationCount < 15)
|
||||||
|
this.lockRotationCount++;
|
||||||
|
// Try wall kicks
|
||||||
|
const originalIndex = this.piece.rotationIndex;
|
||||||
|
if (dir === "cw") this.piece.rotateCW();
|
||||||
|
else this.piece.rotateCCW();
|
||||||
|
const kicks = [0, -1, 1, -2, 2];
|
||||||
|
for (const k of kicks) {
|
||||||
|
this.piece.x += k;
|
||||||
|
if (!this.collides(this.piece)) return;
|
||||||
|
this.piece.x -= k;
|
||||||
|
}
|
||||||
|
// no kick, revert
|
||||||
|
this.piece.rotationIndex = originalIndex;
|
||||||
|
this.piece.shape = this.piece.rotations[originalIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
movePiece(dx: number, dy: number) {
|
||||||
|
if (!this.piece) return;
|
||||||
|
this.piece.x += dx;
|
||||||
|
this.piece.y += dy;
|
||||||
|
|
||||||
|
if (this.collides(this.piece)) {
|
||||||
|
this.piece.x -= dx;
|
||||||
|
this.piece.y -= dy;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hardDrop() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
let dropped = 0;
|
||||||
|
while (this.movePiece(0, 1)) dropped++;
|
||||||
|
this.score += dropped * 2;
|
||||||
|
this.lockPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
softDrop() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
if (!this.movePiece(0, 1)) return;
|
||||||
|
else this.score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys: Record<string, boolean> = {};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
window.removeEventListener("keydown", (e) => {
|
||||||
|
this.keys[e.key] = true;
|
||||||
|
|
||||||
|
if (this.isGameOver) return;
|
||||||
|
|
||||||
|
if (e.key === "p" || e.key === "P" || e.key === "Escape")
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
|
||||||
|
if (this.isPaused) return;
|
||||||
|
|
||||||
|
if (e.key === "ArrowLeft")
|
||||||
|
{
|
||||||
|
this.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();
|
||||||
|
this.hardDrop();
|
||||||
|
} else if (e.key === "Shift" || e.key === "c" || e.key === "C") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.hold();
|
||||||
|
} else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.rotatePiece("cw");
|
||||||
|
} else if (e.key === "z" || e.key === "Z" || e.key === "Control") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.rotatePiece("ccw");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.removeEventListener("keyup", (e) => {
|
||||||
|
this.keys[e.key] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerListeners() {
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
this.keys[e.key] = true;
|
||||||
|
|
||||||
|
if (this.isGameOver) return;
|
||||||
|
|
||||||
|
if (e.key === "p" || e.key === "P" || e.key === "Escape")
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
|
||||||
|
if (this.isPaused) return;
|
||||||
|
|
||||||
|
if (e.key === "ArrowLeft")
|
||||||
|
{
|
||||||
|
this.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();
|
||||||
|
this.hardDrop();
|
||||||
|
} else if (e.key === "Shift" || e.key === "c" || e.key === "C") {
|
||||||
|
this.hold();
|
||||||
|
} else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") {
|
||||||
|
//e.preventDefault();
|
||||||
|
this.rotatePiece("cw");
|
||||||
|
} else if (e.key === "z" || e.key === "Z" || e.key === "Control") {
|
||||||
|
this.rotatePiece("ccw");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
|
this.keys[e.key] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loop(timestamp: number) {
|
||||||
|
if (!view.running) return this.removeListeners();
|
||||||
|
if (!this.lastDrop) this.lastDrop = timestamp;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (await isLogged())
|
||||||
|
{
|
||||||
|
let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
fetch(`${user_api}/users/${uuid}/matchHistory?game=tetris`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"game": "tetris",
|
||||||
|
"myScore": this.score,
|
||||||
|
"date": Date.now(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById("game-buttons")?.classList.remove("hidden");
|
||||||
|
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 = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(14.5% 0 0)" : "oklch(55.6% 0 0)";
|
||||||
|
for (let r = 0; r <= ROWS; r++) {
|
||||||
|
// horizontal lines
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, r * BLOCK);
|
||||||
|
ctx.lineTo(COLS * BLOCK, r * BLOCK);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let c = 0; c <= COLS; c++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(c * BLOCK, 0);
|
||||||
|
ctx.lineTo(c * BLOCK, ROWS * BLOCK);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBoard() {
|
||||||
|
this.drawGrid();
|
||||||
|
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], this.ctx);
|
||||||
|
else this.clearBlock(c, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPiece() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
|
||||||
|
for (const cell of this.piece.getCells())
|
||||||
|
if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val], this.ctx);
|
||||||
|
|
||||||
|
let offset: number = this.getGhostOffset(this.piece);
|
||||||
|
for (const cell of this.piece.getCells())
|
||||||
|
if (cell.y + offset >= 0 && offset > 0)
|
||||||
|
this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHold() {
|
||||||
|
if (!this.holdCtx || !this.holdCanvas) return;
|
||||||
|
|
||||||
|
this.holdCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
|
||||||
|
this.holdCtx.fillRect(0, 0, this.holdCanvas.width, this.holdCanvas.height);
|
||||||
|
|
||||||
|
if (!this.holdPiece) return;
|
||||||
|
let y: number = 0;
|
||||||
|
for (const row of this.holdPiece.rotations[0]) {
|
||||||
|
let x: number = 0;
|
||||||
|
for (const val of row) {
|
||||||
|
if (val)
|
||||||
|
this.fillBlock(x + (4 - this.holdPiece.rotations[0].length)/ 2 + 0.35, y + 0.65, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : ["#8c8c84", "#393934"], this.holdCtx);
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawQueue() {
|
||||||
|
if (!this.queueCtx || !this.queueCanvas) return ;
|
||||||
|
|
||||||
|
this.queueCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
|
||||||
|
this.queueCtx.fillRect(0, 0, this.queueCanvas.width, this.queueCanvas.height);
|
||||||
|
let placement: number = 0;
|
||||||
|
for (const nextPiece of this.nextQueue.slice(0, 5)) {
|
||||||
|
let y: number = 0;
|
||||||
|
for (const row of TETROMINOES[nextPiece][0]) {
|
||||||
|
let x: number = 0;
|
||||||
|
for (const val of row) {
|
||||||
|
if (val)
|
||||||
|
this.fillBlock(x + (4 - TETROMINOES[nextPiece][0].length) / 2 + 0.25, y + 0.5 + placement * 2.69 - (nextPiece ==="I" ? 0.35 : 0), COLORS[["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1], this.queueCtx);
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
placement++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillBlock(x: number, y: number, color: string[], ctx: CanvasRenderingContext2D | null) {
|
||||||
|
if (!ctx) return;
|
||||||
|
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(Math.round(x * BLOCK) + 4, Math.round(y * BLOCK) + 4, BLOCK - 4, BLOCK - 4);
|
||||||
|
const X = Math.round(x * BLOCK);
|
||||||
|
const Y = Math.round(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;
|
||||||
|
|
||||||
|
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.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
|
||||||
|
ctx.fillRect(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);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "12px Kubasta";
|
||||||
|
ctx.fillText(`score: ${this.score}`, 8, 20);
|
||||||
|
ctx.fillText(`lines: ${this.lines}`, 8, 36);
|
||||||
|
ctx.fillText(`level: ${this.level}`, 8, 52);
|
||||||
|
|
||||||
|
if (this.isPaused) {
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
|
ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "24px Kubasta";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(
|
||||||
|
"paused",
|
||||||
|
this.canvas.width / 2,
|
||||||
|
this.canvas.height / 2 + 8,
|
||||||
|
);
|
||||||
|
ctx.textAlign = "start";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isGameOver) {
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
|
ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "28px Kubasta";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(
|
||||||
|
"game over",
|
||||||
|
this.canvas.width / 2,
|
||||||
|
this.canvas.height / 2 + 8,
|
||||||
|
);
|
||||||
|
ctx.textAlign = "start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
if (!this.ctx || !this.canvas) return;
|
||||||
|
// clear everything
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#000";
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "#111";
|
||||||
|
for (let r = 0; r <= ROWS; r++) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(0, r * BLOCK);
|
||||||
|
this.ctx.lineTo(COLS * BLOCK, r * BLOCK);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let c = 0; c <= COLS; c++) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(c * BLOCK, 0);
|
||||||
|
this.ctx.lineTo(c * BLOCK, ROWS * BLOCK);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawBoard();
|
||||||
|
this.drawPiece();
|
||||||
|
this.drawHUD();
|
||||||
|
this.drawQueue();
|
||||||
|
setSleepPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("game-retry")?.addEventListener("click", () => { document.getElementById("game-buttons")?.classList.add("hidden"); const game = new Game("board"); });
|
||||||
|
const game = new Game("board");
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/front/static/ts/views/TetrisMenu.ts
Normal file
42
src/front/static/ts/views/TetrisMenu.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.js"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("knl is trans(cendence)");
|
||||||
|
setOnekoState("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHTML() {
|
||||||
|
return `
|
||||||
|
<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]">tetris_game.ts</span>
|
||||||
|
<div>
|
||||||
|
<button> - </button>
|
||||||
|
<button> □ </button>
|
||||||
|
<a href="/" data-link> × </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div 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 to tetris! :D</p>
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<a class="default-button" href="/tetris/solo" data-link>
|
||||||
|
solo game
|
||||||
|
</a>
|
||||||
|
<a class="default-button" href="/tetris/versus" data-link>
|
||||||
|
versus game
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async run() {
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
}
|
||||||
|
}
|
||||||
987
src/front/static/ts/views/TetrisVersus.ts
Normal file
987
src/front/static/ts/views/TetrisVersus.ts
Normal file
@ -0,0 +1,987 @@
|
|||||||
|
import Aview from "./Aview.ts";
|
||||||
|
import { isLogged, user_api, auth_api } from "../main.js";
|
||||||
|
import { dragElement } from "./drag.js";
|
||||||
|
import { setOnekoState, setBallPos, setOnekoOffset, setSleepPos } from "../oneko.ts";
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
running: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.setTitle("tetris (local match)");
|
||||||
|
setOnekoState("tetris");
|
||||||
|
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]">tetris_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-5 pt-2 space-y-4 reverse-border">
|
||||||
|
<div id="player-inputs" class="flex flex-col space-y-4">
|
||||||
|
<h1 class="text-lg text-neutral-900 dark:text-white font-bold mt-2">enter the users ids/names</h1>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<span class="reverse-border w-full ml-2"><input type="text" id="player1" placeholder="Player 1" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input></span>
|
||||||
|
<span class="reverse-border w-full ml-2"><input type="text" id="player2" placeholder="Player 2" class="bg-white text-neutral-900 px-4 py-2 w-full input-border" required></input></span>
|
||||||
|
</div>
|
||||||
|
<button id="game-start" class="default-button">play</button>
|
||||||
|
</div>
|
||||||
|
<div id="game-boards" class="hidden flex flex-row justify-center items-start space-x-4">
|
||||||
|
<canvas id="board1-hold" class="reverse-border" width="140" height="100"></canvas>
|
||||||
|
<canvas id="board1-board" class="reverse-border" width="300" height="600"></canvas>
|
||||||
|
<canvas id="board1-queue" class="reverse-border" width="140" height="420"></canvas>
|
||||||
|
<canvas id="board2-hold" class="reverse-border" width="140" height="100"></canvas>
|
||||||
|
<canvas id="board2-board" class="reverse-border" width="300" height="600"></canvas>
|
||||||
|
<canvas id="board2-queue" class="reverse-border" width="140" height="420"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="game-buttons" class="hidden flex mt-4">
|
||||||
|
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
|
||||||
|
<a id="game-back" class="default-button w-full mx-4 py-2" href="/tetris" data-link>back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
setSleepPos();
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
const COLS = 10;
|
||||||
|
const ROWS = 20;
|
||||||
|
const BLOCK = 30; // pixels per block
|
||||||
|
|
||||||
|
let uuid: string;
|
||||||
|
let game1: Game;
|
||||||
|
let game2: Game;
|
||||||
|
let p1_score: number = 0;
|
||||||
|
let p2_score: number = 0;
|
||||||
|
let p1_name: string;
|
||||||
|
let p2_name: string;
|
||||||
|
let p1_displayName: string;
|
||||||
|
let p2_displayName: string;
|
||||||
|
|
||||||
|
const view = this;
|
||||||
|
|
||||||
|
type Cell = number;
|
||||||
|
|
||||||
|
// Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix
|
||||||
|
const TETROMINOES: { [key: string]: number[][][] } = {
|
||||||
|
I: [
|
||||||
|
[
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
J: [
|
||||||
|
[
|
||||||
|
[2, 0, 0],
|
||||||
|
[2, 2, 2],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 2, 2],
|
||||||
|
[0, 2, 0],
|
||||||
|
[0, 2, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[2, 2, 2],
|
||||||
|
[0, 0, 2],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 2, 0],
|
||||||
|
[0, 2, 0],
|
||||||
|
[2, 2, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
L: [
|
||||||
|
[
|
||||||
|
[0, 0, 3],
|
||||||
|
[3, 3, 3],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 3, 0],
|
||||||
|
[0, 3, 0],
|
||||||
|
[0, 3, 3],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[3, 3, 3],
|
||||||
|
[3, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[3, 3, 0],
|
||||||
|
[0, 3, 0],
|
||||||
|
[0, 3, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
O: [
|
||||||
|
[
|
||||||
|
[4, 4],
|
||||||
|
[4, 4],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[
|
||||||
|
[0, 5, 5],
|
||||||
|
[5, 5, 0],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 5, 0],
|
||||||
|
[0, 5, 5],
|
||||||
|
[0, 0, 5],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 5, 5],
|
||||||
|
[5, 5, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[5, 0, 0],
|
||||||
|
[5, 5, 0],
|
||||||
|
[0, 5, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
T: [
|
||||||
|
[
|
||||||
|
[0, 6, 0],
|
||||||
|
[6, 6, 6],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 6, 0],
|
||||||
|
[0, 6, 6],
|
||||||
|
[0, 6, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[6, 6, 6],
|
||||||
|
[0, 6, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 6, 0],
|
||||||
|
[6, 6, 0],
|
||||||
|
[0, 6, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Z: [
|
||||||
|
[
|
||||||
|
[7, 7, 0],
|
||||||
|
[0, 7, 7],
|
||||||
|
[0, 0, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 7],
|
||||||
|
[0, 7, 7],
|
||||||
|
[0, 7, 0],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 0, 0],
|
||||||
|
[7, 7, 0],
|
||||||
|
[0, 7, 7],
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
[0, 7, 0],
|
||||||
|
[7, 7, 0],
|
||||||
|
[7, 0, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
[ "#000000", "#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
|
||||||
|
[ "#8c8c84", "#393934" ], // garbage - gray
|
||||||
|
];
|
||||||
|
|
||||||
|
class Piece {
|
||||||
|
shape: number[][];
|
||||||
|
rotations: number[][][];
|
||||||
|
rotationIndex: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
colorIndex: number;
|
||||||
|
|
||||||
|
constructor(public type: string) {
|
||||||
|
this.rotations = TETROMINOES[type];
|
||||||
|
this.rotationIndex = 0;
|
||||||
|
this.shape = this.rotations[this.rotationIndex];
|
||||||
|
this.colorIndex = this.findColorIndex();
|
||||||
|
|
||||||
|
this.x = Math.floor((COLS - this.shape[0].length) / 2);
|
||||||
|
this.y = -2; //start on tiles 21 and 22
|
||||||
|
}
|
||||||
|
|
||||||
|
findColorIndex() {
|
||||||
|
for (const row of this.shape)
|
||||||
|
for (const v of row)
|
||||||
|
if (v)
|
||||||
|
return v;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCW() {
|
||||||
|
this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length;
|
||||||
|
this.shape = this.rotations[this.rotationIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCCW() {
|
||||||
|
this.rotationIndex =
|
||||||
|
(this.rotationIndex - 1 + this.rotations.length) %
|
||||||
|
this.rotations.length;
|
||||||
|
this.shape = this.rotations[this.rotationIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
getCells(): { x: number; y: number; val: number }[] {
|
||||||
|
const cells: { x: number; y: number; val: number }[] = [];
|
||||||
|
|
||||||
|
for (let r = 0; r < this.shape.length; r++) {
|
||||||
|
for (let c = 0; c < this.shape[r].length; c++) {
|
||||||
|
const val = this.shape[r][c];
|
||||||
|
if (val) cells.push({ x: this.x + c, y: this.y + r, val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Game {
|
||||||
|
board: Cell[][];
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
holdCanvas: HTMLCanvasElement | null;
|
||||||
|
queueCanvas: HTMLCanvasElement | null;
|
||||||
|
ctx: CanvasRenderingContext2D | null;
|
||||||
|
holdCtx: CanvasRenderingContext2D | null;
|
||||||
|
queueCtx: CanvasRenderingContext2D | null;
|
||||||
|
piece: Piece | null = null;
|
||||||
|
holdPiece: Piece | null = null;
|
||||||
|
canHold: boolean = true;
|
||||||
|
nextQueue: string[] = [];
|
||||||
|
score: number = 0;
|
||||||
|
level: number = 1;
|
||||||
|
lines: number = 0;
|
||||||
|
garbage: number = 0;
|
||||||
|
dropInterval: number = 1000;
|
||||||
|
lastDrop: number = 0;
|
||||||
|
isLocking: boolean = false;
|
||||||
|
lockRotationCount: number = 0;
|
||||||
|
lockLastRotationCount: number = 0;
|
||||||
|
isGameOver: boolean = false;
|
||||||
|
isPaused: boolean = false;
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
constructor(canvasId: string, id: number) {
|
||||||
|
this.id = id;
|
||||||
|
const el = document.getElementById(
|
||||||
|
canvasId + "-board",
|
||||||
|
) 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(canvasId + "-hold") as HTMLCanvasElement;
|
||||||
|
this.queueCanvas = document.getElementById(canvasId + "-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");
|
||||||
|
if (!this.holdCtx || !this.queueCtx)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.holdCtx.clearRect(0, 0, 200, 200);
|
||||||
|
this.queueCtx.clearRect(0, 0, 500, 500);
|
||||||
|
|
||||||
|
this.board = this.createEmptyBoard();
|
||||||
|
if (id == 0)
|
||||||
|
this.fillBag();
|
||||||
|
else
|
||||||
|
this.nextQueue = game1.nextQueue;
|
||||||
|
|
||||||
|
this.spawnPiece();
|
||||||
|
if (id != 0)
|
||||||
|
this.piece.type = game1.piece.type;
|
||||||
|
this.registerListeners();
|
||||||
|
requestAnimationFrame(this.loop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmptyBoard(): Cell[][] {
|
||||||
|
const b: Cell[][] = [];
|
||||||
|
for (let r = 0; r < ROWS; r++) {
|
||||||
|
const row: Cell[] = new Array(COLS).fill(0);
|
||||||
|
b.push(row);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillBag() {
|
||||||
|
// classic 7-bag randomizer
|
||||||
|
const pieces = Object.keys(TETROMINOES);
|
||||||
|
const bag = [...pieces];
|
||||||
|
for (let i = bag.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[bag[i], bag[j]] = [bag[j], bag[i]];
|
||||||
|
}
|
||||||
|
this.nextQueue.push(...bag);
|
||||||
|
}
|
||||||
|
|
||||||
|
hold() {
|
||||||
|
if (!this.canHold) return;
|
||||||
|
|
||||||
|
[this.piece, this.holdPiece] = [this.holdPiece, this.piece];
|
||||||
|
if (!this.piece) this.spawnPiece();
|
||||||
|
if (!this.piece) return;
|
||||||
|
|
||||||
|
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
|
||||||
|
this.piece.y = -2;
|
||||||
|
this.piece.rotationIndex = 0;
|
||||||
|
this.piece.shape = this.piece.rotations[this.piece.rotationIndex];
|
||||||
|
|
||||||
|
this.canHold = false;
|
||||||
|
this.drawHold();
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnPiece() {
|
||||||
|
this.canHold = true;
|
||||||
|
if (this.nextQueue.length < 7) this.fillBag();
|
||||||
|
const type = this.nextQueue.shift()!;
|
||||||
|
this.piece = new Piece(type);
|
||||||
|
if (this.collides(this.piece)) {
|
||||||
|
game1.isGameOver = true;
|
||||||
|
game2.isGameOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawHold();
|
||||||
|
this.drawQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
collides(piece: Piece): boolean {
|
||||||
|
for (const cell of piece.getCells()) {
|
||||||
|
if (cell.y >= ROWS) return true;
|
||||||
|
if (cell.x < 0 || cell.x >= COLS) return true;
|
||||||
|
if (cell.y >= 0 && this.board[cell.y][cell.x]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGhostOffset(piece: Piece): number {
|
||||||
|
let y: number = 0;
|
||||||
|
while (true) {
|
||||||
|
for (const cell of piece.getCells()) {
|
||||||
|
if (
|
||||||
|
cell.y + y >= ROWS ||
|
||||||
|
(cell.y + y >= 0 && this.board[cell.y + y][cell.x])
|
||||||
|
)
|
||||||
|
return y - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lockPiece() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
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 > 0) isValid = true;
|
||||||
|
}
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
this.id == 0 ? p2_score++ : p1_score++;
|
||||||
|
game1.isGameOver = true;
|
||||||
|
game2.isGameOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.garbage)
|
||||||
|
{
|
||||||
|
const empty_spot = Math.floor(Math.random() * 10);
|
||||||
|
while (this.garbage)
|
||||||
|
{
|
||||||
|
//if () // if anything else than 0 on top, die >:3
|
||||||
|
this.board.shift();
|
||||||
|
this.board.push(Array(COLS).fill(8));
|
||||||
|
this.board[19][empty_spot] = 0;
|
||||||
|
this.garbage--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clearLines();
|
||||||
|
this.spawnPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
addGarbage(lines: number) {
|
||||||
|
this.garbage += lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLines() {
|
||||||
|
let linesCleared = 0;
|
||||||
|
outer: for (let r = ROWS - 1; r >= 0; r--) {
|
||||||
|
for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer;
|
||||||
|
|
||||||
|
this.board.splice(r, 1);
|
||||||
|
this.board.unshift(new Array(COLS).fill(0));
|
||||||
|
linesCleared++;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linesCleared > 0) {
|
||||||
|
this.lines += linesCleared;
|
||||||
|
const points = [0, 40, 100, 300, 1200];
|
||||||
|
this.score += (points[linesCleared] || 0) * this.level;
|
||||||
|
// level up every 10 lines (Fixed Goal System)
|
||||||
|
const newLevel = Math.floor(this.lines / 10) + 1;
|
||||||
|
if (newLevel > this.level) {
|
||||||
|
this.level = newLevel;
|
||||||
|
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.garbage)
|
||||||
|
{
|
||||||
|
while (linesCleared)
|
||||||
|
{
|
||||||
|
this.garbage--;
|
||||||
|
linesCleared--;
|
||||||
|
if (!this.garbage)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.id == 0 && linesCleared)
|
||||||
|
game2.addGarbage(linesCleared < 4 ? linesCleared - 1 : linesCleared);
|
||||||
|
else
|
||||||
|
game1.addGarbage(linesCleared < 4 ? linesCleared - 1 : linesCleared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rotatePiece(dir: "cw" | "ccw") {
|
||||||
|
if (!this.piece) return;
|
||||||
|
if (this.isLocking && this.lockRotationCount < 15)
|
||||||
|
this.lockRotationCount++;
|
||||||
|
// Try wall kicks
|
||||||
|
const originalIndex = this.piece.rotationIndex;
|
||||||
|
if (dir === "cw") this.piece.rotateCW();
|
||||||
|
else this.piece.rotateCCW();
|
||||||
|
const kicks = [0, -1, 1, -2, 2];
|
||||||
|
for (const k of kicks) {
|
||||||
|
this.piece.x += k;
|
||||||
|
if (!this.collides(this.piece)) return;
|
||||||
|
this.piece.x -= k;
|
||||||
|
}
|
||||||
|
// no kick, revert
|
||||||
|
this.piece.rotationIndex = originalIndex;
|
||||||
|
this.piece.shape = this.piece.rotations[originalIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
movePiece(dx: number, dy: number) {
|
||||||
|
if (!this.piece) return;
|
||||||
|
this.piece.x += dx;
|
||||||
|
this.piece.y += dy;
|
||||||
|
|
||||||
|
if (this.collides(this.piece)) {
|
||||||
|
this.piece.x -= dx;
|
||||||
|
this.piece.y -= dy;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hardDrop() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
let dropped = 0;
|
||||||
|
while (this.movePiece(0, 1)) dropped++;
|
||||||
|
this.score += dropped * 2;
|
||||||
|
this.lockPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
softDrop() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
if (!this.movePiece(0, 1)) return;
|
||||||
|
else this.score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys: Record<string, boolean> = {};
|
||||||
|
direction: number = 0;
|
||||||
|
inputDelay = 200;
|
||||||
|
inputTimestamp = Date.now();
|
||||||
|
move: boolean = false;
|
||||||
|
|
||||||
|
inputManager() {
|
||||||
|
const left = this.id === 0 ? this.keys["KeyA"] : this.keys["Numpad4"]
|
||||||
|
const right = this.id === 0 ? this.keys["KeyD"] : this.keys["Numpad6"]
|
||||||
|
if (this.move || Date.now() > this.inputTimestamp + this.inputDelay)
|
||||||
|
{
|
||||||
|
if (left && !right)
|
||||||
|
this.movePiece(-1, 0);
|
||||||
|
else if (!left && right)
|
||||||
|
this.movePiece(1, 0);
|
||||||
|
else if (left && right)
|
||||||
|
this.movePiece(this.direction, 0);
|
||||||
|
this.move = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (this.keys["ArrowDown"])
|
||||||
|
this.softDrop();*/
|
||||||
|
}
|
||||||
|
|
||||||
|
registerListeners() {
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
this.keys[e.code] = true;
|
||||||
|
|
||||||
|
if (this.isGameOver) return;
|
||||||
|
|
||||||
|
if (e.key === "p" || e.key === "P" || e.key === "Escape")
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
|
||||||
|
if (this.isPaused) return;
|
||||||
|
|
||||||
|
if (this.id === 0 ? e.code === "KeyA" : e.code === "Numpad4")
|
||||||
|
{
|
||||||
|
this.inputTimestamp = Date.now();
|
||||||
|
this.direction = -1;//this.movePiece(-1, 0);
|
||||||
|
this.move = true;
|
||||||
|
}
|
||||||
|
else if (this.id === 0 ? e.code === "KeyD" : e.code === "Numpad6")
|
||||||
|
{
|
||||||
|
this.inputTimestamp = Date.now();
|
||||||
|
this.direction = 1;//this.movePiece(1, 0);
|
||||||
|
this.move = true;
|
||||||
|
}
|
||||||
|
else if (this.id === 0 ? e.code === "KeyS" : e.code === "Numpad5") this.softDrop();
|
||||||
|
else if (this.id === 0 ? e.code === "Space" : e.code === "Numpad0") {
|
||||||
|
//e.preventDefault();
|
||||||
|
this.hardDrop();
|
||||||
|
} else if (this.id === 0 ? e.code === "ShiftLeft" : e.code === "NumpadEnter") {
|
||||||
|
//e.preventDefault();
|
||||||
|
this.hold();
|
||||||
|
} else if (this.id === 0 ? (e.code === "KeyE" || e.code === "KeyW") : (e.code === "Numpad9" || e.code === "Numpad8")) {
|
||||||
|
//e.preventDefault();
|
||||||
|
this.rotatePiece("cw");
|
||||||
|
} else if (this.id === 0 ? (e.code === "KeyQ" || e.code === "ControlLeft") : e.code === "Numpad7") {
|
||||||
|
//e.preventDefault();
|
||||||
|
this.rotatePiece("ccw");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
|
this.keys[e.code] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loop(timestamp: number) {
|
||||||
|
if (!view.running) return;
|
||||||
|
if (!this.lastDrop) this.lastDrop = timestamp;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (p1_score != 3 && p2_score != 3)
|
||||||
|
{
|
||||||
|
if (this.id == 0)
|
||||||
|
{
|
||||||
|
game1 = new Game("board1", 0);
|
||||||
|
game2 = new Game("board2", 1);
|
||||||
|
}
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if (await isLogged() && this.id == 0)
|
||||||
|
{
|
||||||
|
let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
fetch(`${user_api}/users/${uuid}/matchHistory?game=tetris`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"game": "tetris",
|
||||||
|
"opponent": p2_name,
|
||||||
|
"myScore": p1_score,
|
||||||
|
"opponentScore": p2_score,
|
||||||
|
"date": Date.now(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById("game-buttons")?.classList.remove("hidden");
|
||||||
|
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 = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(14.5% 0 0)" : "oklch(55.6% 0 0)";
|
||||||
|
for (let r = 0; r <= ROWS; r++) {
|
||||||
|
// horizontal lines
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, r * BLOCK);
|
||||||
|
ctx.lineTo(COLS * BLOCK, r * BLOCK);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let c = 0; c <= COLS; c++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(c * BLOCK, 0);
|
||||||
|
ctx.lineTo(c * BLOCK, ROWS * BLOCK);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBoard() {
|
||||||
|
this.drawGrid();
|
||||||
|
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], this.ctx);
|
||||||
|
else this.clearBlock(c, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPiece() {
|
||||||
|
if (!this.piece) return;
|
||||||
|
|
||||||
|
for (const cell of this.piece.getCells())
|
||||||
|
if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val], this.ctx);
|
||||||
|
|
||||||
|
let offset: number = this.getGhostOffset(this.piece);
|
||||||
|
for (const cell of this.piece.getCells())
|
||||||
|
if (cell.y + offset >= 0 && offset > 0)
|
||||||
|
this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHold() {
|
||||||
|
if (!this.holdCtx || !this.holdCanvas) return;
|
||||||
|
|
||||||
|
this.holdCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
|
||||||
|
this.holdCtx.fillRect(0, 0, this.holdCanvas.width, this.holdCanvas.height);
|
||||||
|
|
||||||
|
if (!this.holdPiece) return;
|
||||||
|
let y: number = 0;
|
||||||
|
for (const row of this.holdPiece.rotations[0]) {
|
||||||
|
let x: number = 0;
|
||||||
|
for (const val of row) {
|
||||||
|
if (val)
|
||||||
|
this.fillBlock(x + (4 - this.holdPiece.rotations[0].length)/ 2 + 0.35, y + 0.5, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : COLORS[8], this.holdCtx);
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawQueue() {
|
||||||
|
if (!this.queueCtx || !this.queueCanvas) return ;
|
||||||
|
|
||||||
|
this.queueCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
|
||||||
|
this.queueCtx.fillRect(0, 0, this.queueCanvas.width, this.queueCanvas.height);
|
||||||
|
let placement: number = 0;
|
||||||
|
for (const nextPiece of this.nextQueue.slice(0, 5)) {
|
||||||
|
let y: number = 0;
|
||||||
|
for (const row of TETROMINOES[nextPiece][0]) {
|
||||||
|
let x: number = 0;
|
||||||
|
for (const val of row) {
|
||||||
|
if (val)
|
||||||
|
this.fillBlock(x + (4 - TETROMINOES[nextPiece][0].length) / 2 + 0.25, y + 0.5 + placement * 2.69 - (nextPiece ==="I" ? 0.35 : 0), COLORS[["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1], this.queueCtx);
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
placement++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillBlock(x: number, y: number, color: string[], ctx: CanvasRenderingContext2D | null) {
|
||||||
|
if (!ctx) return;
|
||||||
|
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(Math.round(x * BLOCK) + 4, Math.round(y * BLOCK) + 4, BLOCK - 4, BLOCK - 4);
|
||||||
|
const X = Math.round(x * BLOCK);
|
||||||
|
const Y = Math.round(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;
|
||||||
|
|
||||||
|
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.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
|
||||||
|
ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHUD() {
|
||||||
|
if (!this.ctx || !this.canvas) return;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
|
||||||
|
if (this.garbage)
|
||||||
|
{
|
||||||
|
ctx.fillStyle ="red";
|
||||||
|
ctx.fillRect(0, this.canvas.height - BLOCK * this.garbage, 6, BLOCK * this.garbage);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.6)";
|
||||||
|
ctx.fillRect(4, 4, 120, 60);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "12px Kubasta";
|
||||||
|
ctx.fillText(`${this.id == 0 ? p1_displayName : p2_displayName}: ${this.id == 0 ? p1_score : p2_score}`, 8, 20);
|
||||||
|
ctx.fillText(`score: ${this.score}`, 8, 36);
|
||||||
|
ctx.fillText(`lines: ${this.lines}`, 8, 52);
|
||||||
|
|
||||||
|
if (this.isPaused) {
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
|
ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "24px Kubasta";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(
|
||||||
|
"paused",
|
||||||
|
this.canvas.width / 2,
|
||||||
|
this.canvas.height / 2 + 8,
|
||||||
|
);
|
||||||
|
ctx.textAlign = "start";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isGameOver) {
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
|
ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "28px Kubasta";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(
|
||||||
|
"game over",
|
||||||
|
this.canvas.width / 2,
|
||||||
|
this.canvas.height / 2 + 8,
|
||||||
|
);
|
||||||
|
ctx.textAlign = "start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
if (!this.ctx || !this.canvas) return;
|
||||||
|
// clear everything
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#000";
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "#111";
|
||||||
|
for (let r = 0; r <= ROWS; r++) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(0, r * BLOCK);
|
||||||
|
this.ctx.lineTo(COLS * BLOCK, r * BLOCK);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let c = 0; c <= COLS; c++) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(c * BLOCK, 0);
|
||||||
|
this.ctx.lineTo(c * BLOCK, ROWS * BLOCK);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawBoard();
|
||||||
|
this.drawPiece();
|
||||||
|
this.drawHUD();
|
||||||
|
this.drawQueue();
|
||||||
|
setSleepPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("game-retry")?.addEventListener("click", () => { document.getElementById("game-buttons")?.classList.add("hidden"); game1 = new Game("board1", 0); game2 = new Game("board2", 1); });
|
||||||
|
|
||||||
|
let p1_input: HTMLInputElement = document.getElementById("player1") as HTMLInputElement;
|
||||||
|
let p2_input: HTMLInputElement = document.getElementById("player2") as HTMLInputElement;
|
||||||
|
|
||||||
|
p2_input.value = "player 2";
|
||||||
|
if (await isLogged())
|
||||||
|
{
|
||||||
|
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
|
||||||
|
p1_input.value = uuid;
|
||||||
|
p1_input.readOnly = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
p1_input.value = "player 1";
|
||||||
|
|
||||||
|
document.getElementById("game-start")?.addEventListener("click", async () => {
|
||||||
|
let p1_isvalid = true;
|
||||||
|
let p2_isvalid = true;
|
||||||
|
if (await isLogged()) {
|
||||||
|
const p1_req = await fetch(`${user_api}/users/${p1_input.value}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const p2_req = await fetch(`${user_api}/users/${p2_input.value}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (p1_req.status != 200)
|
||||||
|
p1_isvalid = false;
|
||||||
|
else
|
||||||
|
p1_displayName = (await p1_req.json()).displayName;
|
||||||
|
|
||||||
|
if (p2_req.status != 200)
|
||||||
|
p2_isvalid = false;
|
||||||
|
else
|
||||||
|
p2_displayName = (await p2_req.json()).displayName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
p1_isvalid = p2_isvalid = false;
|
||||||
|
|
||||||
|
p1_name = p1_input.value;
|
||||||
|
p2_name = p2_input.value;
|
||||||
|
if (!p1_isvalid)
|
||||||
|
p1_displayName = p1_name;
|
||||||
|
if (!p2_isvalid)
|
||||||
|
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;
|
||||||
|
|
||||||
|
document.getElementById("player-inputs").remove();
|
||||||
|
document.getElementById("game-boards").classList.remove("hidden");
|
||||||
|
game1 = new Game("board1", 0);
|
||||||
|
game2 = new Game("board2", 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/front/static/ts/views/TotpEnable.ts
Normal file
110
src/front/static/ts/views/TotpEnable.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { navigationManager, user_api, auth_api } from "../main.ts";
|
||||||
|
import { dragElement } from "./drag.ts";
|
||||||
|
|
||||||
|
async function totpVerify() {
|
||||||
|
const code = (document.getElementById("totpPin") as HTMLInputElement).value;
|
||||||
|
const data_req = await fetch(auth_api + '/2fa/verify', {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: code
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data_req.status === 200) {
|
||||||
|
navigationManager("/settings");
|
||||||
|
} else if (data_req.status === 401 || data_req.status === 400) {
|
||||||
|
const popup_content = document.getElementById("2fa-enable-content");
|
||||||
|
|
||||||
|
if (!document.getElementById("error-totp")) {
|
||||||
|
const error = document.createElement("p");
|
||||||
|
error.id = "error-totp";
|
||||||
|
error.classList.add("text-red-700", "dark:text-red-500", "text-center");
|
||||||
|
error.innerHTML = (await data_req.json()).error;
|
||||||
|
|
||||||
|
popup_content?.appendChild(error)
|
||||||
|
} else {
|
||||||
|
const error = document.getElementById("error-totp") as HTMLParagraphElement;
|
||||||
|
error.innerHTML = (await data_req.json()).error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Unexpected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function totpEnablePopup(username: String, secret: String, url: String) {
|
||||||
|
const popup: HTMLDivElement = document.createElement("div");
|
||||||
|
popup.id = "2fa-enable-popup";
|
||||||
|
popup.classList.add("z-10", "absolute", "default-border");
|
||||||
|
const header = popup.appendChild(document.createElement("div"));;
|
||||||
|
header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2");
|
||||||
|
header.id = "2fa-enable-popup-header";
|
||||||
|
header.appendChild(document.createElement("span")).innerText = "2fa_enable.ts";
|
||||||
|
const btn = header.appendChild(document.createElement("button"));
|
||||||
|
btn.innerText = " × ";
|
||||||
|
btn.onclick = () => { document.getElementById("2fa-enable-popup")?.remove(); };
|
||||||
|
|
||||||
|
const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div"));
|
||||||
|
popup_content.id = "2fa-enable-content";
|
||||||
|
popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4");
|
||||||
|
|
||||||
|
const qrDivTOTP = document.createElement("div");
|
||||||
|
qrDivTOTP.classList.add("flex", "justify-center");
|
||||||
|
|
||||||
|
const qrCodeTOTP = document.createElement("img");
|
||||||
|
qrCodeTOTP.id = "qrCodeTOTP";
|
||||||
|
qrCodeTOTP.src = `https://api.qrserver.com/v1/create-qr-code/?margin=10&size=512x512&data=${url}`;
|
||||||
|
qrCodeTOTP.classList.add("w-60");
|
||||||
|
qrDivTOTP.appendChild(qrCodeTOTP);
|
||||||
|
|
||||||
|
const secretText = document.createElement("p");
|
||||||
|
secretText.innerHTML = `key: <div class="select-all">${secret}</div>`;
|
||||||
|
secretText.classList.add("text-center")
|
||||||
|
|
||||||
|
const tokenInput = document.createElement("input");
|
||||||
|
tokenInput.type = "tel";
|
||||||
|
tokenInput.id = "totpPin";
|
||||||
|
tokenInput.name = "totpPin";
|
||||||
|
tokenInput.placeholder = "TOTP code";
|
||||||
|
tokenInput.required = true;
|
||||||
|
tokenInput.autocomplete = "off";
|
||||||
|
tokenInput.pattern = "[0-9]*";
|
||||||
|
tokenInput.setAttribute("inputmode", "numeric");
|
||||||
|
tokenInput.classList.add("bg-white", "text-neutral-900", "w-full", "px-4", "py-2", "input-border");
|
||||||
|
|
||||||
|
const tokenSubmit = document.createElement("button");
|
||||||
|
tokenSubmit.type = "submit";
|
||||||
|
tokenSubmit.classList.add("default-button", "w-full");
|
||||||
|
tokenSubmit.id = "totp-submit";
|
||||||
|
tokenSubmit.innerHTML = "submit";
|
||||||
|
|
||||||
|
const hr = document.createElement("hr");
|
||||||
|
hr.classList.add("my-2", "w-full", "reverse-border");
|
||||||
|
|
||||||
|
const t = document.createElement("h2");
|
||||||
|
t.innerHTML = "hey " + username +
|
||||||
|
` you are trying to add 2fa</br>
|
||||||
|
just add the following to your app and enter the code bellow ↓
|
||||||
|
`;
|
||||||
|
t.classList.add("text-center")
|
||||||
|
|
||||||
|
document.getElementById("app")?.appendChild(popup);
|
||||||
|
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.method = "dialog";
|
||||||
|
form.classList.add("space-y-4");
|
||||||
|
form.appendChild(tokenInput);
|
||||||
|
form.appendChild(tokenSubmit);
|
||||||
|
|
||||||
|
popup_content.appendChild(t)
|
||||||
|
popup_content.appendChild(qrDivTOTP);
|
||||||
|
popup_content.appendChild(secretText);
|
||||||
|
popup_content.appendChild(hr)
|
||||||
|
popup_content.appendChild(form);
|
||||||
|
dragElement(document.getElementById("2fa-enable-popup"));
|
||||||
|
|
||||||
|
document.getElementById("totp-submit")?.addEventListener("click", totpVerify)
|
||||||
|
}
|
||||||
544
src/front/static/ts/views/TournamentMenu.ts
Normal file
544
src/front/static/ts/views/TournamentMenu.ts
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
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[] = [];
|
||||||
|
for (let i of Array(initPlayerCount).keys()) {
|
||||||
|
players.push((document.getElementById(`playerName${i}`) as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (tournament[0].length > 1)
|
||||||
|
{
|
||||||
|
this.updateBracketDisplay(tournament, players);
|
||||||
|
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[p1]} vs ${players[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[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/front/static/ts/views/drag.ts
Normal file
43
src/front/static/ts/views/drag.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { setOnekoOffset } from "../oneko.ts";
|
||||||
|
|
||||||
|
export function dragElement(el) {
|
||||||
|
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
||||||
|
if (document.getElementById(el.id + "-header")) {
|
||||||
|
// if present, the header is where you move the DIV from:
|
||||||
|
document.getElementById(el.id + "-header").onmousedown = dragMouseDown;
|
||||||
|
} else {
|
||||||
|
// otherwise, move the DIV from anywhere inside the DIV:
|
||||||
|
el.onmousedown = dragMouseDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragMouseDown(e) {
|
||||||
|
e = e || window.event;
|
||||||
|
e.preventDefault();
|
||||||
|
// get the mouse cursor position at startup:
|
||||||
|
pos3 = e.clientX;
|
||||||
|
pos4 = e.clientY;
|
||||||
|
document.onmouseup = closeDragElement;
|
||||||
|
// call a function whenever the cursor moves:
|
||||||
|
document.onmousemove = elementDrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementDrag(e) {
|
||||||
|
e = e || window.event;
|
||||||
|
e.preventDefault();
|
||||||
|
// calculate the new cursor position:
|
||||||
|
pos1 = pos3 - e.clientX;
|
||||||
|
pos2 = pos4 - e.clientY;
|
||||||
|
pos3 = e.clientX;
|
||||||
|
pos4 = e.clientY;
|
||||||
|
// set the element's new position:
|
||||||
|
el.style.top = (el.offsetTop - pos2) + "px";
|
||||||
|
el.style.left = (el.offsetLeft - pos1) + "px";
|
||||||
|
setOnekoOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDragElement() {
|
||||||
|
// stop moving when mouse button is released:
|
||||||
|
document.onmouseup = null;
|
||||||
|
document.onmousemove = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
@ -10,10 +10,10 @@ export async function authUserCreate(username, fastify) {
|
|||||||
};
|
};
|
||||||
const cookie = fastify.jwt.sign({ user: "admin" });
|
const cookie = fastify.jwt.sign({ user: "admin" });
|
||||||
|
|
||||||
const url = process.env.USER_URL || "http://localhost:3002/"
|
const url = process.env.USER_URL || "http://localhost:3002"
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
url + "users/" + username,
|
url + "/users/" + username,
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
content: ['./src/front/**/*.{html,js}'],
|
content: ['./src/front/**/*.{html,js,ts,css}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
jersey: ['"Jersey 10"', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user