Add Zones 3-6 with new enemy types (SprayerEnemy, BomberEnemy) and enhanced barrier interaction

This commit is contained in:
Brian Fertig 2026-03-07 17:55:19 -07:00
parent 0a24272c88
commit 303956ae4d
7 changed files with 315 additions and 4 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

View File

@ -17,5 +17,48 @@
{ "enemies": [{ "type": "ChaseEnemy", "count": 5 }, { "type": "ShooterEnemy", "count": 3 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 6 }, { "type": "ShooterEnemy", "count": 4 }, { "type": "SwarmerEnemy", "count": 10 }] }
]
},
{
"id": 3,
"interiorBarriers": 3,
"waves": [
{ "enemies": [{ "type": "ChaseEnemy", "count": 4 }, { "type": "SwarmerEnemy", "count": 6 }, { "type": "SprayerEnemy", "count": 2 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 3 }, { "type": "SwarmerEnemy", "count": 8 }, { "type": "BomberEnemy", "count": 2 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 4 }, { "type": "ShooterEnemy", "count": 2 }, { "type": "SprayerEnemy", "count": 2 }, { "type": "BomberEnemy", "count": 1 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 5 }, { "type": "SprayerEnemy", "count": 3 }, { "type": "BomberEnemy", "count": 3 }, { "type": "SwarmerEnemy", "count": 8 }] }
]
},
{
"id": 4,
"interiorBarriers": 4,
"waves": [
{ "enemies": [{ "type": "ChaseEnemy", "count": 6 }, { "type": "SwarmerEnemy", "count": 10 }, { "type": "ShooterEnemy", "count": 2 }, { "type": "SprayerEnemy", "count": 2 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 4 }, { "type": "ShooterEnemy", "count": 5 }, { "type": "BomberEnemy", "count": 3 }, { "type": "SwarmerEnemy", "count": 8 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 8 }, { "type": "SprayerEnemy", "count": 3 }, { "type": "BomberEnemy", "count": 4 }] },
{ "enemies": [{ "type": "ShooterEnemy", "count": 5 }, { "type": "SprayerEnemy", "count": 4 }, { "type": "SwarmerEnemy", "count": 12 }, { "type": "ChaseEnemy", "count": 3 }] },
{ "enemies": [{ "type": "BomberEnemy", "count": 5 }, { "type": "SprayerEnemy", "count": 4 }, { "type": "ShooterEnemy", "count": 5 }, { "type": "ChaseEnemy", "count": 5 }] }
]
},
{
"id": 5,
"interiorBarriers": 5,
"waves": [
{ "enemies": [{ "type": "ChaseEnemy", "count": 8 }, { "type": "SwarmerEnemy", "count": 12 }, { "type": "ShooterEnemy", "count": 4 }, { "type": "SprayerEnemy", "count": 3 }] },
{ "enemies": [{ "type": "ShooterEnemy", "count": 5 }, { "type": "SprayerEnemy", "count": 5 }, { "type": "BomberEnemy", "count": 5 }, { "type": "SwarmerEnemy", "count": 10 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 7 }, { "type": "SprayerEnemy", "count": 5 }, { "type": "BomberEnemy", "count": 4 }, { "type": "ShooterEnemy", "count": 3 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 10 }, { "type": "ShooterEnemy", "count": 6 }, { "type": "SprayerEnemy", "count": 4 }, { "type": "BomberEnemy", "count": 5 }] },
{ "enemies": [{ "type": "BomberEnemy", "count": 6 }, { "type": "SprayerEnemy", "count": 6 }, { "type": "ShooterEnemy", "count": 5 }, { "type": "SwarmerEnemy", "count": 15 }] }
]
},
{
"id": 6,
"interiorBarriers": 5,
"waves": [
{ "enemies": [{ "type": "ChaseEnemy", "count": 10 }, { "type": "SwarmerEnemy", "count": 15 }, { "type": "ShooterEnemy", "count": 5 }, { "type": "SprayerEnemy", "count": 4 }] },
{ "enemies": [{ "type": "ShooterEnemy", "count": 8 }, { "type": "BomberEnemy", "count": 6 }, { "type": "SwarmerEnemy", "count": 15 }, { "type": "ChaseEnemy", "count": 5 }] },
{ "enemies": [{ "type": "SprayerEnemy", "count": 8 }, { "type": "BomberEnemy", "count": 6 }, { "type": "ChaseEnemy", "count": 8 }] },
{ "enemies": [{ "type": "ChaseEnemy", "count": 10 }, { "type": "ShooterEnemy", "count": 8 }, { "type": "SprayerEnemy", "count": 6 }, { "type": "BomberEnemy", "count": 5 }] },
{ "enemies": [{ "type": "BomberEnemy", "count": 8 }, { "type": "SprayerEnemy", "count": 8 }, { "type": "ShooterEnemy", "count": 8 }, { "type": "ChaseEnemy", "count": 8 }, { "type": "SwarmerEnemy", "count": 5 }] }
]
}
]

