185 lines
7.3 KiB
JavaScript
185 lines
7.3 KiB
JavaScript
/* ============================================================
|
|
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');
|
|
})(); |