orbit/js/ui/CombatBar.js

182 lines
6.8 KiB
JavaScript

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 };
};
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
);
}
/** Tear down (scene destroy) — the children go with the scene anyway. */
destroy() {
this.onCast = null;
super.destroy();
}
}