diff --git a/data/comms.json b/data/comms.json index 0095637..bb4988b 100644 --- a/data/comms.json +++ b/data/comms.json @@ -586,6 +586,11 @@ "dim": "#7d92c4", "faint": "#3d4c74", "amber": "#ffc94d" + }, + "combat": { + "$comment": "In-combat 'visual' comms: the box sheds to a video-only plate hugging the screen's right edge (the hub's compact hook — GameScene passes it when combat is live). scale = the clip's size as a fraction of videoSize; margin = px from the screen's right edge.", + "scale": 0.667, + "margin": 16 } } } diff --git a/js/comms/CommsHub.js b/js/comms/CommsHub.js index cac2df5..869d6f6 100644 --- a/js/comms/CommsHub.js +++ b/js/comms/CommsHub.js @@ -77,6 +77,12 @@ export class CommsHub { * the ACKNOWLEDGE press itself — * scene bookkeeping that rides on * a comm being acknowledged) + * compact?: (comm: object) => boolean, + * the comm should play in the compact + * (video-only) form — the box's small + * right-edge plate (the in-combat 'visual' + * call is the scene's; it passes the comm + * about to start) * onAck?: (comm: object) => void, an 'ack' comm was ACKNOWLEDGED — * the press itself, fired BEFORE * onEnd for the same def (so the @@ -102,6 +108,7 @@ export class CommsHub { this.onStart = typeof o.onStart === 'function' ? o.onStart : null; this.onEnd = typeof o.onEnd === 'function' ? o.onEnd : null; this.onAck = typeof o.onAck === 'function' ? o.onAck : null; + this.compact = typeof o.compact === 'function' ? o.compact : null; this.queue = []; // { def, dueAt, assets, loadingDone } this.current = null; // the comm in play (its def) @@ -284,6 +291,17 @@ export class CommsHub { const video = entry.assets.video && _hasVideo(s, entry.assets.video.key) ? { key: entry.assets.video.key } : null; + // The compact (video-only) form — the scene's call (the in-combat + // 'visual' comms in GameScene). A failing hook falls back to the + // full box, never to no box. + let compact = false; + if (this.compact) { + try { + compact = !!this.compact(def); + } catch (e) { + console.warn('[comms] compact hook failed', e); + } + } this.box = new CommsBox(s, this._anchor(), { label: char.label ?? def.from, role: char.role ?? '', @@ -292,6 +310,7 @@ export class CommsHub { video, showAck: def.type === 'ack', onAck: () => this._ack(), + compact, }); this.box.open(); } diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index b71d64b..1f50b56 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -1385,6 +1385,12 @@ export class GameScene extends Phaser.Scene { if (!hud) return null; return { right: this.scale.width - hud.pad, top: hud.barY + hud.barH }; }, + // IN-COMBAT 'visual' comms take the compact form: the box's small + // video-only plate hugging the right edge of the screen (2/3-size + // clip, no text) — the battlefield stays clear while the video and + // the voice still play. 'ack' comms keep the full box (they need + // the words + the ACKNOWLEDGE button). + compact: (def) => def?.type === 'visual' && !!(this.combat && this.combat.active), // The ack pause's scene-side effects: the action holds — the ship // parks dead (target + throttle cleared, velocity zeroed) and the // mining arm drops. Resuming needs no work (the ship stays parked; diff --git a/js/ui/CommsBox.js b/js/ui/CommsBox.js index 663414d..6271213 100644 --- a/js/ui/CommsBox.js +++ b/js/ui/CommsBox.js @@ -65,7 +65,17 @@ export class CommsBox extends Phaser.GameObjects.Container { * video: {key:string}|null, the sender's cached clip (null → NO SIGNAL plate) * showAck: boolean, the ACKNOWLEDGE button (ack comms) * onAck: () => void, the button press (the hub finishes the comm) + * compact: boolean, the IN-COMBAT 'visual' form (see below) * }} spec + * + * COMPACT MODE (in-combat 'visual' comms — spec.compact): the box sheds + * everything but the clip — no header, no text, no button — and docks + * as a small plate hugging the RIGHT edge of the screen, vertically + * centered, its clip at box.combat.scale (2/3) of the normal videoSize. + * The goal: the combat battlefield stays as unobstructed as possible + * while the comm's video still shows. The clip is still muted (the + * voice is the hub's speech clip — the project's video rule, unchanged); + * the box just stops showing the words. */ constructor(scene, anchor, spec) { super(scene, 0, 0); @@ -101,6 +111,7 @@ export class CommsBox extends Phaser.GameObjects.Container { this.inMs = Math.max(0, anim.inMs ?? 260); this.outMs = Math.max(0, anim.outMs ?? 420); + this.compact = spec.compact === true; this.onAck = typeof spec.onAck === 'function' ? spec.onAck : null; this.showAck = spec.showAck === true; this.loopVideo = spec.loop === true; // the clip loops silently (ack) or plays once (visual) @@ -118,6 +129,29 @@ export class CommsBox extends Phaser.GameObjects.Container { this._ringA = null; this._ringB = null; + // ---- compact (in-combat) form — video only, hugging the right ---- + // The clip at box.combat.scale of the normal videoSize, in a plate + // of just its frame: the battlefield keeps the rest of the screen. + // No header, no text, no decode, no button — the hub still plays + // the voice over it, and the open/close paths are the same as the + // full box. + if (this.compact) { + const c = boxCfg.combat ?? {}; + this.videoSize = Math.max(40, Math.round(this.videoSize * (c.scale ?? 2 / 3))); + this.W = this.pad * 2 + this.videoSize; + this.H = this.pad * 2 + this.videoSize; + const sw = this.scene.scale?.width ?? 1280; + const sh = this.scene.scale?.height ?? 720; + const m = c.margin ?? 16; + this.finalX = sw - m - this.W; + this.finalY = Math.round(sh / 2 - this.H / 2); // the screen's right-mid line + this.setPosition(this.finalX, this.finalY - 16); // the same 16px slide-in + this.setAlpha(0); + this._buildBackplate(); + this._buildVideoPanel(this.pad); // the clip, contain-fit as always + return; // no header / text / button / pulse in this form + } + const fam = fontStack('body'); // ---- measure the text (its height sizes the box) ------------------ @@ -428,8 +462,10 @@ export class CommsBox extends Phaser.GameObjects.Container { // the hub's handles // ------------------------------------------------------------------ - /** The comm's words decoding in (the hub calls this once, at open). */ + /** The comm's words decoding in (the hub calls this once, at open). + * A no-op in the compact form (no text object — the clip carries it). */ beginText() { + if (!this.text) return; const dur = decodeDur(this._textValue.length); this._decT0 = this.scene.time.now; this._dec = new ScrambleDecode(this._textValue, this._decT0, dur); @@ -448,9 +484,12 @@ export class CommsBox extends Phaser.GameObjects.Container { } } - /** The voice is live / done — the header dot pulses while it talks. */ + /** The voice is live / done — the header dot pulses while it talks. + * The compact form has no dot (no header): the state is noted and + * that's all it is for. */ setVoice(on) { this.voiceOn = !!on; + if (!this.dot) return; if (this._dotTween) { this._dotTween.stop(); this._dotTween = null;