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",
|
"dim": "#7d92c4",
|
||||||
"faint": "#3d4c74",
|
"faint": "#3d4c74",
|
||||||
"amber": "#ffc94d"
|
"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 —
|
* the ACKNOWLEDGE press itself —
|
||||||
* scene bookkeeping that rides on
|
* scene bookkeeping that rides on
|
||||||
* a comm being acknowledged)
|
* 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 —
|
* onAck?: (comm: object) => void, an 'ack' comm was ACKNOWLEDGED —
|
||||||
* the press itself, fired BEFORE
|
* the press itself, fired BEFORE
|
||||||
* onEnd for the same def (so the
|
* onEnd for the same def (so the
|
||||||
|
|
@ -102,6 +108,7 @@ export class CommsHub {
|
||||||
this.onStart = typeof o.onStart === 'function' ? o.onStart : null;
|
this.onStart = typeof o.onStart === 'function' ? o.onStart : null;
|
||||||
this.onEnd = typeof o.onEnd === 'function' ? o.onEnd : null;
|
this.onEnd = typeof o.onEnd === 'function' ? o.onEnd : null;
|
||||||
this.onAck = typeof o.onAck === 'function' ? o.onAck : 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.queue = []; // { def, dueAt, assets, loadingDone }
|
||||||
this.current = null; // the comm in play (its def)
|
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)
|
const video = entry.assets.video && _hasVideo(s, entry.assets.video.key)
|
||||||
? { key: entry.assets.video.key }
|
? { key: entry.assets.video.key }
|
||||||
: null;
|
: 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(), {
|
this.box = new CommsBox(s, this._anchor(), {
|
||||||
label: char.label ?? def.from,
|
label: char.label ?? def.from,
|
||||||
role: char.role ?? '',
|
role: char.role ?? '',
|
||||||
|
|
@ -292,6 +310,7 @@ export class CommsHub {
|
||||||
video,
|
video,
|
||||||
showAck: def.type === 'ack',
|
showAck: def.type === 'ack',
|
||||||
onAck: () => this._ack(),
|
onAck: () => this._ack(),
|
||||||
|
compact,
|
||||||
});
|
});
|
||||||
this.box.open();
|
this.box.open();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1385,6 +1385,12 @@ export class GameScene extends Phaser.Scene {
|
||||||
if (!hud) return null;
|
if (!hud) return null;
|
||||||
return { right: this.scale.width - hud.pad, top: hud.barY + hud.barH };
|
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
|
// The ack pause's scene-side effects: the action holds — the ship
|
||||||
// parks dead (target + throttle cleared, velocity zeroed) and the
|
// parks dead (target + throttle cleared, velocity zeroed) and the
|
||||||
// mining arm drops. Resuming needs no work (the ship stays parked;
|
// 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)
|
* video: {key:string}|null, the sender's cached clip (null → NO SIGNAL plate)
|
||||||
* showAck: boolean, the ACKNOWLEDGE button (ack comms)
|
* showAck: boolean, the ACKNOWLEDGE button (ack comms)
|
||||||
* onAck: () => void, the button press (the hub finishes the comm)
|
* onAck: () => void, the button press (the hub finishes the comm)
|
||||||
|
* compact: boolean, the IN-COMBAT 'visual' form (see below)
|
||||||
* }} spec
|
* }} 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) {
|
constructor(scene, anchor, spec) {
|
||||||
super(scene, 0, 0);
|
super(scene, 0, 0);
|
||||||
|
|
@ -101,6 +111,7 @@ export class CommsBox extends Phaser.GameObjects.Container {
|
||||||
this.inMs = Math.max(0, anim.inMs ?? 260);
|
this.inMs = Math.max(0, anim.inMs ?? 260);
|
||||||
this.outMs = Math.max(0, anim.outMs ?? 420);
|
this.outMs = Math.max(0, anim.outMs ?? 420);
|
||||||
|
|
||||||
|
this.compact = spec.compact === true;
|
||||||
this.onAck = typeof spec.onAck === 'function' ? spec.onAck : null;
|
this.onAck = typeof spec.onAck === 'function' ? spec.onAck : null;
|
||||||
this.showAck = spec.showAck === true;
|
this.showAck = spec.showAck === true;
|
||||||
this.loopVideo = spec.loop === true; // the clip loops silently (ack) or plays once (visual)
|
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._ringA = null;
|
||||||
this._ringB = 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');
|
const fam = fontStack('body');
|
||||||
|
|
||||||
// ---- measure the text (its height sizes the box) ------------------
|
// ---- measure the text (its height sizes the box) ------------------
|
||||||
|
|
@ -428,8 +462,10 @@ export class CommsBox extends Phaser.GameObjects.Container {
|
||||||
// the hub's handles
|
// 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() {
|
beginText() {
|
||||||
|
if (!this.text) return;
|
||||||
const dur = decodeDur(this._textValue.length);
|
const dur = decodeDur(this._textValue.length);
|
||||||
this._decT0 = this.scene.time.now;
|
this._decT0 = this.scene.time.now;
|
||||||
this._dec = new ScrambleDecode(this._textValue, this._decT0, dur);
|
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) {
|
setVoice(on) {
|
||||||
this.voiceOn = !!on;
|
this.voiceOn = !!on;
|
||||||
|
if (!this.dot) return;
|
||||||
if (this._dotTween) {
|
if (this._dotTween) {
|
||||||
this._dotTween.stop();
|
this._dotTween.stop();
|
||||||
this._dotTween = null;
|
this._dotTween = null;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue