182 lines
6.8 KiB
JavaScript
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();
|
|
}
|
|
}
|