mirror of
https://github.com/KeyZox71/knl_meowscendence.git
synced 2025-10-14 02:54:44 +02:00
「✨」 feat(src.front): :D
This commit is contained in:
@ -13,7 +13,24 @@
|
|||||||
|
|
||||||
<!--body class="bg-gray-100 dark:bg-neutral-950 h-screen flex flex-col"-->
|
<!--body class="bg-gray-100 dark:bg-neutral-950 h-screen flex flex-col"-->
|
||||||
<body class="bg-neutral-950 dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col">
|
<body class="bg-neutral-950 dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col">
|
||||||
<script src="https://kanel.ovh/oneko.js"></script>
|
<!--script src="https://kanel.ovh/oneko.js"></script-->
|
||||||
|
<!--script src="./static/ts/oneko.js"></script-->
|
||||||
|
|
||||||
|
<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 id="app" class="flex-1 flex items-center justify-center">
|
||||||
</div>
|
</div>
|
||||||
|
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 |
@ -1,3 +1,5 @@
|
|||||||
|
import { oneko } from "./oneko.ts";
|
||||||
|
|
||||||
export async function isLogged(): boolean {
|
export async function isLogged(): boolean {
|
||||||
let uuid_req = await fetch("http://localhost:3001/me", {
|
let uuid_req = await fetch("http://localhost:3001/me", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -82,12 +84,14 @@ const routes = [
|
|||||||
{ path: "/pong/local", view: () => import("./views/Game.ts") },
|
{ path: "/pong/local", view: () => import("./views/Game.ts") },
|
||||||
{ path: "/pong/tournament", view: () => import("./views/TournamentMenu.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/Tetris.ts") },
|
||||||
|
|
||||||
{ path: "/login", view: () => import("./views/LoginPage.ts") },
|
{ path: "/login", view: () => import("./views/LoginPage.ts") },
|
||||||
{ path: "/register", view: () => import("./views/RegisterPage.ts") },
|
{ path: "/register", view: () => import("./views/RegisterPage.ts") },
|
||||||
|
|
||||||
{ path: "/profile", view: () => import("./views/Profile.ts") },
|
{ path: "/profile", view: () => import("./views/Profile.ts") },
|
||||||
|
|
||||||
{ path: "/tetris", view: () => import("./views/Tetris.ts") },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = async () => {
|
const router = async () => {
|
||||||
@ -124,7 +128,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigationManager(e.target.href);
|
navigationManager(e.target.href);
|
||||||
}
|
}
|
||||||
|
if (e.target.closest("[data-icon]"))
|
||||||
|
{
|
||||||
|
console.log("xd");
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("dblclick", e=> {
|
||||||
|
if (e.target.closest("[data-icon]"))
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
navigationManager(e.target.closest("[data-icon]").href);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router();
|
router();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
oneko();
|
||||||
|
273
src/front/static/ts/oneko.ts
Normal file
273
src/front/static/ts/oneko.ts
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOnekoOffset() {
|
||||||
|
if (oneko_state == 1)
|
||||||
|
{
|
||||||
|
offsetX = document.getElementById("window").offsetLeft + 44;
|
||||||
|
offsetY = document.getElementById("window").offsetTop + 44 + 24;
|
||||||
|
console.log(offsetX, offsetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBallPos(x: number, y: number) {
|
||||||
|
mousePosX = x + offsetX;
|
||||||
|
mousePosY = y + offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
function onAnimationFrame(timestamp) {
|
||||||
|
// 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 (
|
||||||
|
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();
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import Aview from "./Aview.ts"
|
import Aview from "./Aview.ts"
|
||||||
import { isLogged } from "../main.js"
|
import { isLogged } from "../main.js"
|
||||||
|
import { dragElement } from "./drag.js"
|
||||||
|
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts"
|
||||||
|
|
||||||
export default class extends Aview {
|
export default class extends Aview {
|
||||||
|
|
||||||
@ -10,13 +12,14 @@ export default class extends Aview {
|
|||||||
super();
|
super();
|
||||||
this.setTitle("pong (local match)");
|
this.setTitle("pong (local match)");
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
setOnekoState("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHTML() {
|
async getHTML() {
|
||||||
return `
|
return `
|
||||||
<div class="default-border">
|
<div id="window" class="absolute default-border">
|
||||||
<div class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
|
<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]">knl_meowscendence</span>
|
<span class="font-[Kubasta]">pong_game.ts</span>
|
||||||
<div>
|
<div>
|
||||||
<button> - </button>
|
<button> - </button>
|
||||||
<button> □ </button>
|
<button> □ </button>
|
||||||
@ -34,14 +37,16 @@ export default class extends Aview {
|
|||||||
<button id="game-start" class="default-button">play</button>
|
<button id="game-start" class="default-button">play</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="game-buttons" class="hidden flex mt-4">
|
<div id="game-buttons" class="hidden flex mt-4">
|
||||||
<button id="game-retry" class="bg-blue-600 text-white hover:bg-blue-500 w-full mx-4 py-2 rounded-md transition-colors">play again</button>
|
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
|
||||||
<a id="game-back" class="bg-gray-600 text-white hover:bg-gray-500 w-full mx-4 py-2 rounded-md transition-colors" href="/pong" data-link>back</a>
|
<a id="game-back" class="default-button w-full mx-4 py-2" href="/pong" data-link>back</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
|
||||||
let start: number = 0;
|
let start: number = 0;
|
||||||
let elapsed: number;
|
let elapsed: number;
|
||||||
|
|
||||||
@ -128,6 +133,7 @@ export default class extends Aview {
|
|||||||
// scoring
|
// scoring
|
||||||
if (ballX < 0 || ballX > canvas.width - ballSize)
|
if (ballX < 0 || ballX > canvas.width - ballSize)
|
||||||
{
|
{
|
||||||
|
setOnekoState("default");
|
||||||
game_playing = false;
|
game_playing = false;
|
||||||
if (ballX < 0)
|
if (ballX < 0)
|
||||||
p2_score++;
|
p2_score++;
|
||||||
@ -158,6 +164,7 @@ export default class extends Aview {
|
|||||||
leftPaddleY = canvas.height / 2 - paddleHeight / 2;
|
leftPaddleY = canvas.height / 2 - paddleHeight / 2;
|
||||||
rightPaddleY = canvas.height / 2 - paddleHeight / 2;
|
rightPaddleY = canvas.height / 2 - paddleHeight / 2;
|
||||||
}
|
}
|
||||||
|
setBallPos(ballX, ballY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
@ -235,6 +242,7 @@ export default class extends Aview {
|
|||||||
|
|
||||||
|
|
||||||
document.getElementById("game-retry")?.addEventListener("click", () => {
|
document.getElementById("game-retry")?.addEventListener("click", () => {
|
||||||
|
setOnekoState("pong");
|
||||||
document.getElementById("game-buttons").classList.add("hidden");
|
document.getElementById("game-buttons").classList.add("hidden");
|
||||||
game_playing = false;
|
game_playing = false;
|
||||||
match_over = false;
|
match_over = false;
|
||||||
@ -273,6 +281,8 @@ export default class extends Aview {
|
|||||||
ballX = canvas.width / 2;
|
ballX = canvas.width / 2;
|
||||||
ballY = canvas.height / 2;
|
ballY = canvas.height / 2;
|
||||||
|
|
||||||
|
setOnekoState("pong");
|
||||||
|
setOnekoOffset();
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Aview from "./Aview.ts"
|
import Aview from "./Aview.ts"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
import { isLogged, navigationManager } from "../main.ts"
|
import { isLogged, navigationManager } from "../main.ts"
|
||||||
|
|
||||||
export default class extends Aview {
|
export default class extends Aview {
|
||||||
@ -7,6 +8,7 @@ export default class extends Aview {
|
|||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setTitle("login");
|
this.setTitle("login");
|
||||||
|
setOnekoState("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHTML() {
|
async getHTML() {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Aview from "./Aview.ts"
|
import Aview from "./Aview.ts"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
|
||||||
export default class extends Aview {
|
export default class extends Aview {
|
||||||
|
|
||||||
@ -6,12 +7,13 @@ export default class extends Aview {
|
|||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setTitle("knl is trans(cendence)");
|
this.setTitle("knl is trans(cendence)");
|
||||||
|
setOnekoState("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHTML() {
|
async getHTML() {
|
||||||
// <div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4"-->
|
// <div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4"-->
|
||||||
return `
|
return `
|
||||||
<div class="default-border">
|
<!--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">
|
<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>
|
<span class="font-[Kubasta]">knl_meowscendence</span>
|
||||||
<div>
|
<div>
|
||||||
@ -26,7 +28,7 @@ export default class extends Aview {
|
|||||||
Pong
|
Pong
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div-->
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import Aview from "./Aview.ts"
|
import Aview from "./Aview.ts"
|
||||||
|
import { dragElement } from "./drag.js"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
|
|
||||||
export default class extends Aview {
|
export default class extends Aview {
|
||||||
|
|
||||||
@ -6,12 +8,13 @@ export default class extends Aview {
|
|||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setTitle("knl is trans(cendence)");
|
this.setTitle("knl is trans(cendence)");
|
||||||
|
setOnekoState("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHTML() {
|
async getHTML() {
|
||||||
return `
|
return `
|
||||||
<div class="default-border">
|
<div id="window" class="absolute default-border">
|
||||||
<div class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
|
<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>
|
<span class="font-[Kubasta]">pong_game.ts</span>
|
||||||
<div>
|
<div>
|
||||||
<button> - </button>
|
<button> - </button>
|
||||||
@ -20,7 +23,7 @@ export default class extends Aview {
|
|||||||
</div>
|
</div>
|
||||||
</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="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">pong is funny yay</p>
|
<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">
|
<div class="flex flex-col space-y-4">
|
||||||
<a class="default-button" href="/pong/local" data-link>
|
<a class="default-button" href="/pong/local" data-link>
|
||||||
local match
|
local match
|
||||||
@ -33,4 +36,7 @@ export default class extends Aview {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
async run() {
|
||||||
|
dragElement(document.getElementById("window"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Aview from "./Aview.ts"
|
import Aview from "./Aview.ts"
|
||||||
|
import { setOnekoState } from "../oneko.ts"
|
||||||
import { isLogged, navigationManager } from "../main.ts"
|
import { isLogged, navigationManager } from "../main.ts"
|
||||||
|
|
||||||
export default class extends Aview {
|
export default class extends Aview {
|
||||||
@ -7,6 +8,7 @@ export default class extends Aview {
|
|||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setTitle("register");
|
this.setTitle("register");
|
||||||
|
setOnekoState("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHTML() {
|
async getHTML() {
|
||||||
|
590
src/front/static/ts/views/Tetris.ts
Normal file
590
src/front/static/ts/views/Tetris.ts
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
import Aview from "./Aview.ts"
|
||||||
|
|
||||||
|
export default class extends Aview {
|
||||||
|
|
||||||
|
running: boolean;
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("tetris (local match)");
|
||||||
|
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 space-y-4 reverse-border">
|
||||||
|
<canvas id="board" class="reverse-border" width="300" height="600"></canvas>
|
||||||
|
<div id="game-buttons" class="hidden flex">
|
||||||
|
<button id="game-retry" class="default-button">play again</button>
|
||||||
|
<a id="game-back" class="default-button" href="/tetris" data-link>back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const COLS = 10;
|
||||||
|
const ROWS = 20;
|
||||||
|
const BLOCK = 30; // pixels per block
|
||||||
|
|
||||||
|
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', // placeholder for 0
|
||||||
|
'#00ffff', // I - cyan
|
||||||
|
'#0000ff', // J - blue
|
||||||
|
'#ff7f00', // L - orange
|
||||||
|
'#ffff00', // O - yellow
|
||||||
|
'#00ff00', // S - green
|
||||||
|
'#800080', // T - purple
|
||||||
|
'#ff0000', // Z - red
|
||||||
|
];
|
||||||
|
|
||||||
|
class Piece {
|
||||||
|
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;
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
piece: Piece | null = null;
|
||||||
|
holdPiece: Piece | null = null;
|
||||||
|
canHold: boolean = true;
|
||||||
|
nextQueue: string[] = [];
|
||||||
|
score = 0;
|
||||||
|
level = 1;
|
||||||
|
lines = 0;
|
||||||
|
dropInterval = 1000;
|
||||||
|
lastDrop = 0;
|
||||||
|
isGameOver = false;
|
||||||
|
isPaused = false;
|
||||||
|
|
||||||
|
constructor(canvasId: string) {
|
||||||
|
const el = document.getElementById(canvasId);
|
||||||
|
this.canvas = el;
|
||||||
|
this.canvas.width = COLS * BLOCK;
|
||||||
|
this.canvas.height = ROWS * BLOCK;
|
||||||
|
const ctx = this.canvas.getContext('2d');
|
||||||
|
if (!ctx)
|
||||||
|
throw new Error('2D context not available');
|
||||||
|
this.ctx = ctx;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
|
||||||
|
this.piece.y = -2;
|
||||||
|
|
||||||
|
this.canHold = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnPiece() {
|
||||||
|
this.canHold = true;
|
||||||
|
if (this.nextQueue.length < 7)
|
||||||
|
this.fillBag();
|
||||||
|
const type = this.nextQueue.shift()!;
|
||||||
|
this.piece = new Piece(type);
|
||||||
|
// If spawn collides immediately -> game over
|
||||||
|
if (this.collides(this.piece))
|
||||||
|
{
|
||||||
|
this.isGameOver = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockPiece() {
|
||||||
|
if (!this.piece)
|
||||||
|
return;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
// scoring like classic tetris
|
||||||
|
const points = [0, 40, 100, 300, 1200];
|
||||||
|
this.score += (points[linesCleared] || 0) * this.level;
|
||||||
|
// level up every 10 lines (Fixed Goal System)
|
||||||
|
const newLevel = Math.floor(this.lines / 10) + 1;
|
||||||
|
if (newLevel > this.level)
|
||||||
|
{
|
||||||
|
this.level = newLevel;
|
||||||
|
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rotatePiece(dir: 'cw' | 'ccw') {
|
||||||
|
if (!this.piece)
|
||||||
|
return;
|
||||||
|
// Try rotation with wall kicks
|
||||||
|
const originalIndex = this.piece.rotationIndex;
|
||||||
|
if (dir === 'cw')
|
||||||
|
this.piece.rotateCW();
|
||||||
|
else
|
||||||
|
this.piece.rotateCCW();
|
||||||
|
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 valid 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;
|
||||||
|
//this.lockPiece();
|
||||||
|
else
|
||||||
|
this.score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerListeners() {
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (this.isGameOver) return;
|
||||||
|
if (e.key === 'ArrowLeft')
|
||||||
|
this.movePiece(-1, 0);
|
||||||
|
else if (e.key === 'ArrowRight')
|
||||||
|
this.movePiece(1, 0);
|
||||||
|
else if (e.key === 'ArrowDown')
|
||||||
|
this.softDrop();
|
||||||
|
else if (e.code === 'Space')
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
this.hardDrop();
|
||||||
|
}
|
||||||
|
else if (e.code === 'ShiftLeft')
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
this.hold();
|
||||||
|
}
|
||||||
|
else if (e.key === 'x' || e.key === 'X' || e.key === 'ArrowUp')
|
||||||
|
this.rotatePiece('cw');
|
||||||
|
else if (e.key === 'z' || e.key === 'Z' || e.code === 'ControlLeft')
|
||||||
|
this.rotatePiece('ccw');
|
||||||
|
else if (e.key === 'p' || e.key === 'P' || e.key === 'Escape')
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loop(timestamp: number)
|
||||||
|
{
|
||||||
|
if (!this.lastDrop)
|
||||||
|
this.lastDrop = timestamp;
|
||||||
|
if (!this.isPaused && !this.isGameOver)
|
||||||
|
{
|
||||||
|
if (timestamp - this.lastDrop > this.dropInterval)
|
||||||
|
{
|
||||||
|
if (!this.movePiece(0, 1))
|
||||||
|
this.lockPiece();
|
||||||
|
this.lastDrop = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.draw();
|
||||||
|
requestAnimationFrame(this.loop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid() {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
ctx.strokeStyle = '#222';
|
||||||
|
for (let r = 0; r <= ROWS; r++)
|
||||||
|
{
|
||||||
|
// 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() {
|
||||||
|
for (let r = 0; r < ROWS; r++)
|
||||||
|
{
|
||||||
|
for (let c = 0; c < COLS; c++)
|
||||||
|
{
|
||||||
|
const val = this.board[r][c];
|
||||||
|
if (val)
|
||||||
|
this.fillBlock(c, r, COLORS[val]);
|
||||||
|
else
|
||||||
|
this.clearBlock(c, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillBlock(x: number, y: number, color: string) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
|
||||||
|
/*ctx.strokeStyle = '#111';
|
||||||
|
ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBlock(x: number, y: number) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.clearRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHUD() {
|
||||||
|
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() {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.getElementById('board');
|
||||||
|
if (!canvas) {
|
||||||
|
console.error('D:');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const game = new Game('board');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
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"));
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import Aview from "./Aview.ts"
|
import Aview from "./Aview.ts"
|
||||||
|
import { setOnekoState, setBallPos } from "../oneko.ts"
|
||||||
|
|
||||||
export default class extends Aview {
|
export default class extends Aview {
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ export default class extends Aview {
|
|||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setTitle("Tournament");
|
this.setTitle("Tournament");
|
||||||
|
setOnekoState("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHTML() {
|
async getHTML() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user