Add barrier system with edge traps and interior obstacles
- Introduce `BarrierManager` to handle neon cyberpunk-style barriers on screen edges and optional interior obstacles. - Edge barriers blink in after a short delay, trap the player if they are inside when solidified (instant death + respawn), and explode when zone waves complete to allow exit. - Interior barriers block enemies but not the player, adding tactical depth to arena layouts. - Update `zones.json` with `interiorBarriers` config per zone. - Adjust player/enemy sprite depths for correct rendering order relative to barriers. - Integrate barrier collision into physics: player vs all barriers, enemies vs interior only; bullets destroyed on contact with any barrier. - Refactor zone transitions and exit logic to coordinate barrier activation/destruction animations.
This commit is contained in:
parent
7da6459abe
commit
0a24272c88
|
|
@ -1,6 +1,7 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"interiorBarriers": 0,
|
||||
"waves": [
|
||||
{ "enemies": [{ "type": "ChaseEnemy", "count": 5 }] },
|
||||
{ "enemies": [{ "type": "ChaseEnemy", "count": 4 }, { "type": "SwarmerEnemy", "count": 6 }] },
|
||||
|
|
@ -9,6 +10,7 @@
|
|||
},
|
||||
{
|
||||
"id": 2,
|
||||
"interiorBarriers": 2,
|
||||
"waves": [
|
||||
{ "enemies": [{ "type": "ChaseEnemy", "count": 7 }, { "type": "SwarmerEnemy", "count": 8 }] },
|
||||
{ "enemies": [{ "type": "ShooterEnemy", "count": 4 }, { "type": "SwarmerEnemy", "count": 10 }] },
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export class Player {
|
|||
}
|
||||
|
||||
_buildSprite(x, y) {
|
||||
this.sprite = this.scene.add.sprite(x, y, 'player');
|
||||
this.sprite = this.scene.add.sprite(x, y, 'player').setDepth(6);
|
||||
this.scene.physics.add.existing(this.sprite);
|
||||
this.sprite.body.setCollideWorldBounds(true);
|
||||
// Circle hitbox centered in the 48×48 sprite: radius 16, offset 8 from top-left
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class BaseEnemy {
|
|||
}
|
||||
|
||||
_buildSprite(x, y) {
|
||||
this.sprite = this.scene.add.sprite(x, y, 'enemies', this.frameOffset);
|
||||
this.sprite = this.scene.add.sprite(x, y, 'enemies', this.frameOffset).setDepth(5);
|
||||
this.scene.physics.add.existing(this.sprite);
|
||||
// Circle hitbox centered in the 48×48 frame
|
||||
const offset = 24 - this.radius;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { SkillTree } from '../systems/SkillTree.js';
|
|||
import { HUD } from '../ui/HUD.js';
|
||||
import { SkillTreeUI } from '../ui/SkillTreeUI.js';
|
||||
import { Reticle } from '../ui/Reticle.js';
|
||||
import { BarrierManager } from '../systems/BarrierManager.js';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
|
|
@ -57,9 +58,38 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
this.reticle = new Reticle(this);
|
||||
|
||||
this._setupBarriers();
|
||||
|
||||
this.waveManager.start();
|
||||
}
|
||||
|
||||
_setupBarriers() {
|
||||
// Remove old colliders before destroying the groups they reference
|
||||
if (this._barrierColliders) {
|
||||
this._barrierColliders.forEach(c => c.destroy());
|
||||
}
|
||||
this.barrierManager?.destroy();
|
||||
|
||||
const zoneData = this.waveManager.currentZone ?? this.waveManager.zones[0];
|
||||
const interiorCount = zoneData?.interiorBarriers ?? 0;
|
||||
this.barrierManager = new BarrierManager(this, interiorCount);
|
||||
|
||||
// Player blocked by all barriers; enemies only blocked by interior ones
|
||||
this._barrierColliders = [
|
||||
this.physics.add.collider(this.player.sprite, this.barrierManager.staticGroup),
|
||||
this.physics.add.collider(this.waveManager.spriteGroup, this.barrierManager.interiorStaticGroup),
|
||||
];
|
||||
|
||||
this.barrierManager.activateEdgeBarriers(() => {
|
||||
const W = this.scale.width;
|
||||
const H = this.scale.height;
|
||||
if (this.barrierManager.isInsideEdgeBarrier(this.player.x, this.player.y)) {
|
||||
this.player.sprite.body.reset(W / 2, H / 2);
|
||||
this.player.takeDamage(this.player.hp); // lose a life, respawn at center
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_drawArena(W, H) {
|
||||
// Dark background
|
||||
this.add.rectangle(W / 2, H / 2, W, H, 0x111118);
|
||||
|
|
@ -97,6 +127,13 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
for (const bullet of bullets) {
|
||||
if (!bullet.active) continue;
|
||||
|
||||
// Destroy bullet if it hits a barrier
|
||||
if (this.barrierManager.isPointBlocked(bullet.x, bullet.y)) {
|
||||
bullet.destroy();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const enemy of enemies) {
|
||||
if (!enemy.active) continue;
|
||||
const dist = Phaser.Math.Distance.Between(bullet.x, bullet.y, enemy.x, enemy.y);
|
||||
|
|
@ -128,10 +165,12 @@ export class GameScene extends Phaser.Scene {
|
|||
// ── Zone exit (Smash TV style) ─────────────────────────────────────────────
|
||||
|
||||
_startZoneExit() {
|
||||
this._waitingForExit = true;
|
||||
// Let the player walk off any edge
|
||||
this.player.sprite.body.setCollideWorldBounds(false);
|
||||
this._showExitArrows();
|
||||
// Explode edge barriers first, then allow exit
|
||||
this.barrierManager.explodeEdgeBarriers(() => {
|
||||
this._waitingForExit = true;
|
||||
this.player.sprite.body.setCollideWorldBounds(false);
|
||||
this._showExitArrows();
|
||||
});
|
||||
}
|
||||
|
||||
_showExitArrows() {
|
||||
|
|
@ -238,6 +277,8 @@ export class GameScene extends Phaser.Scene {
|
|||
this.player.sprite.setPosition(spawnX, spawnY);
|
||||
this.player.sprite.body.setCollideWorldBounds(true);
|
||||
this.waveManager.startNextZone();
|
||||
// Set up barriers AFTER zone index is incremented so we read the new zone's config
|
||||
if (this.waveManager.currentZone) this._setupBarriers();
|
||||
}
|
||||
|
||||
// ── Level up ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -289,7 +330,9 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
shutdown() {
|
||||
this.events.removeAllListeners();
|
||||
this._barrierColliders?.forEach(c => c.destroy());
|
||||
this.reticle?.destroy();
|
||||
this.barrierManager?.destroy();
|
||||
this._hideExitArrows();
|
||||
this._waitingForExit = false;
|
||||
this.player?.destroy();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,382 @@
|
|||
// Cyberpunk building-facade barriers on screen edges + optional interior obstacles.
|
||||
// Edge barriers blink in ~5s after zone start; explode when zone waves complete.
|
||||
|
||||
const EDGE_DEPTH = 3;
|
||||
const INT_DEPTH = 3;
|
||||
const NEON_CYAN = 0x00ffff;
|
||||
const NEON_PINK = 0xff00aa;
|
||||
const NEON_YELLOW = 0xffee00;
|
||||
const WALL_FILL = 0x08080f;
|
||||
|
||||
const ACTIVATE_DELAY = 2000; // ms before edge barriers start blinking in
|
||||
const BLINK_DURATION = 300; // ms per half-blink (yoyo)
|
||||
const BLINK_REPEATS = 4; // total blink cycles (×2 for yoyo = 2.4s of blinking)
|
||||
|
||||
// Edge segments — 3 per side
|
||||
function buildEdgeSegments(W, H) {
|
||||
const T = 52;
|
||||
const segs = [];
|
||||
|
||||
const topWidths = [Math.floor(W * 0.32), Math.floor(W * 0.36), W - Math.floor(W * 0.32) - Math.floor(W * 0.36)];
|
||||
let ox = 0;
|
||||
topWidths.forEach((w, i) => {
|
||||
segs.push({ dir: 'top', x: ox + w / 2, y: T / 2, w, h: T, segIdx: i });
|
||||
ox += w;
|
||||
});
|
||||
ox = 0;
|
||||
topWidths.forEach((w, i) => {
|
||||
segs.push({ dir: 'bottom', x: ox + w / 2, y: H - T / 2, w, h: T, segIdx: i });
|
||||
ox += w;
|
||||
});
|
||||
|
||||
const sideH = H - T * 2;
|
||||
const leftHeights = [Math.floor(sideH * 0.33), Math.floor(sideH * 0.34), sideH - Math.floor(sideH * 0.33) - Math.floor(sideH * 0.34)];
|
||||
let oy = T;
|
||||
leftHeights.forEach((h, i) => {
|
||||
segs.push({ dir: 'left', x: T / 2, y: oy + h / 2, w: T, h, segIdx: i });
|
||||
oy += h;
|
||||
});
|
||||
oy = T;
|
||||
leftHeights.forEach((h, i) => {
|
||||
segs.push({ dir: 'right', x: W - T / 2, y: oy + h / 2, w: T, h, segIdx: i });
|
||||
oy += h;
|
||||
});
|
||||
|
||||
return segs;
|
||||
}
|
||||
|
||||
// Pool of interior obstacle rects (picked from by index)
|
||||
function allInteriorRects(W, H) {
|
||||
const T = 52;
|
||||
const p = { x: T, y: T, w: W - T * 2, h: H - T * 2 };
|
||||
return [
|
||||
{ x: p.x + p.w * 0.18, y: p.y + p.h * 0.20, w: 90, h: 24 },
|
||||
{ x: p.x + p.w * 0.72, y: p.y + p.h * 0.22, w: 100, h: 24 },
|
||||
{ x: p.x + p.w * 0.50, y: p.y + p.h * 0.42, w: 24, h: 90 },
|
||||
{ x: p.x + p.w * 0.20, y: p.y + p.h * 0.68, w: 110, h: 24 },
|
||||
{ x: p.x + p.w * 0.75, y: p.y + p.h * 0.70, w: 24, h: 80 },
|
||||
{ x: p.x + p.w * 0.45, y: p.y + p.h * 0.80, w: 80, h: 24 },
|
||||
];
|
||||
}
|
||||
|
||||
const FLY_DIR = { top: [0, -1], bottom: [0, 1], left: [-1, 0], right: [1, 0] };
|
||||
|
||||
export class BarrierManager {
|
||||
/**
|
||||
* @param {Phaser.Scene} scene
|
||||
* @param {number} interiorCount - how many interior obstacles to create (0 = none)
|
||||
*/
|
||||
constructor(scene, interiorCount = 0) {
|
||||
this.scene = scene;
|
||||
this._edgeSegments = [];
|
||||
this._interiorSegs = [];
|
||||
this._allRects = []; // all barrier rects for isPointBlocked
|
||||
this._edgeRects = []; // edge-only rects for isInsideEdgeBarrier
|
||||
this._glitchTimer = null;
|
||||
this._glitchTargets = []; // only contains visible/active gfx
|
||||
this._edgeActive = false;
|
||||
|
||||
this._init(interiorCount);
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** All barrier physics (player + bullet blocking). */
|
||||
get staticGroup() { return this._staticGroup; }
|
||||
|
||||
/** Interior-only physics (enemy collision — enemies spawn outside edge barriers). */
|
||||
get interiorStaticGroup() { return this._interiorStaticGroup; }
|
||||
|
||||
/** True if (x,y) is inside any barrier rect. */
|
||||
isPointBlocked(x, y) {
|
||||
for (const r of this._allRects) {
|
||||
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) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True if (x,y) is inside any edge barrier rect (for trap check). */
|
||||
isInsideEdgeBarrier(x, y) {
|
||||
for (const r of this._edgeRects) {
|
||||
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) 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
|
||||
*/
|
||||
activateEdgeBarriers(onTrapped) {
|
||||
// Edge barriers start invisible and with physics disabled
|
||||
this._edgeSegments.forEach(seg => {
|
||||
seg.gfx.setAlpha(0);
|
||||
if (seg.bodyImg?.body) seg.bodyImg.body.enable = false;
|
||||
});
|
||||
|
||||
// Use a repeating time event to blink manually — more reliable on Graphics objects
|
||||
let blinksLeft = BLINK_REPEATS * 2; // each tick toggles on/off
|
||||
let visible = false;
|
||||
|
||||
const blinkEvent = this.scene.time.addEvent({
|
||||
delay: BLINK_DURATION,
|
||||
startAt: ACTIVATE_DELAY,
|
||||
repeat: BLINK_REPEATS * 2 - 1,
|
||||
callback: () => {
|
||||
visible = !visible;
|
||||
const a = visible ? 1 : 0;
|
||||
this._edgeSegments.forEach(seg => { if (seg.gfx?.active) seg.gfx.setAlpha(a); });
|
||||
|
||||
blinksLeft--;
|
||||
if (blinksLeft <= 0) {
|
||||
// Final state: fully solid
|
||||
this._edgeSegments.forEach(seg => {
|
||||
if (seg.gfx?.active) seg.gfx.setAlpha(1);
|
||||
if (seg.bodyImg?.body) seg.bodyImg.body.enable = true;
|
||||
});
|
||||
this._edgeActive = true;
|
||||
this._glitchTargets.push(...this._edgeSegments.map(s => s.gfx).filter(g => g?.active));
|
||||
if (onTrapped) onTrapped();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this._activateEvent = blinkEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explode edge barriers with animation, then call cb when done.
|
||||
*/
|
||||
explodeEdgeBarriers(cb) {
|
||||
const scene = this.scene;
|
||||
const W = scene.scale.width;
|
||||
const H = scene.scale.height;
|
||||
|
||||
if (this._glitchTimer) { this._glitchTimer.remove(); this._glitchTimer = null; }
|
||||
|
||||
// Disable physics immediately
|
||||
this._edgeSegments.forEach(seg => {
|
||||
if (seg.bodyImg) { seg.bodyImg.destroy(); seg.bodyImg = null; }
|
||||
seg.body = null;
|
||||
});
|
||||
|
||||
// Camera shake + flash
|
||||
scene.cameras.main.shake(350, 0.014);
|
||||
const flash = scene.add.rectangle(W / 2, H / 2, W, H, 0xffffff).setDepth(80).setAlpha(0.55);
|
||||
scene.tweens.add({ targets: flash, alpha: 0, duration: 250, onComplete: () => flash.destroy() });
|
||||
|
||||
let remaining = this._edgeSegments.length;
|
||||
const onSegDone = () => { if (--remaining === 0 && cb) cb(); };
|
||||
|
||||
this._edgeSegments.forEach((seg, idx) => {
|
||||
if (!seg.gfx?.active) { onSegDone(); return; }
|
||||
const [dx, dy] = FLY_DIR[seg.dir];
|
||||
const flyDist = seg.dir === 'top' || seg.dir === 'bottom' ? H * 0.7 : W * 0.7;
|
||||
const delay = idx * 28 + Phaser.Math.Between(0, 40);
|
||||
|
||||
scene.time.delayedCall(delay, () => {
|
||||
this._spawnDebris(seg);
|
||||
scene.tweens.add({
|
||||
targets: seg.gfx,
|
||||
x: seg.gfx.x + dx * flyDist,
|
||||
y: seg.gfx.y + dy * flyDist,
|
||||
alpha: 0,
|
||||
angle: Phaser.Math.Between(-25, 25),
|
||||
duration: 480,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => { seg.gfx.destroy(); onSegDone(); },
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._glitchTimer) this._glitchTimer.remove();
|
||||
if (this._activateEvent) this._activateEvent.remove();
|
||||
this._edgeSegments.forEach(s => s.gfx?.destroy());
|
||||
this._interiorSegs.forEach(s => s.gfx?.destroy());
|
||||
this._staticGroup?.destroy(true);
|
||||
this._interiorStaticGroup?.destroy(true);
|
||||
}
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────────────
|
||||
|
||||
_init(interiorCount) {
|
||||
const scene = this.scene;
|
||||
const W = scene.scale.width;
|
||||
const H = scene.scale.height;
|
||||
|
||||
if (!scene.textures.exists('_barrier_px')) {
|
||||
const pg = scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
pg.fillStyle(0xffffff, 1);
|
||||
pg.fillRect(0, 0, 2, 2);
|
||||
pg.generateTexture('_barrier_px', 2, 2);
|
||||
pg.destroy();
|
||||
}
|
||||
|
||||
this._staticGroup = scene.physics.add.staticGroup();
|
||||
this._interiorStaticGroup = scene.physics.add.staticGroup();
|
||||
|
||||
buildEdgeSegments(W, H).forEach(def => {
|
||||
const gfx = scene.add.graphics().setDepth(EDGE_DEPTH);
|
||||
this._drawBarrier(gfx, def.w, def.h, def.dir, def.segIdx);
|
||||
gfx.x = def.x;
|
||||
gfx.y = def.y;
|
||||
|
||||
const bodyImg = this._makeBody(this._staticGroup, def.x, def.y, def.w, def.h);
|
||||
|
||||
this._edgeSegments.push({ gfx, bodyImg, body: bodyImg.body, dir: def.dir, rect: { x: def.x, y: def.y, w: def.w, h: def.h } });
|
||||
this._allRects.push({ x: def.x, y: def.y, w: def.w, h: def.h });
|
||||
this._edgeRects.push({ x: def.x, y: def.y, w: def.w, h: def.h });
|
||||
});
|
||||
|
||||
const intPool = allInteriorRects(W, H);
|
||||
for (let i = 0; i < Math.min(interiorCount, intPool.length); i++) {
|
||||
const def = intPool[i];
|
||||
const gfx = scene.add.graphics().setDepth(INT_DEPTH);
|
||||
this._drawBarrier(gfx, def.w, def.h, null, 0);
|
||||
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);
|
||||
|
||||
this._interiorSegs.push({ gfx, rect: def });
|
||||
this._allRects.push({ x: def.x, y: def.y, w: def.w, h: def.h });
|
||||
this._glitchTargets.push(gfx);
|
||||
}
|
||||
|
||||
this._startGlitch();
|
||||
}
|
||||
|
||||
_makeBody(group, cx, cy, w, h) {
|
||||
const img = group.create(cx, cy, '_barrier_px');
|
||||
img.setVisible(false);
|
||||
img.setDisplaySize(w, h);
|
||||
img.refreshBody();
|
||||
return img;
|
||||
}
|
||||
|
||||
// ── Drawing ────────────────────────────────────────────────────────────────
|
||||
|
||||
_drawBarrier(g, w, h, dir, segIdx) {
|
||||
const hw = w / 2, hh = h / 2;
|
||||
|
||||
g.fillStyle(WALL_FILL, 1);
|
||||
g.fillRect(-hw, -hh, w, h);
|
||||
|
||||
g.lineStyle(2, NEON_CYAN, 0.85);
|
||||
g.strokeRect(-hw, -hh, w, h);
|
||||
|
||||
const INS = 5;
|
||||
g.lineStyle(1, NEON_CYAN, 0.35);
|
||||
g.strokeRect(-hw + INS, -hh + INS, w - INS * 2, h - INS * 2);
|
||||
|
||||
if (dir) {
|
||||
this._drawFacade(g, w, h, dir, segIdx);
|
||||
} else {
|
||||
this._drawInteriorDetail(g, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
_drawFacade(g, w, h, dir, segIdx) {
|
||||
const hw = w / 2, hh = h / 2;
|
||||
const isHoriz = dir === 'top' || dir === 'bottom';
|
||||
|
||||
g.fillStyle(NEON_PINK, 0.18);
|
||||
if (isHoriz) g.fillRect(-hw + 8, -3, w - 16, 6);
|
||||
else g.fillRect(-3, -hh + 8, 6, h - 16);
|
||||
|
||||
g.fillStyle(NEON_YELLOW, 0.25);
|
||||
const WIN_W = isHoriz ? 14 : 10;
|
||||
const WIN_H = isHoriz ? 10 : 14;
|
||||
const cols = isHoriz ? 4 + segIdx : 2;
|
||||
const rows = isHoriz ? 2 : 3 + segIdx;
|
||||
const xGap = (w - 20) / (cols + 1);
|
||||
const yGap = (h - 10) / (rows + 1);
|
||||
for (let r = 1; r <= rows; r++) {
|
||||
for (let c = 1; c <= cols; c++) {
|
||||
g.fillRect(-hw + 10 + xGap * c - WIN_W / 2, -hh + 5 + yGap * r - WIN_H / 2, WIN_W, WIN_H);
|
||||
}
|
||||
}
|
||||
|
||||
const L = 10;
|
||||
g.lineStyle(2, NEON_PINK, 0.8);
|
||||
[[-hw, -hh], [hw, -hh], [-hw, hh], [hw, hh]].forEach(([cx, cy]) => {
|
||||
const sx = cx < 0 ? 1 : -1;
|
||||
const sy = cy < 0 ? 1 : -1;
|
||||
g.beginPath();
|
||||
g.moveTo(cx + sx * L, cy);
|
||||
g.lineTo(cx, cy);
|
||||
g.lineTo(cx, cy + sy * L);
|
||||
g.strokePath();
|
||||
});
|
||||
}
|
||||
|
||||
_drawInteriorDetail(g, w, h) {
|
||||
const hw = w / 2, hh = h / 2;
|
||||
const step = Math.min(w, h) > 50 ? 16 : 12;
|
||||
g.lineStyle(1, NEON_PINK, 0.3);
|
||||
for (let i = -Math.max(w, h); i < Math.max(w, h); i += step) {
|
||||
g.beginPath();
|
||||
g.moveTo(Math.max(-hw, i - hh), Math.min(hh, i + hw));
|
||||
g.lineTo(Math.min(hw, i + hh), Math.max(-hh, i - hw));
|
||||
g.strokePath();
|
||||
}
|
||||
g.fillStyle(NEON_CYAN, 0.6);
|
||||
g.fillRect(-3, -3, 6, 6);
|
||||
}
|
||||
|
||||
// ── Glitch ─────────────────────────────────────────────────────────────────
|
||||
|
||||
_startGlitch() {
|
||||
this._glitchTimer = this.scene.time.addEvent({
|
||||
delay: 800,
|
||||
loop: true,
|
||||
callback: () => {
|
||||
if (!this._glitchTargets.length || Math.random() > 0.45) return;
|
||||
const target = Phaser.Utils.Array.GetRandom(this._glitchTargets);
|
||||
if (!target?.active) return;
|
||||
this.scene.tweens.add({
|
||||
targets: target,
|
||||
alpha: { from: 1, to: 0.2 },
|
||||
duration: 60,
|
||||
yoyo: true,
|
||||
repeat: Phaser.Math.Between(1, 3),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Explosion debris ────────────────────────────────────────────────────────
|
||||
|
||||
_spawnDebris({ rect, dir }) {
|
||||
const scene = this.scene;
|
||||
const colors = [NEON_CYAN, NEON_PINK, NEON_YELLOW];
|
||||
const [dx, dy] = FLY_DIR[dir];
|
||||
|
||||
for (let i = 0; i < Phaser.Math.Between(6, 12); i++) {
|
||||
const piece = scene.add.rectangle(
|
||||
rect.x + Phaser.Math.Between(-rect.w / 2, rect.w / 2),
|
||||
rect.y + Phaser.Math.Between(-rect.h / 2, rect.h / 2),
|
||||
Phaser.Math.Between(4, 18),
|
||||
Phaser.Math.Between(4, 14),
|
||||
Phaser.Utils.Array.GetRandom(colors),
|
||||
).setDepth(60).setAlpha(0.9);
|
||||
|
||||
const speed = Phaser.Math.Between(80, 300);
|
||||
scene.tweens.add({
|
||||
targets: piece,
|
||||
x: piece.x + dx * speed + Phaser.Math.Between(-120, 120),
|
||||
y: piece.y + dy * speed + Phaser.Math.Between(-120, 120),
|
||||
alpha: 0, scaleX: 0.1, scaleY: 0.1,
|
||||
angle: Phaser.Math.Between(-180, 180),
|
||||
duration: Phaser.Math.Between(350, 700),
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => piece.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ export class WaveManager {
|
|||
this.waveIndex = 0;
|
||||
this.enemies = [];
|
||||
this.active = false;
|
||||
this.spriteGroup = scene.physics.add.group();
|
||||
}
|
||||
|
||||
load(data) {
|
||||
|
|
@ -44,6 +45,7 @@ export class WaveManager {
|
|||
const [x, y] = this._edgePosition();
|
||||
const enemy = new EnemyClass(this.scene, x, y, this.player);
|
||||
this.enemies.push(enemy);
|
||||
this.spriteGroup.add(enemy.sprite);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -104,6 +106,7 @@ export class WaveManager {
|
|||
reset() {
|
||||
this.enemies.forEach(e => { if (e.active) e.destroy(); });
|
||||
this.enemies = [];
|
||||
this.spriteGroup.clear(false, false);
|
||||
this.zoneIndex = 0;
|
||||
this.waveIndex = 0;
|
||||
this.active = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue