|
|
|
@ -34,7 +34,7 @@
|
|
|
|
hud.style.cssText =
|
|
|
|
hud.style.cssText =
|
|
|
|
"width:800px;max-width:100%;margin:8px auto;color:#e8eadf;font:16px/1.4 system-ui,sans-serif;display:flex;gap:18px;flex-wrap:wrap;";
|
|
|
|
"width:800px;max-width:100%;margin:8px auto;color:#e8eadf;font:16px/1.4 system-ui,sans-serif;display:flex;gap:18px;flex-wrap:wrap;";
|
|
|
|
hud.innerHTML =
|
|
|
|
hud.innerHTML =
|
|
|
|
'Score: <span id="score">0</span> Lives: <span id="lives">3</span> Enemies: <span id="enemies">0</span>';
|
|
|
|
'Score: <span id="score">0</span> Health: <span id="lives">100%</span> Enemies: <span id="enemies">0</span>';
|
|
|
|
canvas.parentNode.insertBefore(hud, canvas.nextSibling);
|
|
|
|
canvas.parentNode.insertBefore(hud, canvas.nextSibling);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -80,6 +80,8 @@
|
|
|
|
const FIRE_COOLDOWN = 0.34;
|
|
|
|
const FIRE_COOLDOWN = 0.34;
|
|
|
|
const ENEMY_FIRE_COOLDOWN = 1.35;
|
|
|
|
const ENEMY_FIRE_COOLDOWN = 1.35;
|
|
|
|
const BRICK_SIZE = 20;
|
|
|
|
const BRICK_SIZE = 20;
|
|
|
|
|
|
|
|
const PLAYER_MAX_HEALTH = 100;
|
|
|
|
|
|
|
|
const PLAYER_HIT_DAMAGE = 34;
|
|
|
|
const DIRS = {
|
|
|
|
const DIRS = {
|
|
|
|
up: { x: 0, y: -1 },
|
|
|
|
up: { x: 0, y: -1 },
|
|
|
|
down: { x: 0, y: 1 },
|
|
|
|
down: { x: 0, y: 1 },
|
|
|
|
@ -90,7 +92,7 @@
|
|
|
|
const keys = new Set();
|
|
|
|
const keys = new Set();
|
|
|
|
let state = "menu";
|
|
|
|
let state = "menu";
|
|
|
|
let score = 0;
|
|
|
|
let score = 0;
|
|
|
|
let lives = 3;
|
|
|
|
let health = PLAYER_MAX_HEALTH;
|
|
|
|
let player;
|
|
|
|
let player;
|
|
|
|
let enemies = [];
|
|
|
|
let enemies = [];
|
|
|
|
let bullets = [];
|
|
|
|
let bullets = [];
|
|
|
|
@ -147,14 +149,15 @@
|
|
|
|
// Generate a handful of vertical/horizontal steel segments at random tile
|
|
|
|
// Generate a handful of vertical/horizontal steel segments at random tile
|
|
|
|
// positions, skipping any that overlap a spawn keep-out zone.
|
|
|
|
// positions, skipping any that overlap a spawn keep-out zone.
|
|
|
|
const pieces = [];
|
|
|
|
const pieces = [];
|
|
|
|
const count = 3 + Math.floor(Math.random() * 3); // 3-5 steel walls
|
|
|
|
const segmentCount = 5 + Math.floor(Math.random() * 3); // 5-7 steel wall segments
|
|
|
|
const cols = Math.floor(WIDTH / TILE);
|
|
|
|
const cols = Math.floor(WIDTH / TILE);
|
|
|
|
const rows = Math.floor(HEIGHT / TILE);
|
|
|
|
const rows = Math.floor(HEIGHT / TILE);
|
|
|
|
|
|
|
|
let placedSegments = 0;
|
|
|
|
let attempts = 0;
|
|
|
|
let attempts = 0;
|
|
|
|
while (pieces.length < count && attempts < count * 20) {
|
|
|
|
while (placedSegments < segmentCount && attempts < segmentCount * 30) {
|
|
|
|
attempts += 1;
|
|
|
|
attempts += 1;
|
|
|
|
const vertical = Math.random() < 0.5;
|
|
|
|
const vertical = Math.random() < 0.5;
|
|
|
|
const length = (2 + Math.floor(Math.random() * 2)) * TILE; // 2-3 tiles long
|
|
|
|
const length = (2 + Math.floor(Math.random() * 3)) * TILE; // 2-4 tiles long
|
|
|
|
const x = Math.floor(Math.random() * cols) * TILE;
|
|
|
|
const x = Math.floor(Math.random() * cols) * TILE;
|
|
|
|
const y = Math.floor(Math.random() * rows) * TILE;
|
|
|
|
const y = Math.floor(Math.random() * rows) * TILE;
|
|
|
|
const w = vertical ? TILE : length;
|
|
|
|
const w = vertical ? TILE : length;
|
|
|
|
@ -165,6 +168,7 @@
|
|
|
|
if (occupied.some((area) => intersects(candidate, area))) continue;
|
|
|
|
if (occupied.some((area) => intersects(candidate, area))) continue;
|
|
|
|
pieces.push(...steelWall(x, y, w, h));
|
|
|
|
pieces.push(...steelWall(x, y, w, h));
|
|
|
|
occupied.push(candidate);
|
|
|
|
occupied.push(candidate);
|
|
|
|
|
|
|
|
placedSegments += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return pieces;
|
|
|
|
return pieces;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -213,7 +217,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
function resetGame() {
|
|
|
|
function resetGame() {
|
|
|
|
score = 0;
|
|
|
|
score = 0;
|
|
|
|
lives = 3;
|
|
|
|
health = PLAYER_MAX_HEALTH;
|
|
|
|
bullets = [];
|
|
|
|
bullets = [];
|
|
|
|
particles = [];
|
|
|
|
particles = [];
|
|
|
|
regenerateWalls();
|
|
|
|
regenerateWalls();
|
|
|
|
@ -278,7 +282,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
function updateHud() {
|
|
|
|
function updateHud() {
|
|
|
|
scoreEl.textContent = score;
|
|
|
|
scoreEl.textContent = score;
|
|
|
|
livesEl.textContent = lives;
|
|
|
|
livesEl.textContent = `${health}%`;
|
|
|
|
enemiesEl.textContent = enemies.filter((enemy) => enemy.alive).length;
|
|
|
|
enemiesEl.textContent = enemies.filter((enemy) => enemy.alive).length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -445,9 +449,9 @@
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (intersects(bullet, player)) {
|
|
|
|
} else if (intersects(bullet, player)) {
|
|
|
|
lives -= 1;
|
|
|
|
health = Math.max(0, health - PLAYER_HIT_DAMAGE);
|
|
|
|
spark(player.x + player.w / 2, player.y + player.h / 2, "#77b255");
|
|
|
|
spark(player.x + player.w / 2, player.y + player.h / 2, "#77b255");
|
|
|
|
if (lives <= 0) {
|
|
|
|
if (health <= 0) {
|
|
|
|
player.alive = false;
|
|
|
|
player.alive = false;
|
|
|
|
setState("gameOver");
|
|
|
|
setState("gameOver");
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -551,13 +555,13 @@
|
|
|
|
let glowColor, glowBlur, outerColor, innerColor, coreColor;
|
|
|
|
let glowColor, glowBlur, outerColor, innerColor, coreColor;
|
|
|
|
|
|
|
|
|
|
|
|
if (isPlayer) {
|
|
|
|
if (isPlayer) {
|
|
|
|
if (lives >= 3) {
|
|
|
|
if (health > 66) {
|
|
|
|
glowColor = "rgba(132, 238, 212, 0.75)";
|
|
|
|
glowColor = "rgba(132, 238, 212, 0.75)";
|
|
|
|
glowBlur = 12;
|
|
|
|
glowBlur = 12;
|
|
|
|
outerColor = "#e8fff7";
|
|
|
|
outerColor = "#e8fff7";
|
|
|
|
innerColor = "#102822";
|
|
|
|
innerColor = "#102822";
|
|
|
|
coreColor = entity.color;
|
|
|
|
coreColor = entity.color;
|
|
|
|
} else if (lives === 2) {
|
|
|
|
} else if (health > 33) {
|
|
|
|
glowColor = "rgba(240, 182, 70, 0.45)";
|
|
|
|
glowColor = "rgba(240, 182, 70, 0.45)";
|
|
|
|
glowBlur = 8;
|
|
|
|
glowBlur = 8;
|
|
|
|
outerColor = "#c8c0a0";
|
|
|
|
outerColor = "#c8c0a0";
|
|
|
|
@ -585,7 +589,7 @@
|
|
|
|
ctx.fillStyle = "rgba(255,255,255,0.18)";
|
|
|
|
ctx.fillStyle = "rgba(255,255,255,0.18)";
|
|
|
|
ctx.fillRect(-7, -8, 14, 16);
|
|
|
|
ctx.fillRect(-7, -8, 14, 16);
|
|
|
|
|
|
|
|
|
|
|
|
if (isPlayer && lives === 2) {
|
|
|
|
if (isPlayer && health <= 66 && health > 33) {
|
|
|
|
ctx.strokeStyle = "rgba(30, 20, 10, 0.55)";
|
|
|
|
ctx.strokeStyle = "rgba(30, 20, 10, 0.55)";
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.beginPath();
|
|
|
|
@ -601,7 +605,7 @@
|
|
|
|
ctx.fillRect(5, -4, 4, 4);
|
|
|
|
ctx.fillRect(5, -4, 4, 4);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isPlayer && lives <= 1) {
|
|
|
|
if (isPlayer && health <= 33) {
|
|
|
|
ctx.strokeStyle = "rgba(80, 10, 10, 0.6)";
|
|
|
|
ctx.strokeStyle = "rgba(80, 10, 10, 0.6)";
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.beginPath();
|
|
|
|
@ -623,7 +627,7 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = isPlayer ? "#f4fff8" : "#1b0805";
|
|
|
|
ctx.fillStyle = isPlayer ? "#f4fff8" : "#1b0805";
|
|
|
|
if (isPlayer && lives <= 1 && Math.floor(Date.now() / 300) % 2 === 0) {
|
|
|
|
if (isPlayer && health <= 33 && Math.floor(Date.now() / 300) % 2 === 0) {
|
|
|
|
ctx.globalAlpha = 0.3;
|
|
|
|
ctx.globalAlpha = 0.3;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ctx.fillRect(-4, -26, 8, 20);
|
|
|
|
ctx.fillRect(-4, -26, 8, 20);
|
|
|
|
@ -633,7 +637,7 @@
|
|
|
|
ctx.fillRect(-10, -2, 20, 4);
|
|
|
|
ctx.fillRect(-10, -2, 20, 4);
|
|
|
|
ctx.fillStyle = "#79e5c9";
|
|
|
|
ctx.fillStyle = "#79e5c9";
|
|
|
|
ctx.fillRect(-5, -5, 10, 10);
|
|
|
|
ctx.fillRect(-5, -5, 10, 10);
|
|
|
|
if (lives <= 1 && Math.floor(Date.now() / 300) % 2 === 0) {
|
|
|
|
if (health <= 33 && Math.floor(Date.now() / 300) % 2 === 0) {
|
|
|
|
ctx.fillStyle = "rgba(212, 93, 69, 0.7)";
|
|
|
|
ctx.fillStyle = "rgba(212, 93, 69, 0.7)";
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(0, -14);
|
|
|
|
ctx.moveTo(0, -14);
|
|
|
|
|