diff --git a/public/js/games/snake.js b/public/js/games/snake.js new file mode 100644 index 0000000..8c529c7 --- /dev/null +++ b/public/js/games/snake.js @@ -0,0 +1,185 @@ +/* ============================================================ + SNAKE PROTOCOL — Snake, spaceport energy theme + ============================================================ */ +(function () { + 'use strict'; + const { Game, STATE } = window.DSP; + const S = window.DSP_SPRITES; + + const W = 800, H = 600; + const CELL = 20, COLS = 36, ROWS = 24; + const OX = (W - COLS * CELL) / 2, OY = (H - ROWS * CELL) / 2; + + class SnakeProtocol extends Game { + constructor(canvas) { + super({ name: 'snake', title: 'SNAKE PROTOCOL', width: W, height: H, canvas, + loadSteps: ['BOOTING GRID', 'CHARGING CELLS', 'SYNCING SERPENT', 'SYNCING GRID'] }); + this.grid = null; + this.snake = []; + this.dir = { x: 1, y: 0 }; + this.nextDir = { x: 1, y: 0 }; + this.food = null; + this.special = null; + this.tickRate = 8; + this.tickAcc = 0; + this.grow = 0; + this.slowTimer = 0; + this.ghostTimer = 0; + this.stars = Array.from({ length: 50 }, () => ({ x: Math.random() * W, y: Math.random() * H, s: Math.random() < 0.2 ? 2 : 1 })); + } + reset() { + this.snake = [{ x: 8, y: 12 }, { x: 7, y: 12 }, { x: 6, y: 12 }]; + this.dir = { x: 1, y: 0 }; + this.nextDir = { x: 1, y: 0 }; + this.grow = 0; + this.slowTimer = 0; + this.ghostTimer = 0; + this.tickRate = 8; + this.placeFood(); + this.special = null; + } + placeFood() { + let p; + do { + p = { x: Math.floor(Math.random() * COLS), y: Math.floor(Math.random() * ROWS) }; + } while (this.snake.some(s => s.x === p.x && s.y === p.y)); + this.food = p; + } + placeSpecial() { + let p; + do { + p = { x: Math.floor(Math.random() * COLS), y: Math.floor(Math.random() * ROWS) }; + } while (this.snake.some(s => s.x === p.x && s.y === p.y) || (this.food && this.food.x === p.x && this.food.y === p.y)); + this.special = { ...p, kind: Math.random() < 0.5 ? 'bonus' : 'ghost', life: 480 }; + } + update() { + const i = this.input; + if (i.wasPressed('ArrowUp', 'KeyW') && this.dir.y === 0) this.nextDir = { x: 0, y: -1 }; + else if (i.wasPressed('ArrowDown', 'KeyS') && this.dir.y === 0) this.nextDir = { x: 0, y: 1 }; + else if (i.wasPressed('ArrowLeft', 'KeyA') && this.dir.x === 0) this.nextDir = { x: -1, y: 0 }; + else if (i.wasPressed('ArrowRight', 'KeyD') && this.dir.x === 0) this.nextDir = { x: 1, y: 0 }; + + if (this.slowTimer > 0) this.slowTimer--; + if (this.ghostTimer > 0) this.ghostTimer--; + if (this.special) { + this.special.life--; + if (this.special.life <= 0) this.special = null; + } + + const rate = this.slowTimer > 0 ? this.tickRate + 4 : this.tickRate; + this.tickAcc++; + if (this.tickAcc < rate) return; + this.tickAcc = 0; + + this.dir = this.nextDir; + const head = { x: this.snake[0].x + this.dir.x, y: this.snake[0].y + this.dir.y }; + + if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) { + window.DSP_AUDIO.sfx('bigboom'); + this.particles.burst(OX + head.x * CELL + CELL / 2, OY + head.y * CELL + CELL / 2, '#ff4d6d', 20, 3, 30); + this.shake.trigger(6, 20); + this.lives--; + if (this.lives <= 0) { this.gameOver(); return; } + this.reset(); + return; + } + if (this.ghostTimer <= 0 && this.snake.some(s => s.x === head.x && s.y === head.y)) { + window.DSP_AUDIO.sfx('bigboom'); + this.particles.burst(OX + head.x * CELL + CELL / 2, OY + head.y * CELL + CELL / 2, '#ff4d6d', 20, 3, 30); + this.shake.trigger(6, 20); + this.lives--; + if (this.lives <= 0) { this.gameOver(); return; } + this.reset(); + return; + } + + this.snake.unshift(head); + + if (this.food && head.x === this.food.x && head.y === this.food.y) { + this.addScore(10); + window.DSP_AUDIO.sfx('eat'); + this.grow += 2; + this.placeFood(); + if (this.tickRate > 4 && this.score % 50 === 0) { + this.tickRate--; + this.level = Math.floor(this.score / 100) + 1; + window.DSP_AUDIO.sfx('levelup'); + } + if (Math.random() < 0.25 && !this.special) this.placeSpecial(); + } + if (this.special && head.x === this.special.x && head.y === this.special.y) { + if (this.special.kind === 'bonus') { + this.addScore(50); + window.DSP_AUDIO.sfx('bonus'); + this.particles.burst(OX + head.x * CELL + CELL / 2, OY + head.y * CELL + CELL / 2, '#ff9d2d', 16, 3, 30); + } else { + this.ghostTimer = 300; + window.DSP_AUDIO.sfx('powerup'); + } + this.special = null; + } + + if (this.grow > 0) this.grow--; + else this.snake.pop(); + } + render() { + const ctx = this.ctx; + ctx.fillStyle = '#05010d'; + ctx.fillRect(0, 0, W, H); + for (const s of this.stars) { + ctx.fillStyle = s.s > 1 ? '#7df9ff' : '#2a2145'; + ctx.fillRect(s.x, s.y, s.s, s.s); + } + ctx.strokeStyle = '#3a2f5b'; + ctx.lineWidth = 2; + ctx.strokeRect(OX - 2, OY - 2, COLS * CELL + 4, ROWS * CELL + 4); + ctx.strokeStyle = 'rgba(58,47,91,0.25)'; + ctx.lineWidth = 1; + for (let x = 0; x <= COLS; x++) { + ctx.beginPath(); ctx.moveTo(OX + x * CELL, OY); ctx.lineTo(OX + x * CELL, OY + ROWS * CELL); ctx.stroke(); + } + for (let y = 0; y <= ROWS; y++) { + ctx.beginPath(); ctx.moveTo(OX, OY + y * CELL); ctx.lineTo(OX + COLS * CELL, OY + y * CELL); ctx.stroke(); + } + if (this.food) { + const fx = OX + this.food.x * CELL, fy = OY + this.food.y * CELL; + const pulse = 1 + Math.sin(this.time * 0.15) * 0.15; + ctx.fillStyle = '#ffe14d'; + ctx.fillRect(fx + CELL / 2 - 5 * pulse, fy + CELL / 2 - 5 * pulse, 10 * pulse, 10 * pulse); + } + if (this.special) { + const sx = OX + this.special.x * CELL, sy = OY + this.special.y * CELL; + const blink = this.special.life < 120 && Math.floor(this.time / 6) % 2 === 0; + if (!blink) { + ctx.fillStyle = this.special.kind === 'bonus' ? '#ff9d2d' : '#7df9ff'; + ctx.fillRect(sx + 3, sy + 3, CELL - 6, CELL - 6); + ctx.fillStyle = '#05010d'; + ctx.font = '10px "Press Start 2P", monospace'; + ctx.textAlign = 'center'; + ctx.fillText(this.special.kind === 'bonus' ? 'B' : 'G', sx + CELL / 2, sy + CELL / 2 + 3); + } + } + const ghost = this.ghostTimer > 0; + for (let i = this.snake.length - 1; i >= 0; i--) { + const s = this.snake[i]; + const x = OX + s.x * CELL, y = OY + s.y * CELL; + if (i === 0) { + ctx.fillStyle = ghost ? 'rgba(125,249,255,0.5)' : '#7dff5e'; + ctx.fillRect(x + 1, y + 1, CELL - 2, CELL - 2); + ctx.fillStyle = '#05010d'; + const ex = this.dir.x !== 0 ? (this.dir.x > 0 ? 12 : 4) : 6; + const ey = this.dir.y !== 0 ? (this.dir.y > 0 ? 12 : 4) : 6; + ctx.fillRect(x + ex, y + ey, 3, 3); + ctx.fillRect(x + (CELL - 6 - ex), y + ey, 3, 3); + } else { + const t = i / this.snake.length; + ctx.fillStyle = ghost ? `rgba(125,249,255,${0.5 - t * 0.3})` : `rgba(125,255,94,${1 - t * 0.5})`; + ctx.fillRect(x + 2, y + 2, CELL - 4, CELL - 4); + } + } + } + } + + window.DSP_ACTIVE_GAME = new SnakeProtocol(document.getElementById('game')); + window.DSP_AUDIO.playMusic('snake'); +})(); \ No newline at end of file