View File

@ -0,0 +1,142 @@
import { BaseEnemy } from './BaseEnemy.js';
const EXPLODE_RADIUS = 180; // px — triggers explosion when this close to player
const SHARD_COUNT = 8;
const SHARD_SPEED = 360; // px/s — travels ~250px over SHARD_DURATION
const SHARD_DAMAGE = 15;
const SHARD_DURATION = 700; // ms until shards disappear
const SHARD_HIT_RADIUS = 18; // px — how close a shard must be to damage player
const TICK_MS = 16;
export class BomberEnemy extends BaseEnemy {
constructor(scene, x, y, player) {
super(scene, x, y, player, {
frameOffset: 12,
radius: 10,
hp: 20,
speed: 130,
xp: 20,
contactDamage: 5, // low — explosion is the main attack
});
this._offsetAngle = Math.random() * Math.PI * 2;
this._wobble = 0.3 + Math.random() * 0.4;
}
update(delta) {
super.update(delta);
if (this._dying) return;
// Swarm-like wobble movement
const t = this.scene.time.now * 0.001;
const baseAngle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
const angle = baseAngle + Math.sin(t * 4 + this._offsetAngle) * this._wobble;
this.sprite.body.setVelocity(Math.cos(angle) * this.speed, Math.sin(angle) * this.speed);
// Proximity check — explode when close enough
const dist = Phaser.Math.Distance.Between(this.x, this.y, this.player.x, this.player.y);
if (dist < EXPLODE_RADIUS) {
this._triggerExplosion();
}
}
_triggerExplosion() {
if (this._dying) return;
this._spawnShards();
this._die();
}
_spawnShards() {
const scene = this.scene;
const ox = this.x;
const oy = this.y;
const player = this.player;
const barriers = scene.barrierManager;
// Burst flash at explosion center
const flash = scene.add.circle(ox, oy, 36, 0xff6600, 0.9).setDepth(25);
scene.tweens.add({
targets: flash, alpha: 0, scaleX: 2.5, scaleY: 2.5,
duration: 320, onComplete: () => flash.destroy(),
});
const ring = scene.add.circle(ox, oy, 42, 0xffaa00, 0).setDepth(25)
.setStrokeStyle(3, 0xffaa00, 0.9);
scene.tweens.add({
targets: ring, scaleX: 2.2, scaleY: 2.2, alpha: 0,
duration: 400, onComplete: () => ring.destroy(),
});
scene.cameras.main.shake(180, 0.01);
// Build shards: each is a Phaser Triangle game object
const shards = [];
for (let i = 0; i < SHARD_COUNT; i++) {
const angle = (i / SHARD_COUNT) * Math.PI * 2;
// Triangle pointing "up" in local space: tip at top, base at bottom
const tri = scene.add.triangle(
ox, oy,
0, -10, // tip
7, 7, // base-right
-7, 7, // base-left
0xff4400,
).setDepth(22).setRotation(angle); // rotate to face travel direction
// Add a neon outline
tri.setStrokeStyle(1, 0xffaa00);
shards.push({
obj: tri,
vx: Math.cos(angle) * SHARD_SPEED,
vy: Math.sin(angle) * SHARD_SPEED,
rotSpeed: (Math.random() > 0.5 ? 1 : -1) * (6 + Math.random() * 6), // rad/s
alive: true,
});
}
// Manual update loop — avoids tween onComplete reliability issues
let elapsed = 0;
const playerDamaged = new Set();
const ticker = scene.time.addEvent({
delay: TICK_MS,
repeat: Math.ceil(SHARD_DURATION / TICK_MS),
callback: () => {
elapsed += TICK_MS;
const dt = TICK_MS / 1000;
const fade = Math.max(0, 1 - elapsed / SHARD_DURATION);
for (const shard of shards) {
if (!shard.alive) continue;
shard.obj.x += shard.vx * dt;
shard.obj.y += shard.vy * dt;
shard.obj.rotation += shard.rotSpeed * dt;
shard.obj.alpha = 0.4 + fade * 0.6;
// Player hit
if (!playerDamaged.has(shard)) {
const d = Phaser.Math.Distance.Between(shard.obj.x, shard.obj.y, player.x, player.y);
if (d < SHARD_HIT_RADIUS + 16) {
player.takeDamage(SHARD_DAMAGE);
playerDamaged.add(shard);
shard.alive = false;
shard.obj.destroy();
continue;
}
}
// Interior barrier hit
if (barriers?.destroyInteriorBarrierAt(shard.obj.x, shard.obj.y)) {
shard.alive = false;
shard.obj.destroy();
continue;
}
}
if (elapsed >= SHARD_DURATION) {
shards.forEach(s => { if (s.alive) s.obj.destroy(); });
ticker.remove();
}
},
});
}
}

