Sync escort convoy speed to transport while visible and out of combat
- Add `_syncEscortSpeed()` in GameScene: when an escort contract is open, the transport is on screen, and combat is inactive, cap the player hull and escort fighters at the transport's own top speed; restore each craft to its normal/roster speed otherwise. - Fighters keep their base speed on `_baseSpeed` so the restore is exact even after a jump re-form. - Per-frame writes only on change to avoid steady-state churn or steering disturbance. - Hook the sync into `updateSideQuestRoute()` so it runs each frame off the escort lane (no-op elsewhere). - Cover the new behavior in `escort-flow.test.mjs`: match applies in view/out-of-combat, restores on combat open, transport off-screen, and contract end; give test crafts distinct speeds so match and restore are observable.
This commit is contained in:
parent
fdd0b4900c
commit
f49051be69
|
|
@ -102,6 +102,11 @@ function makeCraft(kind) {
|
|||
hull: kind === 'transport' ? 1500 : 60,
|
||||
maxShields: kind === 'transport' ? 650 : 0,
|
||||
shields: kind === 'transport' ? 650 : 0,
|
||||
// The craft's OWN top speed (the real TransportShip/NeutralShip read their
|
||||
// data into this; the speed-match law reads it off the craft). The fighters
|
||||
// carry their roster speed (180) — distinct from the transport's 245, so
|
||||
// the match and the restore are both observable.
|
||||
speed: kind === 'transport' ? 245 : 180,
|
||||
dead: false,
|
||||
destroyed: false,
|
||||
isTransport: kind === 'transport',
|
||||
|
|
@ -478,6 +483,63 @@ console.log('== THE DESTINATION’S DOOR — the landing holds until the transpo
|
|||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
console.log('== THE SPEED MATCH — escort visible + out of combat: hull + fighters match the transport ==');
|
||||
{
|
||||
const scene = makeScene('sys-home');
|
||||
const st = scene.sideQuestState;
|
||||
const posting = {
|
||||
id: JOB, systemId: 'sys-home', systemName: 'Hearth', category: 'escort',
|
||||
title: 'Convoy Night', label: 'ESCORT', reward: { credits: 150 },
|
||||
};
|
||||
st.accept(JOB);
|
||||
st.setActive(JOB);
|
||||
scene._establishEscort(JOB, posting);
|
||||
const normal = Number(config.get('ship.maxSpeed', 480)) || 480;
|
||||
scene.ship.maxSpeed = normal;
|
||||
// LINK the convoy (the ship reaches the rendezvous; the transport forms).
|
||||
const wp = st.routeFor(JOB).waypoints[0];
|
||||
scene.ship.x = wp[0]; scene.ship.y = wp[1];
|
||||
scene.updateEscort();
|
||||
assert.equal(st.routeFor(JOB).escort.phase, 'transit', 'the convoy is linked');
|
||||
const tr = scene._escortTransport;
|
||||
assert.ok(tr && !tr.dead, 'the transport is standing');
|
||||
|
||||
// TRANSPORT ON SCREEN + OUT OF COMBAT — the convoy flies as one: the hull
|
||||
// takes the transport's cap, and the escort fighters match its speed.
|
||||
tr.x = 0; tr.y = 0; // the camera's home view (scroll 0,0 — the craft is in view)
|
||||
scene.combat.active = false;
|
||||
const crew = scene.combat.allies.filter((a) => a._escortCrew);
|
||||
assert.ok(crew.length >= 1, 'the escort fighters stand on the wing');
|
||||
scene._syncEscortSpeed();
|
||||
assert.equal(scene.ship.maxSpeed, tr.speed, "the player's top speed MATCHES the transport's");
|
||||
for (const f of crew) assert.equal(f.speed, tr.speed, 'the escort fighters match the transport\u2019s speed');
|
||||
|
||||
// COMBAT OPENS — everyone falls back to their normal top speed.
|
||||
scene.combat.active = true;
|
||||
scene._syncEscortSpeed();
|
||||
assert.equal(scene.ship.maxSpeed, normal, 'in combat the hull returns to NORMAL speed');
|
||||
for (const f of crew) assert.equal(f.speed, f._baseSpeed, 'in combat the fighters return to their OWN speed');
|
||||
|
||||
// COMBAT CLOSES, but the transport is OUT OF VIEW — still normal.
|
||||
scene.combat.active = false;
|
||||
tr.x = 99999; tr.y = 99999;
|
||||
scene._syncEscortSpeed();
|
||||
assert.equal(scene.ship.maxSpeed, normal, 'transport off-screen: the hull stays NORMAL');
|
||||
for (const f of crew) assert.equal(f.speed, f._baseSpeed, '…and the fighters hold their own speed');
|
||||
|
||||
// BACK IN VIEW — the match re-arms for the whole convoy; the close of the
|
||||
// escort releases it.
|
||||
tr.x = 0; tr.y = 0;
|
||||
scene._syncEscortSpeed();
|
||||
assert.equal(scene.ship.maxSpeed, tr.speed, 'back in view: the match re-arms');
|
||||
for (const f of crew) assert.equal(f.speed, tr.speed, '…for the fighters too');
|
||||
st.routeFor(JOB).escort.phase = 'delivered'; // the contract closes
|
||||
scene._syncEscortSpeed();
|
||||
assert.equal(scene.ship.maxSpeed, normal, 'escort complete: the hull returns to NORMAL speed');
|
||||
for (const f of crew) assert.equal(f.speed, f._baseSpeed, '…and the fighters return to their OWN speed');
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
console.log('== THE BOARD — all TWENTY escort dossiers stand and render ==');
|
||||
{
|
||||
|
|
@ -508,4 +570,4 @@ console.log('== THE BOARD — all TWENTY escort dossiers stand and render ==');
|
|||
assert.equal(seen.size, 20, '…all twenty distinct (no two collapse to one row)');
|
||||
}
|
||||
|
||||
console.log('\nescort-flow: OK — the run (rendezvous→link→wave→jump→delivery), the witness gates (convoy + gate on screen before the jump; the convoy on screen before the landing), the destination’s door (no landing until the transport reaches it or is lost), the failure (loss→land→reset→retry), the board (20 dossiers)');
|
||||
console.log('\nescort-flow: OK — the run (rendezvous→link→wave→jump→delivery), the witness gates (convoy + gate on screen before the jump; the convoy on screen before the landing), the destination’s door (no landing until the transport reaches it or is lost), the speed match (transport in view + out of combat: the hull and the escort fighters fly at the transport’s top speed; combat / off-screen / contract-end restore everyone), the failure (loss→land→reset→retry), the board (20 dossiers)');
|
||||
|
|
|
|||
|
|
@ -6024,6 +6024,49 @@ export class GameScene extends Phaser.Scene {
|
|||
return circleInView(a.x, a.y, r, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* THE ESCORT'S SPEED MATCH — while the escort contract is still open
|
||||
* (rendezvous / transit / delivering — 'failed' and 'delivered' are over,
|
||||
* as is a claimed or completed route) AND the transport is VISIBLE ON
|
||||
* SCREEN (its own witness law, _escortCraftOnScreen) AND the player is OUT
|
||||
* OF COMBAT (this.combat.active is the same predicate that raises the
|
||||
* combat bar), the convoy's speed law goes to the TRANSPORT: the player's
|
||||
* hull takes the transport's own top speed (data/combat.json → transport.speed,
|
||||
* riding the craft's `speed`) as its cap, and the escort FIGHTERS match the
|
||||
* transport's speed for the flight (their own base is kept on `_baseSpeed`
|
||||
* so the restore is exact). The moment the transport drops out of view,
|
||||
* combat opens, or the contract ends, every one of them falls back to its
|
||||
* NORMAL speed (data/ship.json → ship.maxSpeed for the hull; the fighters'
|
||||
* own roster speed). A per-frame write only on change (no steady-state
|
||||
* churn, no steering disturbance).
|
||||
*/
|
||||
_syncEscortSpeed() {
|
||||
const ship = this.ship;
|
||||
const normal = Number(config.get('ship.maxSpeed', 480)) || 480;
|
||||
const sel = this._escortRoute?.();
|
||||
const esc = sel?.route?.escort;
|
||||
const tr = this._escortTransport;
|
||||
const open = esc && (esc.phase === 'rendezvous' || esc.phase === 'transit' || esc.phase === 'delivering');
|
||||
const matching = !!(open && tr && !tr.dead && !tr._removed && !this.combat?.active && this._escortCraftOnScreen(tr));
|
||||
if (ship) {
|
||||
const target = matching ? (Number(tr.speed) || normal) : normal;
|
||||
if (Number.isFinite(target) && ship.maxSpeed !== target) ship.maxSpeed = target;
|
||||
}
|
||||
// THE ESCORT FIGHTERS — match the transport's speed (the convoy flies as
|
||||
// one), else restore each fighter's own base (kept on first sight — a
|
||||
// jump's re-form stands a fresh sprite with its roster speed again).
|
||||
if (ship || tr) {
|
||||
const ref = matching ? (Number(tr.speed) || null) : null;
|
||||
for (const a of this._escortConvoyCrafts()) {
|
||||
if (a === tr || !a || a.dead || a._removed) continue;
|
||||
if (typeof a.speed !== 'number' || !Number.isFinite(a.speed)) continue;
|
||||
if (!Number.isFinite(a._baseSpeed)) a._baseSpeed = a.speed;
|
||||
const want = ref !== null ? ref : a._baseSpeed;
|
||||
if (a.speed !== want) a.speed = want;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* THE CONVOY ON SCREEN — every live convoy craft (the transport + its
|
||||
* surviving escorts) flown into view, not merely near it. The escort's
|
||||
|
|
@ -6444,6 +6487,7 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
/** Per-frame route update — the record's OWN kind picks the law (patrol untouched). */
|
||||
updateSideQuestRoute() {
|
||||
this._syncEscortSpeed(); // the escort's speed match (its own seam — a no-op off the escort lane)
|
||||
const kind = this._sideRouteKind();
|
||||
if (kind === 'bounty') this.updateBountyHunt();
|
||||
else if (kind === 'escort') this.updateEscort();
|
||||
|
|
|
|||
Loading…
Reference in New Issue