417 lines
14 KiB
JavaScript
417 lines
14 KiB
JavaScript
// 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(),
|
||
});
|
||
}
|
||
}
|
||
}
|