Add Zones 3-6 with new enemy types (SprayerEnemy, BomberEnemy) and enhanced barrier interaction
This commit is contained in:
parent
0a24272c88
commit
303956ae4d
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
|
@ -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 }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue