overrun/js/systems/BarrierManager.js

417 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}
/**
* 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
*/
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;
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, bodyImg, bodyImg2, 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(),
});
}
}
}