」 feat(src.front): :D

This commit is contained in:
y-syo
2025-10-08 22:08:03 +02:00
parent b6c6564dd3
commit 7d70921297
14 changed files with 1152 additions and 13 deletions

View File

@ -1,5 +1,7 @@
import Aview from "./Aview.ts"
import { isLogged } from "../main.js"
import { dragElement } from "./drag.js"
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts"
export default class extends Aview {
@ -10,13 +12,14 @@ export default class extends Aview {
super();
this.setTitle("pong (local match)");
this.running = true;
setOnekoState("default");
}
async getHTML() {
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 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>
@ -34,14 +37,16 @@ export default class extends Aview {
<button id="game-start" class="default-button">play</button>
</div>
<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>
<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>
<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 start: number = 0;
let elapsed: number;
@ -128,6 +133,7 @@ export default class extends Aview {
// scoring
if (ballX < 0 || ballX > canvas.width - ballSize)
{
setOnekoState("default");
game_playing = false;
if (ballX < 0)
p2_score++;
@ -158,6 +164,7 @@ export default class extends Aview {
leftPaddleY = canvas.height / 2 - paddleHeight / 2;
rightPaddleY = canvas.height / 2 - paddleHeight / 2;
}
setBallPos(ballX, ballY);
}
function draw() {
@ -235,6 +242,7 @@ export default class extends Aview {
document.getElementById("game-retry")?.addEventListener("click", () => {
setOnekoState("pong");
document.getElementById("game-buttons").classList.add("hidden");
game_playing = false;
match_over = false;
@ -273,6 +281,8 @@ export default class extends Aview {
ballX = canvas.width / 2;
ballY = canvas.height / 2;
setOnekoState("pong");
setOnekoOffset();
requestAnimationFrame(gameLoop);
});
}

View File

@ -1,4 +1,5 @@
import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
export default class extends Aview {
@ -7,6 +8,7 @@ export default class extends Aview {
{
super();
this.setTitle("login");
setOnekoState("default");
}
async getHTML() {

View File

@ -1,4 +1,5 @@
import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts"
export default class extends Aview {
@ -6,12 +7,13 @@ export default class extends Aview {
{
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="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>
@ -26,7 +28,7 @@ export default class extends Aview {
Pong
</a>
</div>
</div>
</div-->
`;
}
}

View File

@ -1,4 +1,6 @@
import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { setOnekoState } from "../oneko.ts"
export default class extends Aview {
@ -6,12 +8,13 @@ export default class extends Aview {
{
super();
this.setTitle("knl is trans(cendence)");
setOnekoState("default");
}
async getHTML() {
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">
<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>
@ -20,7 +23,7 @@ export default class extends Aview {
</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">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">
<a class="default-button" href="/pong/local" data-link>
local match
@ -33,4 +36,7 @@ export default class extends Aview {
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
}
}

View File

@ -1,4 +1,5 @@
import Aview from "./Aview.ts"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
export default class extends Aview {
@ -7,6 +8,7 @@ export default class extends Aview {
{
super();
this.setTitle("register");
setOnekoState("default");
}
async getHTML() {

View 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);
}
});
}
}

View 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"));
}
}

View File

@ -1,4 +1,5 @@
import Aview from "./Aview.ts"
import { setOnekoState, setBallPos } from "../oneko.ts"
export default class extends Aview {
@ -6,6 +7,7 @@ export default class extends Aview {
{
super();
this.setTitle("Tournament");
setOnekoState("default");
}
async getHTML() {

View 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;
}
}