View File

@ -0,0 +1,90 @@
import { BaseEnemy } from './BaseEnemy.js';
const PREFERRED_DIST = 280;
const SHOOT_INTERVAL = 3000; // ms between bursts
const PROJECTILE_SPEED = 175;
const PROJECTILE_DAMAGE = 8;
const SPRAY_COUNT = 5;
const SPRAY_HALF_ARC = Math.PI / 6; // ±30° around aim direction
export class SprayerEnemy extends BaseEnemy {
constructor(scene, x, y, player) {
super(scene, x, y, player, {
frameOffset: 9,
radius: 15,
hp: 50,
speed: 40,
xp: 25,
contactDamage: 8,
});
this._shootTimer = SHOOT_INTERVAL * Math.random();
this.projectiles = scene.add.group();
}
update(delta) {
super.update(delta);
if (this._dying) return;
this._strafe();
this._handleShoot(delta);
this._updateProjectiles();
}
_strafe() {
const dist = Phaser.Math.Distance.Between(this.x, this.y, this.player.x, this.player.y);
const angle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
let vx = 0, vy = 0;
if (dist > PREFERRED_DIST + 30) {
vx = Math.cos(angle) * this.speed;
vy = Math.sin(angle) * this.speed;
} else if (dist < PREFERRED_DIST - 30) {
vx = -Math.cos(angle) * this.speed;
vy = -Math.sin(angle) * this.speed;
} else {
const perp = angle + Math.PI / 2;
vx = Math.cos(perp) * this.speed;
vy = Math.sin(perp) * this.speed;
}
this.sprite.body.setVelocity(vx, vy);
}
_handleShoot(delta) {
this._shootTimer -= delta;
if (this._shootTimer <= 0) {
this._shootTimer = SHOOT_INTERVAL;
this._fireSpray();
}
}
_fireSpray() {
const baseAngle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
for (let i = 0; i < SPRAY_COUNT; i++) {
// Spread bullets evenly across the arc: indices 0..4 map to -1..+1
const t = SPRAY_COUNT === 1 ? 0 : (i / (SPRAY_COUNT - 1)) * 2 - 1;
const angle = baseAngle + t * SPRAY_HALF_ARC;
const proj = this.scene.add.circle(this.x, this.y, 5, 0x00ffaa);
this.scene.physics.add.existing(proj);
proj.body.setVelocity(
Math.cos(angle) * PROJECTILE_SPEED,
Math.sin(angle) * PROJECTILE_SPEED,
);
proj.damage = PROJECTILE_DAMAGE;
this.projectiles.add(proj);
this.scene.events.emit('enemy-projectile-spawned', proj);
}
}
_updateProjectiles() {
const W = this.scene.scale.width;
const H = this.scene.scale.height;
this.projectiles.getChildren().forEach(p => {
if (p.x < -30 || p.x > W + 30 || p.y < -30 || p.y > H + 30) p.destroy();
});
}
destroy() {
this.projectiles.clear(true, true);
super.destroy();
}
}

