Add zone-specific arena theming with dynamic background colors and walking shimmer effects
This commit is contained in:
parent
c66b05e5e0
commit
b7691350e8
|
|
@ -5,7 +5,7 @@ const PROXIMITY_SHARD_PX = 250; // travel distance for proximity explosion
|
||||||
const DEATH_SHARD_PX = 200; // travel distance for on-death explosion
|
const DEATH_SHARD_PX = 200; // travel distance for on-death explosion
|
||||||
const SHARD_COUNT = 8;
|
const SHARD_COUNT = 8;
|
||||||
const SHARD_SPEED = 360; // px/s baseline (overridden per explosion type)
|
const SHARD_SPEED = 360; // px/s baseline (overridden per explosion type)
|
||||||
const SHARD_DAMAGE = 15;
|
const SHARD_DAMAGE = 25;
|
||||||
const SHARD_DURATION = 700; // ms until shards disappear
|
const SHARD_DURATION = 700; // ms until shards disappear
|
||||||
const SHARD_HIT_RADIUS = 18; // px — how close a shard must be to damage player
|
const SHARD_HIT_RADIUS = 18; // px — how close a shard must be to damage player
|
||||||
const TICK_MS = 16;
|
const TICK_MS = 16;
|
||||||
|
|
@ -118,6 +118,7 @@ export class BomberEnemy extends BaseEnemy {
|
||||||
elapsed += TICK_MS;
|
elapsed += TICK_MS;
|
||||||
const dt = TICK_MS / 1000;
|
const dt = TICK_MS / 1000;
|
||||||
const fade = Math.max(0, 1 - elapsed / SHARD_DURATION);
|
const fade = Math.max(0, 1 - elapsed / SHARD_DURATION);
|
||||||
|
const enemies = scene.waveManager.enemies;
|
||||||
|
|
||||||
for (const shard of shards) {
|
for (const shard of shards) {
|
||||||
if (!shard.alive) continue;
|
if (!shard.alive) continue;
|
||||||
|
|
@ -139,6 +140,21 @@ export class BomberEnemy extends BaseEnemy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enemy hit (friendly fire)
|
||||||
|
let hitEnemy = false;
|
||||||
|
for (const enemy of enemies) {
|
||||||
|
if (!enemy.active) continue;
|
||||||
|
const d = Phaser.Math.Distance.Between(shard.obj.x, shard.obj.y, enemy.x, enemy.y);
|
||||||
|
if (d < SHARD_HIT_RADIUS + enemy.radius) {
|
||||||
|
enemy.takeDamage(SHARD_DAMAGE);
|
||||||
|
shard.alive = false;
|
||||||
|
shard.obj.destroy();
|
||||||
|
hitEnemy = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hitEnemy) continue;
|
||||||
|
|
||||||
// Interior barrier hit
|
// Interior barrier hit
|
||||||
if (barriers?.destroyInteriorBarrierAt(shard.obj.x, shard.obj.y)) {
|
if (barriers?.destroyInteriorBarrierAt(shard.obj.x, shard.obj.y)) {
|
||||||
shard.alive = false;
|
shard.alive = false;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,16 @@ import { SkillTreeUI } from '../ui/SkillTreeUI.js';
|
||||||
import { Reticle } from '../ui/Reticle.js';
|
import { Reticle } from '../ui/Reticle.js';
|
||||||
import { BarrierManager } from '../systems/BarrierManager.js';
|
import { BarrierManager } from '../systems/BarrierManager.js';
|
||||||
|
|
||||||
|
const ZONE_PALETTE = [
|
||||||
|
null, // index 0 unused
|
||||||
|
{ bg: 0x111118, grid: 0x222233, gridAlpha: 0.50, border: 0x334466, shimmer: null }, // zone 1 — dark blue-grey (unchanged)
|
||||||
|
{ bg: 0x080d28, grid: 0x1a3488, gridAlpha: 0.70, border: 0x2255dd, shimmer: 0x00eeff }, // zone 2 — deep navy
|
||||||
|
{ bg: 0x0e0828, grid: 0x2a1888, gridAlpha: 0.70, border: 0x6633cc, shimmer: 0x8833ff }, // zone 3 — indigo
|
||||||
|
{ bg: 0x160828, grid: 0x420aaa, gridAlpha: 0.70, border: 0x9922dd, shimmer: 0xff00ff }, // zone 4 — deep violet
|
||||||
|
{ bg: 0x1e0820, grid: 0x5a0e55, gridAlpha: 0.70, border: 0xcc22aa, shimmer: 0xff2266 }, // zone 5 — dark magenta
|
||||||
|
{ bg: 0x1e0e06, grid: 0x552208, gridAlpha: 0.70, border: 0xdd5511, shimmer: 0xff7700 }, // zone 6 — burnt amber
|
||||||
|
];
|
||||||
|
|
||||||
export class GameScene extends Phaser.Scene {
|
export class GameScene extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: 'GameScene' });
|
super({ key: 'GameScene' });
|
||||||
|
|
@ -69,6 +79,9 @@ export class GameScene extends Phaser.Scene {
|
||||||
this._frozen = false;
|
this._frozen = false;
|
||||||
this._waitingForExit = false;
|
this._waitingForExit = false;
|
||||||
this._pulseEffects = [];
|
this._pulseEffects = [];
|
||||||
|
this._shimmers = [];
|
||||||
|
this._shimmerTimer = 0;
|
||||||
|
this._shimmerSpawnMs = 1500;
|
||||||
|
|
||||||
this.events.on('wave-start', () => this.sound.play('sfx-enemy-wave', { volume: 0.5 }));
|
this.events.on('wave-start', () => this.sound.play('sfx-enemy-wave', { volume: 0.5 }));
|
||||||
this.events.on('zone-waves-complete', () => this._startZoneExit());
|
this.events.on('zone-waves-complete', () => this._startZoneExit());
|
||||||
|
|
@ -111,18 +124,107 @@ export class GameScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawArena(W, H) {
|
_drawArena(W, H) {
|
||||||
// Dark background
|
this._arenaBg = this.add.rectangle(W / 2, H / 2, W, H, 0x111118);
|
||||||
this.add.rectangle(W / 2, H / 2, W, H, 0x111118);
|
this._arenaGrid = this.add.graphics();
|
||||||
|
const z1 = ZONE_PALETTE[1];
|
||||||
|
this._drawArenaGrid(z1.grid, z1.gridAlpha, z1.border);
|
||||||
|
}
|
||||||
|
|
||||||
// Grid lines for depth cue
|
_drawArenaGrid(gridColor, gridAlpha, borderColor) {
|
||||||
const g = this.add.graphics();
|
const W = this.scale.width;
|
||||||
g.lineStyle(1, 0x222233, 0.5);
|
const H = this.scale.height;
|
||||||
for (let x = 0; x <= W; x += 80) { g.lineBetween(x, 0, x, H); }
|
this._arenaGrid.clear();
|
||||||
for (let y = 0; y <= H; y += 80) { g.lineBetween(0, y, W, y); }
|
this._arenaGrid.lineStyle(1, gridColor, gridAlpha);
|
||||||
|
for (let x = 0; x <= W; x += 80) { this._arenaGrid.lineBetween(x, 0, x, H); }
|
||||||
|
for (let y = 0; y <= H; y += 80) { this._arenaGrid.lineBetween(0, y, W, y); }
|
||||||
|
this._arenaGrid.lineStyle(3, borderColor, 1);
|
||||||
|
this._arenaGrid.strokeRect(2, 2, W - 4, H - 4);
|
||||||
|
}
|
||||||
|
|
||||||
// Arena border
|
_updateArenaColors(zoneNum) {
|
||||||
g.lineStyle(3, 0x334466, 1);
|
const palette = ZONE_PALETTE[zoneNum] ?? ZONE_PALETTE[ZONE_PALETTE.length - 1];
|
||||||
g.strokeRect(2, 2, W - 4, H - 4);
|
this._arenaBg.setFillStyle(palette.bg);
|
||||||
|
this._drawArenaGrid(palette.grid, palette.gridAlpha, palette.border);
|
||||||
|
}
|
||||||
|
|
||||||
|
_spawnShimmer() {
|
||||||
|
const zoneNum = this.waveManager.zoneNum;
|
||||||
|
if (zoneNum < 2) return;
|
||||||
|
if (this._shimmers.length >= 2) return;
|
||||||
|
const palette = ZONE_PALETTE[Math.min(zoneNum, ZONE_PALETTE.length - 1)];
|
||||||
|
if (!palette?.shimmer) return;
|
||||||
|
|
||||||
|
const COLS = Math.floor(this.scale.width / 80);
|
||||||
|
const ROWS = Math.floor(this.scale.height / 80);
|
||||||
|
const col = Phaser.Math.Between(0, COLS - 1);
|
||||||
|
const row = Phaser.Math.Between(0, ROWS - 1);
|
||||||
|
|
||||||
|
const rect = this.add.rectangle(
|
||||||
|
col * 80 + 40, row * 80 + 40, 80, 80, palette.shimmer, 0
|
||||||
|
).setDepth(1);
|
||||||
|
|
||||||
|
this._shimmers.push({
|
||||||
|
rect, col, row,
|
||||||
|
stepTimer: 0,
|
||||||
|
stepMs: Phaser.Math.Between(250, 400),
|
||||||
|
stepsLeft: Phaser.Math.Between(5, 9),
|
||||||
|
phase: 'fadein',
|
||||||
|
phaseTimer: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateShimmers(delta) {
|
||||||
|
if (this.waveManager.zoneNum < 2) return;
|
||||||
|
|
||||||
|
this._shimmerTimer += delta;
|
||||||
|
if (this._shimmerTimer >= this._shimmerSpawnMs) {
|
||||||
|
this._shimmerTimer = 0;
|
||||||
|
this._shimmerSpawnMs = Phaser.Math.Between(3000, 6000);
|
||||||
|
this._spawnShimmer();
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLS = Math.floor(this.scale.width / 80);
|
||||||
|
const ROWS = Math.floor(this.scale.height / 80);
|
||||||
|
const TARGET_ALPHA = 0.22;
|
||||||
|
const FADE_IN_MS = 150;
|
||||||
|
const FADE_OUT_MS = 350;
|
||||||
|
|
||||||
|
this._shimmers = this._shimmers.filter(s => {
|
||||||
|
s.phaseTimer += delta;
|
||||||
|
|
||||||
|
if (s.phase === 'fadein') {
|
||||||
|
s.rect.fillAlpha = Math.min(1, s.phaseTimer / FADE_IN_MS) * TARGET_ALPHA;
|
||||||
|
if (s.phaseTimer >= FADE_IN_MS) {
|
||||||
|
s.phase = 'walk';
|
||||||
|
s.phaseTimer = 0;
|
||||||
|
s.stepTimer = 0;
|
||||||
|
}
|
||||||
|
} else if (s.phase === 'walk') {
|
||||||
|
s.stepTimer += delta;
|
||||||
|
if (s.stepTimer >= s.stepMs) {
|
||||||
|
s.stepTimer = 0;
|
||||||
|
s.stepsLeft--;
|
||||||
|
if (s.stepsLeft <= 0) {
|
||||||
|
s.phase = 'fadeout';
|
||||||
|
s.phaseTimer = 0;
|
||||||
|
} else {
|
||||||
|
const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
|
||||||
|
const [dc, dr] = dirs[Phaser.Math.Between(0, 3)];
|
||||||
|
s.col = Phaser.Math.Clamp(s.col + dc, 0, COLS - 1);
|
||||||
|
s.row = Phaser.Math.Clamp(s.row + dr, 0, ROWS - 1);
|
||||||
|
s.rect.setPosition(s.col * 80 + 40, s.row * 80 + 40);
|
||||||
|
s.stepMs = Phaser.Math.Between(250, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (s.phase === 'fadeout') {
|
||||||
|
s.rect.fillAlpha = Math.max(0, (1 - s.phaseTimer / FADE_OUT_MS)) * TARGET_ALPHA;
|
||||||
|
if (s.phaseTimer >= FADE_OUT_MS) {
|
||||||
|
s.rect.destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update(time, delta) {
|
update(time, delta) {
|
||||||
|
|
@ -138,6 +240,7 @@ export class GameScene extends Phaser.Scene {
|
||||||
this._checkEnemyProjectileHits();
|
this._checkEnemyProjectileHits();
|
||||||
this._pruneEnemyProjectiles();
|
this._pruneEnemyProjectiles();
|
||||||
this._updatePulseEffects(delta);
|
this._updatePulseEffects(delta);
|
||||||
|
this._updateShimmers(delta);
|
||||||
|
|
||||||
if (this._waitingForExit) this._checkPlayerExit();
|
if (this._waitingForExit) this._checkPlayerExit();
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +433,11 @@ export class GameScene extends Phaser.Scene {
|
||||||
this.waveManager.startNextZone();
|
this.waveManager.startNextZone();
|
||||||
// Set up barriers AFTER zone index is incremented so we read the new zone's config
|
// Set up barriers AFTER zone index is incremented so we read the new zone's config
|
||||||
if (this.waveManager.currentZone) this._setupBarriers();
|
if (this.waveManager.currentZone) this._setupBarriers();
|
||||||
|
this._updateArenaColors(this.waveManager.zoneNum);
|
||||||
|
this._shimmers.forEach(s => s.rect?.destroy());
|
||||||
|
this._shimmers = [];
|
||||||
|
this._shimmerTimer = 0;
|
||||||
|
this._shimmerSpawnMs = 1500;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Level up ───────────────────────────────────────────────────────────────
|
// ── Level up ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -395,5 +503,7 @@ export class GameScene extends Phaser.Scene {
|
||||||
this._bgMusic = null;
|
this._bgMusic = null;
|
||||||
this._enemyProjectiles = [];
|
this._enemyProjectiles = [];
|
||||||
this._pulseEffects = [];
|
this._pulseEffects = [];
|
||||||
|
this._shimmers?.forEach(s => s.rect?.destroy());
|
||||||
|
this._shimmers = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue