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,
|
hull: kind === 'transport' ? 1500 : 60,
|
||||||
maxShields: kind === 'transport' ? 650 : 0,
|
maxShields: kind === 'transport' ? 650 : 0,
|
||||||
shields: 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,
|
dead: false,
|
||||||
destroyed: false,
|
destroyed: false,
|
||||||
isTransport: kind === 'transport',
|
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 ==');
|
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)');
|
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);
|
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
|
* THE CONVOY ON SCREEN — every live convoy craft (the transport + its
|
||||||
* surviving escorts) flown into view, not merely near it. The escort's
|
* 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). */
|
/** Per-frame route update — the record's OWN kind picks the law (patrol untouched). */
|
||||||
updateSideQuestRoute() {
|
updateSideQuestRoute() {
|
||||||
|
this._syncEscortSpeed(); // the escort's speed match (its own seam — a no-op off the escort lane)
|
||||||
const kind = this._sideRouteKind();
|
const kind = this._sideRouteKind();
|
||||||
if (kind === 'bounty') this.updateBountyHunt();
|
if (kind === 'bounty') this.updateBountyHunt();
|
||||||
else if (kind === 'escort') this.updateEscort();
|
else if (kind === 'escort') this.updateEscort();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue