Set Special Delivery destination from bar conversation

- Move the home-world destination from the capital departure to the Special Delivery bar vignette completion, matching the story trigger.
- Keep the capital departure only for story trail and post-sale comms, so later capital visits no longer override the player's destination.
- Show a destination-set console note in SurfaceScene, staggered behind the package-handoff note when it appears.
This commit is contained in:
Brian Fertig 2026-09-18 12:58:40 -06:00
parent e5e7f6ce78
commit da2f1fecb9
5 changed files with 222 additions and 52 deletions

View File

@ -154,7 +154,7 @@
"title": "Special Delivery", "title": "Special Delivery",
"description": "The mail routes are dead, and a sealed package that must reach the home world is sitting at the neutral capital's bar. Carol is paying for the run, and Trestle will see it off — the only question is whether your ship can hold the cargo. Meet the two of them at the bar, upgrade the hold, and deliver the package to the home world.", "description": "The mail routes are dead, and a sealed package that must reach the home world is sitting at the neutral capital's bar. Carol is paying for the run, and Trestle will see it off — the only question is whether your ship can hold the cargo. Meet the two of them at the bar, upgrade the hold, and deliver the package to the home world.",
"issuer": "Carol", "issuer": "Carol",
"_comment": "The run's THIRD quest (the 'Leaving the Nest' follow-up — the bar at the neutral capital). It GRANTS the moment 'Leaving the Nest' is COMPLETED AND CLAIMED (unlocks.claimed — the same chain mechanic as leaving_the_nest). The three checks: step 1 'vignettePlayed' (check.id 'special_delivery_bar_meeting' — the bar's dialogue scene in data/vignettes.json: Trestle + Carol, the run's played ledger registry 'vignettesPlayed' is the witness — VignetteHub marks it when the scene's DONE press completes it; the gate — the quest granted, unclaimed, this step open — is what plays it when the bar is entered). The CAPITAL LANDING runs the story ledger (registry 'story', saved as record.story — GameScene._recordLanding): the trade gate lifts (the shop's Buy/Sell tab — ShopView reads GameScene.isTradeUnlocked; the deck's Shop button itself is always live). The scene's DONE press (GameScene.onVignetteComplete) runs the handoff: FIRST the ship's hold is built out (the scene's effects.cargoHold — the run's ship starts with cargoHold 0, data/ship.json), THEN the SPECIAL PACKAGE — packageGood 'special-package', data/goods.json, sheet frame 114, tradeable:false so no shop floor touches it — lands in the hold (GameScene._handoffSpecialPackage); step 2 'cargoUpgraded' (check.id 'special-package') — the witness is the PACKAGE ABOARD: it only fits once the hold is upgraded, so its presence IS the upgrade's proof (the scene's lines are the truth — Carol's crew built the bays); step 3 'packageDelivered' — the delivery's proof (story.delivered, MONOTONIC): the package SOLD on the HOME WORLD for ZERO CREDITS — the home world is the ONE floor that takes it (TradeGrid's questSell gate — the SELL grid refuses the bay everywhere else, the tooltip reads NOT FOR SALE), priced at the good's flat sellFor (0 cr — Economy.questCargoSellPrice; the shop's Confirm is ShopView.confirm → GameScene.onQuestPackageSold, which records it — the parcel in Carol's hands IS the delivery, landing alone no longer completes the step; the sale is not buyback-eligible). The capital's launch still sets the destination back to the home world (SurfaceScene.finishTakeoff → GameScene.onSurfaceDeparture — the compass points the delivery's route). Reward: 200 credits (claimQuest's credit seam — the same as 'Leaving the Nest').", "_comment": "The run's THIRD quest (the 'Leaving the Nest' follow-up — the bar at the neutral capital). It GRANTS the moment 'Leaving the Nest' is COMPLETED AND CLAIMED (unlocks.claimed — the same chain mechanic as leaving_the_nest). The three checks: step 1 'vignettePlayed' (check.id 'special_delivery_bar_meeting' — the bar's dialogue scene in data/vignettes.json: Trestle + Carol, the run's played ledger registry 'vignettesPlayed' is the witness — VignetteHub marks it when the scene's DONE press completes it; the gate — the quest granted, unclaimed, this step open — is what plays it when the bar is entered). The CAPITAL LANDING runs the story ledger (registry 'story', saved as record.story — GameScene._recordLanding): the trade gate lifts (the shop's Buy/Sell tab — ShopView reads GameScene.isTradeUnlocked; the deck's Shop button itself is always live). The scene's DONE press (GameScene.onVignetteComplete) runs the handoff: FIRST the ship's hold is built out (the scene's effects.cargoHold — the run's ship starts with cargoHold 0, data/ship.json), THEN the SPECIAL PACKAGE — packageGood 'special-package', data/goods.json, sheet frame 114, tradeable:false so no shop floor touches it — lands in the hold (GameScene._handoffSpecialPackage), and — while the quest is HELD (granted, unclaimed) — the HOME WORLD becomes the destination (the delivery's route: the bar conversation is the trigger — Carol's 'just bring this package back to the merchant on your home world' line, and the compass points it for the flight home — GameScene.onVignetteComplete's beat 3, _specialDeliveryHeld re-reading the gate so a scene that outlived the quest can't re-aim the destination); step 2 'cargoUpgraded' (check.id 'special-package') — the witness is the PACKAGE ABOARD: it only fits once the hold is upgraded, so its presence IS the upgrade's proof (the scene's lines are the truth — Carol's crew built the bays); step 3 'packageDelivered' — the delivery's proof (story.delivered, MONOTONIC): the package SOLD on the HOME WORLD for ZERO CREDITS — the home world is the ONE floor that takes it (TradeGrid's questSell gate — the SELL grid refuses the bay everywhere else, the tooltip reads NOT FOR SALE), priced at the good's flat sellFor (0 cr — Economy.questCargoSellPrice; the shop's Confirm is ShopView.confirm → GameScene.onQuestPackageSold, which records it — the parcel in Carol's hands IS the delivery, landing alone no longer completes the step; the sale is not buyback-eligible). The capital's launch (SurfaceScene.finishTakeoff → GameScene.onSurfaceDeparture) records the delivery's trail (launchedFromCapital) and, once the sale stands, arms the payoff chain — it no longer touches the destination, so a post-quest visit to the capital (a 'Pirate Problems' return leg) leaves the player's destination alone. Reward: 200 credits (claimQuest's credit seam — the same as 'Leaving the Nest').",
"unlocks": { "unlocks": {
"_comment": "The grant condition: the listed quests' rewards CLAIMED — the run's chain (leaving_the_nest paid out → this one grants).", "_comment": "The grant condition: the listed quests' rewards CLAIMED — the run's chain (leaving_the_nest paid out → this one grants).",
"claimed": ["leaving_the_nest"] "claimed": ["leaving_the_nest"]

View File

@ -23,7 +23,7 @@
"location": "bar", "location": "bar",
"quest": "special_delivery", "quest": "special_delivery",
"step": 0, "step": 0,
"$comment": "'Special Delivery' step 1 — the neutral capital's bar: Trestle's welcome (trestle-q3-01) + Carol's four beats (carol-q3-01..04). It plays when the quest is held and the bar is entered on the capital. COMPLETING it (the DONE press) runs the scene's story beat (effects + the quest's package handoff — GameScene.onVignetteComplete): the ship's hold is built out FIRST (effects.cargoHold — the run's ship starts with cargoHold 0, data/ship.json) and THEN the 'special-package' joins the hold — Carol's 'I've asked some of my men to build out 4 cargo bays' / 'the cargo I need transported has been loaded' lines are the truth the moment the scene ENDS. The completion also marks the step: the quest's check is 'vignettePlayed' (check.id = this id) and reads the run's played ledger (registry 'vignettesPlayed' — VignetteHub marks it on completion, SaveData persists it); the 'cargoUpgraded' step stands done with the package aboard (its witness — check.id 'special-package').", "$comment": "'Special Delivery' step 1 — the neutral capital's bar: Trestle's welcome (trestle-q3-01) + Carol's four beats (carol-q3-01..04). It plays when the quest is held and the bar is entered on the capital. COMPLETING it (the DONE press) runs the scene's story beats (GameScene.onVignetteComplete): the ship's hold is built out FIRST (effects.cargoHold — the run's ship starts with cargoHold 0, data/ship.json) and THEN the 'special-package' joins the hold — Carol's 'I've asked some of my men to build out 4 cargo bays' / 'the cargo I need transported has been loaded' lines are the truth the moment the scene ENDS; and — while the quest is HELD (granted, unclaimed) — the HOME WORLD becomes the destination (the delivery's route — Carol's 'just bring this package back to the merchant on your home world' line is the trigger, and the compass points home for the flight back; the beat is gated on the quest's holding, so once the quest ends a bar visit never re-aims the destination). The completion also marks the step: the quest's check is 'vignettePlayed' (check.id = this id) and reads the run's played ledger (registry 'vignettesPlayed' — VignetteHub marks it on completion, SaveData persists it); the 'cargoUpgraded' step stands done with the package aboard (its witness — check.id 'special-package').",
"effects": { "effects": {
"$comment": "The story beat the DONE press runs (GameScene.onVignetteComplete — BEFORE the quest's package handoff, the hold must fit the package): cargoHold = the ship's unlocked cargo bays, MAX-wins (never a downgrade — the run's ship starts at 0, data/ship.json). The scene's lines are the truth: Carol's crew built out the bays.", "$comment": "The story beat the DONE press runs (GameScene.onVignetteComplete — BEFORE the quest's package handoff, the hold must fit the package): cargoHold = the ship's unlocked cargo bays, MAX-wins (never a downgrade — the run's ship starts at 0, data/ship.json). The scene's lines are the truth: Carol's crew built out the bays.",
"cargoHold": 4 "cargoHold": 4

View File

@ -4,29 +4,50 @@
* *
* node dev/delivery-launch.test.mjs * node dev/delivery-launch.test.mjs
* *
* The launch-after-sale beat (data/comms.json → trigger 'deliveredLaunch' * Two seams, pinned here:
* — carol-delivered-1/2/3 → oac-delivered-1, the four ack comms the *
* player gets when they LAUNCH after selling the special package on the * A — the launch-after-sale beat (data/comms.json → trigger
* home world): the scene-side seam is GameScene.onSurfaceDeparture * 'deliveredLaunch' — carol-delivered-1/2/3 → oac-delivered-1, the four
* (SurfaceScene.finishTakeoff → it), which arms the chain when the run's * ack comms the player gets when they LAUNCH after selling the special
* delivery witness (story.delivered — set by the shop's CONFIRM of the * package on the home world): the scene-side seam is
* package's sale, GameScene.onQuestPackageSold) stands. The comms' own * GameScene.onSurfaceDeparture (SurfaceScene.finishTakeoff → it), which
* once-ledger (js/comms/CommsHub.js) keeps the chain a one-shot, so the * arms the chain when the run's delivery witness (story.delivered — set
* seam may arm on every post-delivery departure — a later re-arm is a * by the shop's CONFIRM of the package's sale, GameScene.onQuestPackageSold)
* no-op. Pinned here: * stands. The comms' own once-ledger (js/comms/CommsHub.js) keeps the
* chain a one-shot, so the seam may arm on every post-delivery departure
* — a later re-arm is a no-op. The capital departure also records the
* story trail (launchedFromCapital) — but NO LONGER sets the
* destination (the route moved to the bar conversation — B). Pinned:
* - a post-delivery departure arms 'deliveredLaunch' exactly once * - a post-delivery departure arms 'deliveredLaunch' exactly once
* (from any world — the sale's world is the home world, not the * (from any world — the sale's world is the home world, not the
* capital); * capital);
* - a pre-delivery departure arms nothing; * - a pre-delivery departure arms nothing;
* - the capital leg still stands alongside the new arm (the departure * - the capital departure still records the trail (launchedFromCapital)
* records launchedFromCapital, routes the compass home, and refreshes * and refreshes the quest ledger — and sets NO destination (a
* the quest ledger — the pre-existing behavior this beat rides on); * post-quest visit to the capital — a 'Pirate Problems' return leg —
* - the defensive no-op (already at home) still leaves the arm in place. * leaves the player's destination alone);
* - the defensive no-op (already at home) still leaves the arm in
* place.
*
* B — the delivery's ROUTE (the destination → the home world): owned by
* the quest's bar conversation (data/vignettes.json →
* special_delivery_bar_meeting — the DONE press,
* GameScene.onVignetteComplete's beat 3): it lands only while the quest
* is HELD (granted, unclaimed — the scene's own gate already keeps it
* playing only then, GameScene._specialDeliveryHeld re-reads it as
* belt-and-braces). Pinned:
* - the bar scene's DONE press routes home (and stands the hold +
* handoff beats + the ledger refresh) while the quest is held;
* - the same DONE press sets NO destination once the quest is claimed
* (or was never granted, or belongs to another quest).
*
* (The config side of the same beat — the roster, the chain's order, and * (The config side of the same beat — the roster, the chain's order, and
* the verbatim text — is pinned in dev/comms.test.mjs.) * the verbatim text — is pinned in dev/comms.test.mjs.)
*/ */
// GameScene.onSurfaceDeparture arms 'deliveredLaunch' when story.delivered // GameScene.onSurfaceDeparture arms 'deliveredLaunch' when story.delivered
// stands, keeps the capital leg, and stays quiet otherwise. // stands, records the capital's trail without routing; GameScene.
// onVignetteComplete routes the delivery (home) only while the quest is
// held.
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
@ -89,7 +110,7 @@ function makeScene({ delivered, capital = true }) {
return scene; return scene;
} }
// 1 — post-delivery departure (any world): the payoff chain arms // A — 1: post-delivery departure (any world): the payoff chain arms
{ {
const s = makeScene({ delivered: true }); const s = makeScene({ delivered: true });
s.onSurfaceDeparture('SomePlanet'); s.onSurfaceDeparture('SomePlanet');
@ -97,24 +118,25 @@ function makeScene({ delivered, capital = true }) {
check('delivered departure (non-capital) does not claim the capital leg', s.story.launchedFromCapital === false); check('delivered departure (non-capital) does not claim the capital leg', s.story.launchedFromCapital === false);
} }
// 2 — not delivered: no arm // A — 2: not delivered: no arm
{ {
const s = makeScene({ delivered: false }); const s = makeScene({ delivered: false });
s.onSurfaceDeparture('SomePlanet'); s.onSurfaceDeparture('SomePlanet');
check('pre-delivery departure arms nothing', s.triggers.length === 0); check('pre-delivery departure arms nothing', s.triggers.length === 0);
} }
// 3 — capital leg still stands (the old behavior, alongside the new arm) // A — 3: capital departure still records the trail — but NO LONGER
// routes (the delivery's route moved to the bar conversation — B)
{ {
const s = makeScene({ delivered: true }); const s = makeScene({ delivered: true });
s.onSurfaceDeparture('CapitalWorld'); s.onSurfaceDeparture('CapitalWorld');
check('capital departure still arms deliveredLaunch (once)', JSON.stringify(s.triggers) === JSON.stringify(['deliveredLaunch'])); check('capital departure still arms deliveredLaunch (once)', JSON.stringify(s.triggers) === JSON.stringify(['deliveredLaunch']));
check('capital departure still records the leg', s.story.launchedFromCapital === true); check('capital departure still records the trail', s.story.launchedFromCapital === true);
check('capital departure still routes home', JSON.stringify(s.destination) === JSON.stringify({ id: 'sys-home', via: 'home' })); check('capital departure sets no destination (the bar scene owns the route)', s.destination === undefined);
check('capital departure still refreshes the quest ledger', s.questRefreshes === 1); check('capital departure still refreshes the quest ledger', s.questRefreshes === 1);
} }
// 4 — already home: the arm still fires (the player left home), the leg is a no-op // A — 4: already home: the arm still fires (the player left home), the leg is a no-op
{ {
const s = makeScene({ delivered: true }); const s = makeScene({ delivered: true });
s.systemRecord = { id: 'sys-home' }; s.systemRecord = { id: 'sys-home' };
@ -123,5 +145,71 @@ function makeScene({ delivered, capital = true }) {
check('home-departure sets no destination (defensive no-op)', s.destination === undefined); check('home-departure sets no destination (defensive no-op)', s.destination === undefined);
} }
// B — the bar scene's DONE press (GameScene.onVignetteComplete) is what
// routes the delivery — and only while the quest is HELD.
function makeBarScene({ granted, claimed }) {
const s = makeScene({ delivered: false });
s.questState = {
isGranted: (id) => id === 'special_delivery' && granted,
isClaimed: (id) => id === 'special_delivery' && claimed,
};
const items = [];
s.ship = {
stats: { cargoHold: 0 },
cargo: {
items,
room: 4,
has: (id) => items.includes(id),
add: (id) => { items.push(id); return 1; },
},
setCargoHold: (n) => { s.ship.stats.cargoHold = n; return n; },
};
s.refreshCreditsHud = () => {};
return s;
}
const barVig = {
id: 'special_delivery_bar_meeting',
location: 'bar',
quest: 'special_delivery',
step: 0,
effects: { cargoHold: 4 },
lines: [],
};
// B — 1: quest held (granted, unclaimed): the DONE press routes home,
// alongside the hold + handoff beats
{
const s = makeBarScene({ granted: true, claimed: false });
const beat = s.onVignetteComplete(barVig);
check('bar scene (quest held) stands the hold + handoff beats', beat.upgradedTo === 4 && beat.handoff === true);
check('bar scene (quest held) routes home', JSON.stringify(s.destination) === JSON.stringify({ id: 'sys-home', via: 'home' }));
check('bar scene (quest held) marks the routed beat', beat.routed === true);
check('bar scene (quest held) refreshes the quest ledger', s.questRefreshes === 1);
}
// B — 2: quest claimed (the scene outlived the quest — belt-and-braces):
// the beats still stand, but the destination is left alone
{
const s = makeBarScene({ granted: true, claimed: true });
const beat = s.onVignetteComplete(barVig);
check('bar scene (quest claimed) still stands the beats', beat.upgradedTo === 4 && beat.handoff === true);
check('bar scene (quest claimed) sets no destination', s.destination === undefined && beat.routed === false);
}
// B — 3: quest not granted (a drift the gate should have caught): same
// no-routing answer
{
const s = makeBarScene({ granted: false, claimed: false });
s.onVignetteComplete(barVig);
check('bar scene (quest not granted) sets no destination', s.destination === undefined);
}
// B — 4: a scene for ANOTHER quest never routes the delivery
{
const s = makeBarScene({ granted: true, claimed: false });
s.onVignetteComplete({ ...barVig, id: 'other_quest_bar', quest: 'pirate_problems' });
check('a non-delivery scene sets no destination', s.destination === undefined);
}
if (failures > 0) { console.error(`\n${failures} delivery-launch check(s) failed`); process.exit(1); } if (failures > 0) { console.error(`\n${failures} delivery-launch check(s) failed`); process.exit(1); }
console.log('\ndelivery-launch: all checks passed'); console.log('\ndelivery-launch: all checks passed');

View File

@ -3569,14 +3569,19 @@ export class GameScene extends Phaser.Scene {
// delivery's proof (the sale is — below); // delivery's proof (the sale is — below);
// · onVignetteComplete — the bar scene's DONE press: the hold is // · onVignetteComplete — the bar scene's DONE press: the hold is
// built out (the scene's effects) and the // built out (the scene's effects) and the
// special package lands in the hold; // special package lands in the hold, and —
// while the quest is held — the HOME WORLD
// becomes the destination (the delivery's
// route — the compass points it); the bar
// conversation IS the trigger;
// · onSurfaceDeparture — SurfaceScene.finishTakeoff (a real // · onSurfaceDeparture — SurfaceScene.finishTakeoff (a real
// launch): after the package's sale it // launch): after the package's sale it
// arms the 'deliveredLaunch' comms chain // arms the 'deliveredLaunch' comms chain
// (Carol ×3 → OAC's sign-off, the payoff); // (Carol ×3 → OAC's sign-off, the payoff);
// the capital launch sets the destination // the capital launch records the delivery
// back to the HOME WORLD (the delivery's // leg (launchedFromCapital). It no longer
// route — the compass points it); // touches the destination — the bar
// conversation owns the route (above);
// · onQuestPackageSold — the shop's CONFIRM of the package's sale: // · onQuestPackageSold — the shop's CONFIRM of the package's sale:
// saleable on the home world ONLY (TradeGrid's // saleable on the home world ONLY (TradeGrid's
// gate), at zero credits — that sale is // gate), at zero credits — that sale is
@ -3678,13 +3683,26 @@ export class GameScene extends Phaser.Scene {
* the 'special-package' joins the hold — AFTER the upgrade (the * the 'special-package' joins the hold — AFTER the upgrade (the
* hold must fit it). Its presence is the 'cargoUpgraded' step's * hold must fit it). Its presence is the 'cargoUpgraded' step's
* witness (the check's id) — the parcel aboard proves the ship * witness (the check's id) — the parcel aboard proves the ship
* can hold cargo. * can hold cargo;
* 3. the DELIVERY ROUTE (the 'Special Delivery' bar scene, and only
* while the quest is HELD — granted, unclaimed): the HOME WORLD
* becomes the run's destination the moment the conversation is
* over. Carol's line set it up ('just bring this package back to
* the merchant on your home world' — data/vignettes.json →
* special_delivery_bar_meeting): hearing it IS the trigger, so
* the compass points home from the bar's DONE press onward. The
* scene's own gate (VignetteModel.vignetteAvailable — granted +
* unclaimed + the step open) already keeps it playing only during
* the quest, and _specialDeliveryHeld re-verifies that here
* (belt and braces — the route is the quest's, and a scene that
* outlived it must not re-aim the destination once it ends).
* _questRefresh then stands the just-made steps (steps 1 + 2) DONE. * _questRefresh then stands the just-made steps (steps 1 + 2) DONE.
* @returns {{upgradedTo: number|null, handoff: boolean}} the beats that * @returns {{upgradedTo: number|null, handoff: boolean, routed: boolean}}
* fired (the SurfaceScene calls them out in its console slot) * the beats that fired (the SurfaceScene calls them out in its
* console slot)
*/ */
onVignetteComplete(vig) { onVignetteComplete(vig) {
const out = { upgradedTo: null, handoff: false }; const out = { upgradedTo: null, handoff: false, routed: false };
if (!vig) return out; if (!vig) return out;
// 1 — the hold's built out (the scene's effects — pure data the // 1 — the hold's built out (the scene's effects — pure data the
// model kept): MAX-wins, never a downgrade (a later hold — a // model kept): MAX-wins, never a downgrade (a later hold — a
@ -3703,13 +3721,47 @@ export class GameScene extends Phaser.Scene {
if (questId !== '') { if (questId !== '') {
const goodId = questDef(questId)?.packageGood; const goodId = questDef(questId)?.packageGood;
if (goodId && this._handoffSpecialPackage(goodId)) out.handoff = true; if (goodId && this._handoffSpecialPackage(goodId)) out.handoff = true;
// 3 — the delivery ROUTE: the bar conversation is the trigger (the
// quest's bar scene, and only while the quest stands) — the home
// world becomes the destination the moment the scene completes, so
// the compass points the delivery's route for the flight home.
// setDestination does the rest (the route derives from it, the
// compass marks the gate to fly; its own toast sits on the sleeping
// flight scene, so the SurfaceScene calls this beat out in its
// console slot — the scout's SET DESTINATION is the same shape).
if (questId === 'special_delivery' && this._specialDeliveryHeld()) {
const homeId = this.galaxy?.homeSystemId;
if (homeId && this.systemRecord?.id !== homeId) {
this.setDestination(homeId, 'home'); // the compass points home
out.routed = true;
}
}
} }
if (out.upgradedTo != null || out.handoff) { if (out.upgradedTo != null || out.handoff || out.routed) {
this._questRefresh(); // steps 1 + 2 may stand done now this._questRefresh(); // steps 1 + 2 may stand done now
} }
return out; return out;
} }
/**
* Is the 'Special Delivery' quest HELD (granted, unclaimed)? The bar
* scene's delivery route (onVignetteComplete, beat 3 — the destination
* → the home world) lands only while it stands: the scene itself plays
* only under the same gate (VignetteModel.vignetteAvailable — granted
* + unclaimed + the step open), so this read is the seam's
* belt-and-braces against the scene outliving the quest (a ledger
* drifted by a save, a re-armed scene) — once the quest ends, the bar
* must not re-aim the player's destination.
*/
_specialDeliveryHeld() {
if (!this.questState) return false;
const qid = 'special_delivery';
return (
this.questState.isGranted(qid) === true &&
this.questState.isClaimed(qid) !== true
);
}
/** /**
* The surface TAKE OFF seam (SurfaceScene.finishTakeoff → here). * The surface TAKE OFF seam (SurfaceScene.finishTakeoff → here).
* Two beats ride on it: * Two beats ride on it:
@ -3727,25 +3779,24 @@ export class GameScene extends Phaser.Scene {
* run — the hub plays the chain once the flight world is back * run — the hub plays the chain once the flight world is back
* (the FIFO, each ack holding until its own ACKNOWLEDGE). * (the FIFO, each ack holding until its own ACKNOWLEDGE).
* *
* 2 — 'Special Delivery' step 3's leg: launching off the NEUTRAL * 2 — the CAPITAL DEPARTURE leg: launching off the NEUTRAL CAPITAL
* CAPITAL (the bar is done, the package is aboard) records the * (the bar is done, the package is aboard) records the launch in
* launch in the story ledger and sets the run's destination back * the story ledger (launchedFromCapital — the delivery's trail).
* to the HOME WORLD (the delivery's route — the compass marks it, * A launch off any other world is a no-op for this leg. This seam
* and the REACHED toast fires on the home world's arrival via * NO LONGER touches the destination: the delivery's route
* _checkDestinationReached). A launch off any other world is a * (destination → the home world) is owned by the bar conversation
* no-op for this leg — the quest's auto-course earns itself only * itself — the quest's bar scene DONE press sets it, while the
* from the capital's departure. * quest is held (onVignetteComplete, beat 3) — so a post-quest
* visit to the capital (a 'Pirate Problems' return) leaves the
* player's destination alone.
*/ */
onSurfaceDeparture(worldName) { onSurfaceDeparture(worldName) {
if (this.story?.delivered === true) { if (this.story?.delivered === true) {
this.comms?.trigger('deliveredLaunch'); // the payoff chain — the once-ledger dedupes this.comms?.trigger('deliveredLaunch'); // the payoff chain — the once-ledger dedupes
} }
if (!this.story || !this.isCapitalWorld(worldName)) return; if (!this.story || !this.isCapitalWorld(worldName)) return;
this.story.launchedFromCapital = true; // the leg is made this.story.launchedFromCapital = true; // the trail: the capital's departure
this._questRefresh(); // the delivery leg may have just completed this._questRefresh(); // a step may have just landed (defensive)
const homeId = this.galaxy?.homeSystemId;
if (!homeId || this.systemRecord?.id === homeId) return; // already home (defensive)
this.setDestination(homeId, 'home'); // the compass points home
} }
/** /**

View File

@ -708,9 +708,13 @@ export class SurfaceScene extends Phaser.Scene {
* run's ship starts with cargoHold 0, data/ship.json), THEN the * run's ship starts with cargoHold 0, data/ship.json), THEN the
* scene's quest's package joins the hold (it must fit — and its * scene's quest's package joins the hold (it must fit — and its
* presence is the 'cargoUpgraded' step's witness, so both steps stand * presence is the 'cargoUpgraded' step's witness, so both steps stand
* done at the DONE press). The surface calls the beats out in its * done at the DONE press), and — the 'Special Delivery' bar scene,
* console slot (the flight HUD sits behind the opaque plate for the * while the quest is held — the HOME WORLD becomes the destination
* whole stay). The bar is the loop again; the shop raises its * (the delivery's route: the bar conversation is the trigger). The
* surface calls the beats out in its console slot (the flight HUD
* sits behind the opaque plate for the whole stay — the game scene's
* own toasts would sit frozen on it; the scout's SET DESTINATION note
* is the same recipe). The bar is the loop again; the shop raises its
* buy/sell view now — it deferred from entry while the scene held * buy/sell view now — it deferred from entry while the scene held
* (the scene plays before the player can touch buy/sell). * (the scene plays before the player can touch buy/sell).
*/ */
@ -731,6 +735,27 @@ export class SurfaceScene extends Phaser.Scene {
durationMs: 3600, durationMs: 3600,
}); });
} }
if (beat?.routed === true) {
// The delivery route (onVignetteComplete's beat 3 — the bar
// conversation just set the destination to the home world): the
// surface calls it out here (the game scene's own toast sits on
// the sleeping scene), the map's SET DESTINATION toast in shape.
// Staggered behind the package note (the slot is one-at-a-time —
// a new note replaces the old): the beats read in order, each for
// a full hold, and a take-off before the delay simply drops the
// echo (the destination is set either way — the compass shows it).
const homeId = g?.galaxy?.homeSystemId;
const plan = homeId ? planRoute(g.galaxy, g.systemRecord?.id, homeId) : null;
const dest = String(g?.homeWorldName ?? 'HOME WORLD').toUpperCase();
const label = `DESTINATION SET — ${dest}` + (plan?.hops != null ? ` \u00b7 ${plan.hops} HOP(S)` : '');
const note = () => this.consoleNote(label, {
glyph: '\u25c6',
glyphColor: toCss(config.get('gates.route.compassColor', '#ff8c1a')),
durationMs: 3600,
});
if (beat?.handoff === true) this.time.delayedCall(3200, note); // behind the package note
else note(); // no package beat to read first — land it now
}
if (this.shopMode) this.buildShopView(); if (this.shopMode) this.buildShopView();
this.vignetteLocation = null; this.vignetteLocation = null;
} }
@ -1743,10 +1768,16 @@ export class SurfaceScene extends Phaser.Scene {
this.destroyVideo(this.takeoffVideo); this.destroyVideo(this.takeoffVideo);
this.takeoffVideo = null; this.takeoffVideo = null;
// The 'Special Delivery' departure seam (GameScene.onSurfaceDeparture): // The 'Special Delivery' departure seam (GameScene.onSurfaceDeparture):
// a launch OFF the neutral capital is the delivery's leg — the run's // a launch OFF the neutral capital records the delivery's trail
// destination is set back to the home world (the compass points it). // (launchedFromCapital — the story ledger) and, once the package's
// Runs now, while the GameScene still owns the run: the wake back to // home-world sale stands, arms the 'deliveredLaunch' payoff chain.
// the flight world is the beat the quest's step 3 waits on. // It no longer sets the destination — the delivery's route is owned
// by the bar conversation itself (the quest's bar scene DONE press,
// GameScene.onVignetteComplete, while the quest is held), so a
// post-quest departure (a 'Pirate Problems' return leg) leaves the
// player's destination alone. Runs now, while the GameScene still
// owns the run: the wake back to the flight world is the beat the
// quest's step 3 waits on.
this.gameScene?.onSurfaceDeparture?.(this.planetName); this.gameScene?.onSurfaceDeparture?.(this.planetName);
// The comms beat (data/comms.json: tetherL2Launch — oac-relaunch → // The comms beat (data/comms.json: tetherL2Launch — oac-relaunch →
// oac-explore): the player is launching back into space with the // oac-explore): the player is launching back into space with the