166 lines
7.1 KiB
JavaScript
166 lines
7.1 KiB
JavaScript
/**
|
||
* The FACTION DUEL PICKER — the in-game panel for the ?combat=apex /
|
||
* ?combat=mindweave arena (data/combat.json → combat.factions; the fight
|
||
* seam is CombatSystem.startDuel / spawnDuel / spawnDuelPack).
|
||
*
|
||
* A top-left Phaser panel: the faction's eight fighters as rows
|
||
* (key 1–8, click works too), row 0 = the whole pack. It is HIDDEN while
|
||
* a duel is live (combat.duel.active) and re-shows the moment the marked
|
||
* fighter goes down (killEnemy drops the flag) — pick the next one.
|
||
*
|
||
* Keyboard: DIGIT_0 … DIGIT_8 (the keyboard plugin cleans its listeners
|
||
* up with the scene). No DOM, no data fetch — the rows come from the
|
||
* faction's own config (EnemyTypes.factionMeta / factionTypeIds).
|
||
*/
|
||
import Phaser from '../vendor/phaser.js';
|
||
import { factionMeta, factionTypeIds } from './EnemyTypes.js';
|
||
|
||
const PANEL_W = 360;
|
||
const ROW_H = 22;
|
||
const TOP_Y = 64; // under the combat bar's top edge
|
||
const HEAD_H = 52; // title + tagline
|
||
const FOOT_H = 34; // the pack row + the hint
|
||
|
||
export class DuelPicker {
|
||
/**
|
||
* @param {object} scene the host Phaser scene (GameScene)
|
||
* @param {object} combat the CombatSystem (its .duel state drives visibility)
|
||
* @param {string} factionId a combat.factions id ('apex' | 'mindweave' | 'zenith')
|
||
*/
|
||
constructor(scene, combat, factionId) {
|
||
const meta = factionMeta(factionId);
|
||
this.scene = scene;
|
||
this.combat = combat;
|
||
this.factionId = factionId;
|
||
this._shown = false;
|
||
if (!meta || !Array.isArray(meta.types) || meta.types.length === 0) {
|
||
this.container = null; // a bad faction id: no panel, no fight (the boot stays a normal game)
|
||
return;
|
||
}
|
||
this.types = meta.types;
|
||
this._rowRects = []; // the pick zones (built with the rows below)
|
||
|
||
const w = PANEL_W;
|
||
const h = HEAD_H + this.types.length * ROW_H + FOOT_H;
|
||
const x0 = 16, y0 = TOP_Y;
|
||
const cx = x0 + w / 2, cy = y0 + h / 2;
|
||
|
||
const bg = scene.add.rectangle(cx, cy, w, h, 0x0a0e18, 0.78);
|
||
bg.setStrokeStyle(1, 0x3a4a6a);
|
||
const title = scene.add.text(x0 + 12, y0 + 8, String(meta.name ?? factionId).toUpperCase(), {
|
||
fontFamily: 'monospace', fontSize: '18px', color: '#ffffff',
|
||
});
|
||
const tag = scene.add.text(x0 + 12, y0 + 30, String(meta.tagline ?? ''), {
|
||
fontFamily: 'monospace', fontSize: '11px', color: '#8fa3c8',
|
||
});
|
||
|
||
const rows = [];
|
||
this.types.forEach((t, i) => {
|
||
const y = y0 + HEAD_H + i * ROW_H;
|
||
const label = scene.add.text(x0 + 12, y, `${i + 1} ${t.name ?? t.id}`, {
|
||
fontFamily: 'monospace', fontSize: '13px', color: '#e8f0ff',
|
||
});
|
||
const style = scene.add.text(x0 + 46, y, `— ${t.style ?? ''}`, {
|
||
fontFamily: 'monospace', fontSize: '11px', color: '#8fa3c8',
|
||
});
|
||
rows.push(label, style);
|
||
this._rowRects.push({ i: i + 1, x: x0 + 8, y: y - 8, w: w - 16, h: ROW_H });
|
||
});
|
||
const pack = scene.add.text(x0 + 12, y0 + HEAD_H + this.types.length * ROW_H,
|
||
`0 the whole pack — ${this.types.length} fighters at once`, {
|
||
fontFamily: 'monospace', fontSize: '13px', color: '#ffd84d',
|
||
});
|
||
rows.push(pack);
|
||
this._rowRects.push({ i: 0, x: x0 + 8, y: y0 + HEAD_H + this.types.length * ROW_H - 8, w: w - 16, h: ROW_H });
|
||
|
||
const hint = scene.add.text(x0 + 12, y0 + h - 20, 'press the key or click a row — one fighter at a time', {
|
||
fontFamily: 'monospace', fontSize: '10px', color: '#5f739a',
|
||
});
|
||
|
||
this._rowRects.forEach((r) => {
|
||
// Place the pick zone EXACTLY over its own row. _rowRects is already in
|
||
// absolute, top-left-anchored screen coords (built from the same x0/y0
|
||
// the text rows use), so use it verbatim. The old formula re-added the
|
||
// panel offset (cx - w/2, cy - h/2) on top, which pushed every zone
|
||
// THREE rows down — a click on a visible row either missed the zones
|
||
// entirely or fired the wrong fighter.
|
||
const hit = scene.add.zone(r.x, r.y, r.w, r.h).setOrigin(0, 0);
|
||
// v4 (see ActionBar / MenuButton): a bare setInteractive() leaves no
|
||
// hit-testable area the input manager will actually claim on click, so
|
||
// re-arm it with an explicit hitArea + callback. The callback receives
|
||
// (hitArea, localX, localY) — local space of the origin-0 zone.
|
||
hit.setInteractive({
|
||
useHandCursor: true,
|
||
hitArea: new Phaser.Geom.Rectangle(0, 0, r.w, r.h),
|
||
hitAreaCallback: (rect, lx, ly) => (lx >= 0 && lx <= r.w && ly >= 0 && ly <= r.h),
|
||
});
|
||
hit.on('pointerdown', () => this.select(r.i));
|
||
rows.push(hit);
|
||
});
|
||
|
||
// PIN THE PANEL TO THE SCREEN (scrollFactor 0) — the same hand every
|
||
// other combat HUD uses. Left at the default scrollFactor 1 the rows
|
||
// are WORLD-anchored: they land mid-canvas (the camera is centred on
|
||
// the ship, not on the origin) and drift off-screen the moment the
|
||
// camera follows the player. Pinned, the panel sits top-left as the
|
||
// x0/y0 layout intends, and the zone hit-boxes track it in screen
|
||
// space (Phaser's input accounts for the scroll factor on the pick).
|
||
[bg, title, tag, ...rows, hint].forEach((o) => {
|
||
if (o && typeof o.setScrollFactor === 'function') o.setScrollFactor(0);
|
||
});
|
||
|
||
this.container = scene.add.container(0, 0, [bg, title, tag, ...rows, hint]);
|
||
this.container.setDepth(2000);
|
||
if (typeof this.container.setScrollFactor === 'function') this.container.setScrollFactor(0);
|
||
|
||
// The keyboard hand (digits 0–8). The manager's plain `keydown` event
|
||
// (the same one the skill seats use) carries the DOM event, so read
|
||
// e.key / e.code and parse the digit. The DuelPicker only exists in the
|
||
// faction arena, and there the skill-seats handler no-ops (no class, the
|
||
// screen is busy), so the digits are free to pick.
|
||
const kb = scene.input && scene.input.keyboard;
|
||
if (kb && typeof kb.on === 'function') {
|
||
kb.on('keydown', (e) => {
|
||
if (!this.types) return;
|
||
const raw = `${String(e?.key ?? '')}|${String(e?.code ?? '')}`;
|
||
const m = /^([0-8])\||^Digit([0-8])$/i.exec(raw);
|
||
const digit = Number((m && (m[1] ?? m[2])) ?? -1);
|
||
if (digit >= 0) this.select(digit);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A pick (the key / the row's click): 0 = the whole pack, 1–8 = the
|
||
* fighter at that row. Replaces any live duel ship (a rematch is a
|
||
* fresh craft). A pick while the duel is live is a RE-PICK (allowed —
|
||
* the picker is hidden while live, so this path is the keyboard's).
|
||
*/
|
||
select(n) {
|
||
const combat = this.combat;
|
||
if (!combat || !this.types) return;
|
||
if (n === 0) {
|
||
combat.spawnDuelPack(this.factionId);
|
||
return;
|
||
}
|
||
const t = this.types[n - 1];
|
||
if (t && typeof t.id === 'string') combat.spawnDuel(t.id);
|
||
}
|
||
|
||
/** The picker's frame step (GameScene.update): visibility = duel not live. */
|
||
update(_time) {
|
||
if (!this.container) return;
|
||
const d = this.combat.duel;
|
||
if (!d) return;
|
||
const show = !d.active;
|
||
if (show === this._shown) return;
|
||
this._shown = show;
|
||
this.container.setVisible(show);
|
||
}
|
||
|
||
/** The panel's rows (a test/dev seam). */
|
||
rows() {
|
||
return (this.types ?? []).map((t, i) => ({ key: i + 1, id: t.id, name: t.name }));
|
||
}
|
||
}
|