View File

@ -105,6 +105,40 @@ export class BarrierManager {
return false;
}
/**
* Destroy the interior barrier at (x,y) if one exists. Returns true if destroyed.
* Used by BomberEnemy shards.
*/
destroyInteriorBarrierAt(x, y) {
for (let i = 0; i < this._interiorSegs.length; i++) {
const seg = this._interiorSegs[i];
const r = seg.rect;
if (x >= r.x - r.w / 2 && x <= r.x + r.w / 2 &&
y >= r.y - r.h / 2 && y <= r.y + r.h / 2) {
// Visual: flash and shatter
if (seg.gfx?.active) {
this.scene.tweens.add({
targets: seg.gfx,
alpha: 0, scaleX: 1.8, scaleY: 1.8,
duration: 250,
onComplete: () => seg.gfx.destroy(),
});
}
seg.bodyImg?.destroy();
seg.bodyImg2?.destroy();
this._interiorSegs.splice(i, 1);
this._allRects = this._allRects.filter(
rect => !(rect.x === r.x && rect.y === r.y),
);
this._glitchTargets = this._glitchTargets.filter(g => g !== seg.gfx);
return true;
}
}
return false;
}
/**
* Animate edge barriers blinking in over ~5s, then enable their physics.
* @param {Function} onTrapped - called if player is inside an edge barrier when it solidifies
@ -240,10 +274,10 @@ export class BarrierManager {
gfx.x = def.x;
gfx.y = def.y;
this._makeBody(this._staticGroup, def.x, def.y, def.w, def.h);
this._makeBody(this._interiorStaticGroup, def.x, def.y, def.w, def.h);
const bodyImg = this._makeBody(this._staticGroup, def.x, def.y, def.w, def.h);
const bodyImg2 = this._makeBody(this._interiorStaticGroup, def.x, def.y, def.w, def.h);
this._interiorSegs.push({ gfx, rect: def });
this._interiorSegs.push({ gfx, bodyImg, bodyImg2, rect: def });
this._allRects.push({ x: def.x, y: def.y, w: def.w, h: def.h });
this._glitchTargets.push(gfx);
}

View File

@ -1,8 +1,10 @@
import { ChaseEnemy } from '../entities/enemies/ChaseEnemy.js';
import { ShooterEnemy } from '../entities/enemies/ShooterEnemy.js';
import { SwarmerEnemy } from '../entities/enemies/SwarmerEnemy.js';
import { SprayerEnemy } from '../entities/enemies/SprayerEnemy.js';
import { BomberEnemy } from '../entities/enemies/BomberEnemy.js';
const ENEMY_CLASSES = { ChaseEnemy, ShooterEnemy, SwarmerEnemy };
const ENEMY_CLASSES = { ChaseEnemy, ShooterEnemy, SwarmerEnemy, SprayerEnemy, BomberEnemy };
const SPAWN_MARGIN = 40;
export class WaveManager {