orbit/js/combat/DuelPicker.js

166 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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