Add compact video-only comms box for in-combat visual calls
- Introduce a `compact` hook on CommsHub so scenes can flag which comms should play as the small right-edge plate. - GameScene passes `true` for 'visual' comms while combat is active, keeping the battlefield clear while the clip and voice still play. - CommsBox builds a header/text/button-free variant scaled to `box.combat.scale` (2/3) with a configurable right-edge margin. - Guard `beginText` and `setVoice` for the compact form, which has no text object or status dot.
This commit is contained in:
parent
a5f69f3221
commit
52650ff5b9
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue