Files
chrish-slingshot-CrasHUtils/js/snake_game.js
T
2025-10-08 23:10:47 +01:00

258 lines
8.9 KiB
JavaScript

import { app } from "../../scripts/app.js";
const CELL_SIZE = 8;
const GRID_SIZE = 20;
const CANVAS_SIZE = CELL_SIZE * GRID_SIZE;
const GAME_SPEED = 150; // ms per frame, Nokia 3210 speed
class SnakeGame {
constructor(node) {
this.node = node;
this.reset();
}
reset() {
this.snake = [{ x: 10, y: 10 }];
this.direction = { x: 1, y: 0 };
this.nextDirection = { x: 1, y: 0 };
this.food = this.spawnFood();
this.score = 0;
this.gameOver = false;
this.paused = false;
}
spawnFood() {
let food;
do {
food = {
x: Math.floor(Math.random() * GRID_SIZE),
y: Math.floor(Math.random() * GRID_SIZE)
};
} while (this.snake.some(segment => segment.x === food.x && segment.y === food.y));
return food;
}
update() {
if (this.gameOver || this.paused) return;
// Update direction
this.direction = this.nextDirection;
// Calculate new head position
const head = {
x: (this.snake[0].x + this.direction.x + GRID_SIZE) % GRID_SIZE,
y: (this.snake[0].y + this.direction.y + GRID_SIZE) % GRID_SIZE
};
// Check self collision
if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) {
this.gameOver = true;
return;
}
this.snake.unshift(head);
// Check food collision
if (head.x === this.food.x && head.y === this.food.y) {
this.score++;
this.food = this.spawnFood();
} else {
this.snake.pop();
}
}
changeDirection(newDir) {
// Prevent 180 degree turns
if (newDir.x === -this.direction.x && newDir.y === -this.direction.y) {
return;
}
this.nextDirection = newDir;
}
draw(ctx) {
// Clear canvas with Nokia-style background
ctx.fillStyle = '#c7f0d8';
ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
// Draw grid (optional, Nokia style)
ctx.strokeStyle = '#a8d5ba';
ctx.lineWidth = 0.5;
for (let i = 0; i <= GRID_SIZE; i++) {
ctx.beginPath();
ctx.moveTo(i * CELL_SIZE, 0);
ctx.lineTo(i * CELL_SIZE, CANVAS_SIZE);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * CELL_SIZE);
ctx.lineTo(CANVAS_SIZE, i * CELL_SIZE);
ctx.stroke();
}
// Draw snake
ctx.fillStyle = '#000000';
this.snake.forEach(segment => {
ctx.fillRect(
segment.x * CELL_SIZE + 1,
segment.y * CELL_SIZE + 1,
CELL_SIZE - 2,
CELL_SIZE - 2
);
});
// Draw food
ctx.fillStyle = '#000000';
ctx.fillRect(
this.food.x * CELL_SIZE + 2,
this.food.y * CELL_SIZE + 2,
CELL_SIZE - 4,
CELL_SIZE - 4
);
// Draw score
ctx.fillStyle = '#000000';
ctx.font = '10px monospace';
ctx.fillText(`Score: ${this.score}`, 5, CANVAS_SIZE - 5);
// Draw game over
if (this.gameOver) {
ctx.fillStyle = 'rgba(199, 240, 216, 0.8)';
ctx.fillRect(0, CANVAS_SIZE / 2 - 20, CANVAS_SIZE, 40);
ctx.fillStyle = '#000000';
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', CANVAS_SIZE / 2, CANVAS_SIZE / 2 - 5);
ctx.font = '10px monospace';
ctx.fillText('Press SPACE', CANVAS_SIZE / 2, CANVAS_SIZE / 2 + 10);
ctx.textAlign = 'left';
}
}
}
app.registerExtension({
name: "Comfy.SnakeGame",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SnakeGame") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
// Call original onNodeCreated first
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
// Set node properties early to avoid conflicts
this.properties = this.properties || {};
this.serialize_widgets = false;
this.widgets = this.widgets || [];
// Prevent re-initialization
if (this.snakeGameInitialized) {
return result;
}
this.snakeGameInitialized = true;
// Create canvas
const canvas = document.createElement('canvas');
canvas.width = CANVAS_SIZE;
canvas.height = CANVAS_SIZE;
canvas.style.width = `${CANVAS_SIZE * 2}px`;
canvas.style.height = `${CANVAS_SIZE * 2}px`;
canvas.style.imageRendering = 'pixelated';
const ctx = canvas.getContext('2d');
// Initialize game
const game = new SnakeGame(this);
this.snakeGame = game;
// Game loop
let lastUpdate = Date.now();
let animationId = null;
const gameLoop = () => {
const now = Date.now();
if (now - lastUpdate >= GAME_SPEED) {
game.update();
game.draw(ctx);
lastUpdate = now;
}
animationId = requestAnimationFrame(gameLoop);
};
gameLoop();
// Store animation ID for cleanup
this.snakeGameAnimationId = animationId;
// Set node size - account for title bar (~30px) and padding
const nodeWidth = CANVAS_SIZE * 2 + 20;
const nodeHeight = CANVAS_SIZE * 2 + 60; // Extra space for title and padding
this.size = [nodeWidth, nodeHeight];
this.setSize([nodeWidth, nodeHeight]);
// Add canvas widget
const widget = this.addDOMWidget("snake_canvas", "canvas", canvas, {
serialize: false,
hideOnZoom: false
});
// Handle keyboard input
const handleKeyDown = (e) => {
if (!this.snakeGame) return;
switch(e.key) {
case 'ArrowUp':
case 'w':
case 'W':
e.preventDefault();
this.snakeGame.changeDirection({ x: 0, y: -1 });
break;
case 'ArrowDown':
case 's':
case 'S':
e.preventDefault();
this.snakeGame.changeDirection({ x: 0, y: 1 });
break;
case 'ArrowLeft':
case 'a':
case 'A':
e.preventDefault();
this.snakeGame.changeDirection({ x: -1, y: 0 });
break;
case 'ArrowRight':
case 'd':
case 'D':
e.preventDefault();
this.snakeGame.changeDirection({ x: 1, y: 0 });
break;
case ' ':
e.preventDefault();
if (this.snakeGame.gameOver) {
this.snakeGame.reset();
} else {
this.snakeGame.paused = !this.snakeGame.paused;
}
break;
}
};
// Add event listeners when node is selected
canvas.tabIndex = 0;
canvas.addEventListener('keydown', handleKeyDown);
canvas.addEventListener('click', () => canvas.focus());
// Auto-focus when created
setTimeout(() => canvas.focus(), 100);
return result;
};
// Cleanup on removal
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function() {
if (this.snakeGameAnimationId) {
cancelAnimationFrame(this.snakeGameAnimationId);
}
if (onRemoved) {
onRemoved.apply(this, arguments);
}
};
}
}
});