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