import Phaser from '../vendor/phaser.js'; import { config } from '../config/Config.js'; import { toColor, toCss } from '../utils/Color.js'; import { fontStack, themeColor } from '../utils/Theme.js'; const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif"; /** * The COMBAT BAR — the main game's combat-mode action bar (data/combat.json * → bar). SHOWN IN PLACE OF THE ACTION DECK while combat is live * (GameScene swaps them: the deck hides, this takes its place, and back * when the pack is wiped): * * [ SHD | HUL ] [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ] [ ENR ] * * left — the player's vertical SHIELDS + HULL gauges * middle — the eight SKILL seats, wearing keys 1-8. The skills land * later — for now they are empty seats (a press lands on * o.onCast(index); the system's cast() is the no-op stub). * right — the player's vertical ENERGY gauge (the skills' resource) * * Built the way ActionBar is (the deck's pattern, verified on this * Giedi build): children created with scene.add.*, parented into the * container with this.add(), each setScrollFactor(0) so they sit over * the world at any zoom. */ export class CombatBar extends Phaser.GameObjects.Container { /** * @param {Phaser.Scene} scene a scale/texture-equipped scene (GameScene) * @param {object} [o] * onCast (index) => {} — a skill seat was pressed (index 0-7) */ constructor(scene, o = {}) { super(scene, 0, 0); // v4 quirk (same note as ActionBar): register the directly-built // container on the scene's display list or it never renders. this.scene.add.existing(this); this.setScrollFactor(0); // UI — pinned to the screen, not the world this.setDepth(50); // the deck's depth this.onCast = typeof o?.onCast === 'function' ? o.onCast : null; const bar = config.section('combat.bar', {}); const { width: W, height: H } = scene.scale; const mL = Number(bar.margin?.left) || 20; const mR = Number(bar.margin?.right) || 20; const mB = Number(bar.margin?.bottom) || 12; const barH = Number(bar.height) || 92; const x0 = mL; const y0 = H - mB - barH; this.rect = { x: x0, y: y0, w: W - mL - mR, h: barH }; const c = bar.colors ?? {}; const panel = toColor(c.bg, themeColor('panel', 0x0b1322)); const edge = toColor(c.edge, themeColor('neon', 0x00e5ff)); const shieldC = toColor(c.shield, 0x00e5ff); const hullC = toColor(c.hull, 0x7dff8a); const energyC = toColor(c.energy, 0xffd84d); const slotBg = toColor(c.slotBg, 0x0b1322); const slotEdge = toColor(c.slotBorder, 0x22405f); const keyC = toColor(c.key, 0x5c7196); const alpha = Number(bar.bgAlpha) || 0.92; // ---- the panel (the deck's footprint — margin/height mirror it) ---- this.add(this._rect(x0, y0, this.rect.w, barH, panel, alpha)); this.add(this._rect(x0, y0, this.rect.w, 2, edge, 0.4)); // top edge // ---- the gauges (vertical; fills grow UP from the bottom) ---------- const g = bar.gauge ?? {}; const gaugeW = Number(g.width) || 14; const gaugeGap = Number(g.gap) || 8; const pad = Number(g.pad) || 12; const labelPx = Number(g.labelSize) || 11; const gaugeH = barH - pad * 2 - labelPx - 4; const gaugeTop = y0 + pad; const mkGauge = (cx, color, label) => { this.add(this._rect(cx - gaugeW / 2, gaugeTop, gaugeW, gaugeH, slotBg, 1)); // The fill: a bottom-anchored rect whose scaleY IS the level. const fill = scene.add.rectangle(cx, gaugeTop + gaugeH, gaugeW - 4, gaugeH - 4, color, 1); fill.setOrigin(0.5, 1); fill.setScrollFactor(0); this.add(fill); const labelT = scene.add .text(cx, y0 + barH - 11, label, { fontFamily: fontStack('body', FONT_FALLBACK), fontSize: `${labelPx}px`, color: toCss(themeColor('faint', 0x8391ad)), letterSpacing: 1, }) .setOrigin(0.5, 0.5) .setScrollFactor(0); this.add(labelT); return { fill, frac: 1, track: { x: cx - gaugeW / 2, y: gaugeTop, w: gaugeW, h: gaugeH } }; }; const leftX = x0 + pad + gaugeW / 2; this.gShield = mkGauge(leftX, shieldC, 'SHD'); this.gHull = mkGauge(leftX + gaugeW + gaugeGap, hullC, 'HUL'); const rightX = x0 + this.rect.w - pad - gaugeW / 2; this.gEnergy = mkGauge(rightX, energyC, 'ENR'); // ---- the skill seats (middle; keys 1-8; the skills land later) ----- const s = bar.slot ?? {}; const count = Math.max(0, Number(s.count) || 8); const size = Number(s.size) || 64; const gap = Number(s.gap) || 10; const keyPx = Number(s.keySize) || 12; const totalW = count * size + (count - 1) * gap; let sx = W / 2 - totalW / 2 + size / 2; const sy = y0 + barH / 2; this.slots = []; for (let i = 0; i < count; i++) { const bg = scene.add.rectangle(sx - size / 2, sy - size / 2, size, size, slotBg, 1); bg.setScrollFactor(0); this.add(bg); this.add(this._rect(sx - size / 2, sy - size / 2, size, 1, slotEdge, 0.9)); // top lip bg.setInteractive({ useHandCursor: true }); bg.on('pointerdown', () => { if (this.onCast) this.onCast(i); }); const keyT = scene.add .text(sx - size / 2 + 6, sy - size / 2 + 5, String(i + 1), { fontFamily: fontStack('body', FONT_FALLBACK), fontSize: `${keyPx}px`, color: toCss(keyC), }) .setOrigin(0, 0.5) .setScrollFactor(0); this.add(keyT); this.slots.push({ x: sx, bg, keyT }); sx += size + gap; } } /** * A solid rectangle, created on the scene (the scene.add.* factory is * what puts it on the display list) — the bar's panel/chrome primitive. * The caller parents it into the container with this.add(rect). */ _rect(x, y, w, h, color, alpha = 1) { const r = this.scene.add.rectangle(x, y, w, h, color, alpha); r.setScrollFactor(0); return r; } /** * Feed the gauges. Accepts a CombatSystem (uses its stats()) or a * plain snapshot { hull, maxHull, shields, maxShields, energy, maxEnergy }. */ setStats(src) { if (!src) return; const s = typeof src.stats === 'function' ? src.stats() : src; const frac = (v, m) => (m > 0 ? Math.max(0, Math.min(1, v / m)) : 0); this._gauge(this.gShield, frac(s.shields, s.maxShields)); this._gauge(this.gHull, frac(s.hull, s.maxHull)); this._gauge(this.gEnergy, frac(s.energy, s.maxEnergy)); } _gauge(g, f) { g.frac = f; g.fill.setScale(1, Math.max(0.001, f)); } /** Is (px, py) inside the bar's footprint? (GameScene's click gate.) */ contains(px, py) { const m = 6; // a little slop, like the deck's return ( px >= this.rect.x - m && px <= this.rect.x + this.rect.w + m && py >= this.rect.y - m && py <= this.rect.y + this.rect.h + m ); } /** * The gauge's TRACK rect (for the readability layer's hit-flash — * js/combat/CombatHud.js flashes this rect when the player takes that * layer's hit): 'shield' or 'hull'. */ gaugeRect(kind) { const g = kind === 'hull' ? this.gHull : this.gShield; return g && g.track ? g.track : null; } /** Tear down (scene destroy) — the children go with the scene anyway. */ destroy() { this.onCast = null; super.destroy(); } }