Add 'Special Delivery' payoff comms chain on surface departure
- Register Carol as a comm sender with her video clip and role - Add four ack beats (carol-delivered-1/2/3, oac-delivered-1) under the new 'deliveredLaunch' trigger, fired when the player launches after selling the package - Arm the chain in GameScene.onSurfaceDeparture once story.delivered is set, with the once-ledger keeping it a one-shot - Update comms tests to cover Carol's registration, the four-beat sequence ordering, and re-arm deduplication
This commit is contained in:
parent
f38b3c0b86
commit
8ff7f0ee12
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,11 @@
|
|||
* trestle-meetup → trestle-quest on firstWorld,
|
||||
* trestle-brief-1 → trestle-brief-2 → trestle-brief-3 (the
|
||||
* 'Leaving the Nest' three-brief sequence, ack) on trestleBrief,
|
||||
* trestle-arrive-1 → trestle-arrive-2 (ack) on trestleArrival);
|
||||
* trestle-arrive-1 → trestle-arrive-2 (ack) on trestleArrival,
|
||||
* carol-delivered-1 → carol-delivered-2 → carol-delivered-3 →
|
||||
* oac-delivered-1 (the 'Special Delivery' payoff — the launch back
|
||||
* into space after the package's home-world sale, ack) on
|
||||
* deliveredLaunch, with Carol registered as a sender);
|
||||
* - the reserved triggers are RESERVED (no comm uses them yet);
|
||||
* - normalizeComm drops what can't be shown (bad type / no trigger /
|
||||
* no text for a box / no speech for an audio comm / unknown sender),
|
||||
|
|
@ -76,8 +80,12 @@ check('OAC is registered (label + role + clip)', !!oac && oac.label === 'O.A.C.'
|
|||
const trestle = charEntry(cfg, 'trestle');
|
||||
check('Trestle is registered (label + role + his own clip)', !!trestle && trestle.label === 'TRESTLE' && !!trestle.role && trestle.video === 'assets/videos/characters/trestle.mp4');
|
||||
|
||||
const carol = charEntry(cfg, 'carol');
|
||||
check('Carol is registered (label + role + her own clip)', !!carol && carol.label === 'CAROL' && !!carol.role && carol.video === 'assets/videos/characters/carol.mp4');
|
||||
check('Carol\'s space clip ships on disk', existsSync(join(root, 'assets/videos/characters/carol.mp4')));
|
||||
|
||||
const roster = listComms(cfg);
|
||||
check('the roster holds exactly the shipped comms', roster.length === 20);
|
||||
check('the roster holds exactly the shipped comms', roster.length === 24);
|
||||
const byId = Object.fromEntries(roster.map((c) => [c.id, c]));
|
||||
const intro = byId['oac-intro'];
|
||||
check(
|
||||
|
|
@ -337,6 +345,70 @@ check('trestle-arrive-2: the bar/credits brief (Carol + the 200-credit top-up)',
|
|||
check('trestle-arrive-2: speaks trestle/q2-05.mp3', byId['trestle-arrive-2']?.audio === 'assets/speech/trestle/q2-05.mp3');
|
||||
check('trestle-arrive-2: the clip ships on disk', existsSync(join(root, byId['trestle-arrive-2']?.audio ?? '')));
|
||||
|
||||
// The 'Special Delivery' payoff — the launch back into space after the
|
||||
// package's home-world sale (GameScene.onSurfaceDeparture arms
|
||||
// 'deliveredLaunch' when the run's delivery witness, story.delivered,
|
||||
// stands; the comms' once-ledger keeps the four-beat chain a one-shot —
|
||||
// a later re-arm is a no-op). Carol's thanks + trade sign-off, then
|
||||
// OAC's confirmation that the buy/sell codes are live on every world.
|
||||
check(
|
||||
'carol-delivered-1: the thanks, beat 1/4 (ack, deliveredLaunch, carol, once)',
|
||||
!!byId['carol-delivered-1'] &&
|
||||
byId['carol-delivered-1'].from === 'carol' &&
|
||||
byId['carol-delivered-1'].type === 'ack' &&
|
||||
byId['carol-delivered-1'].trigger === 'deliveredLaunch' &&
|
||||
byId['carol-delivered-1'].once === true,
|
||||
);
|
||||
check('carol-delivered-1: the thanks (verbatim)', byId['carol-delivered-1']?.text === 'Thank you. I appreciate you taking care of that for me.');
|
||||
check('carol-delivered-1: speaks carol/q3-05.mp3', byId['carol-delivered-1']?.audio === 'assets/speech/carol/carol-q3-05.mp3');
|
||||
check('carol-delivered-1: the clip ships on disk', existsSync(join(root, byId['carol-delivered-1']?.audio ?? '')));
|
||||
check('carol-delivered-1: lands after the take-off settles (delayMs > 0)', (byId['carol-delivered-1']?.delayMs ?? 0) > 0);
|
||||
check(
|
||||
'carol-delivered-2: the trade sign-off, beat 2/4 (ack, deliveredLaunch, once, immediate)',
|
||||
!!byId['carol-delivered-2'] &&
|
||||
byId['carol-delivered-2'].from === 'carol' &&
|
||||
byId['carol-delivered-2'].type === 'ack' &&
|
||||
byId['carol-delivered-2'].trigger === 'deliveredLaunch' &&
|
||||
byId['carol-delivered-2'].once === true &&
|
||||
(byId['carol-delivered-2']?.delayMs ?? 0) === 0,
|
||||
);
|
||||
check(
|
||||
'carol-delivered-2: the unlock brief (the brief, verbatim)',
|
||||
byId['carol-delivered-2']?.text === 'All shops in the galaxy are now fully unlocked for you. Trade is an important an often lucrative way to make money in the galaxy.',
|
||||
);
|
||||
check('carol-delivered-2: speaks carol/q3-06.mp3', byId['carol-delivered-2']?.audio === 'assets/speech/carol/carol-q3-06.mp3');
|
||||
check('carol-delivered-2: the clip ships on disk', existsSync(join(root, byId['carol-delivered-2']?.audio ?? '')));
|
||||
check(
|
||||
'carol-delivered-3: the risk/yield hint, beat 3/4 (ack, deliveredLaunch, once, immediate)',
|
||||
!!byId['carol-delivered-3'] &&
|
||||
byId['carol-delivered-3'].from === 'carol' &&
|
||||
byId['carol-delivered-3'].type === 'ack' &&
|
||||
byId['carol-delivered-3'].trigger === 'deliveredLaunch' &&
|
||||
byId['carol-delivered-3'].once === true &&
|
||||
(byId['carol-delivered-3']?.delayMs ?? 0) === 0,
|
||||
);
|
||||
check(
|
||||
'carol-delivered-3: the nuance brief (the brief, verbatim)',
|
||||
byId['carol-delivered-3']?.text === 'The higher the risk, the higher the rewards. Long routes and different planet types have different yields on products. I\'ll leave it to you to figure out the nuance.',
|
||||
);
|
||||
check('carol-delivered-3: speaks carol/q3-07.mp3', byId['carol-delivered-3']?.audio === 'assets/speech/carol/carol-q3-07.mp3');
|
||||
check('carol-delivered-3: the clip ships on disk', existsSync(join(root, byId['carol-delivered-3']?.audio ?? '')));
|
||||
check(
|
||||
'oac-delivered-1: the buy/sell confirmation, beat 4/4 (ack, deliveredLaunch, oac, once, immediate)',
|
||||
!!byId['oac-delivered-1'] &&
|
||||
byId['oac-delivered-1'].from === 'oac' &&
|
||||
byId['oac-delivered-1'].type === 'ack' &&
|
||||
byId['oac-delivered-1'].trigger === 'deliveredLaunch' &&
|
||||
byId['oac-delivered-1'].once === true &&
|
||||
(byId['oac-delivered-1']?.delayMs ?? 0) === 0,
|
||||
);
|
||||
check(
|
||||
'oac-delivered-1: the codes brief (the brief, verbatim)',
|
||||
byId['oac-delivered-1']?.text === "I've confirmed. We have buy and sell codes on all worlds in the galaxy. This is a big deal. We can make real credits now.",
|
||||
);
|
||||
check('oac-delivered-1: speaks oac/q3-01.mp3', byId['oac-delivered-1']?.audio === 'assets/speech/oac/oac-q3-01.mp3');
|
||||
check('oac-delivered-1: the clip ships on disk', existsSync(join(root, byId['oac-delivered-1']?.audio ?? '')));
|
||||
|
||||
check(
|
||||
'tetherResearch arms the pair in config order (minerals → fly-mine)',
|
||||
JSON.stringify(commsForTrigger(cfg, 'tetherResearch').map((c) => c.id)) === JSON.stringify(['oac-minerals', 'oac-fly-mine']),
|
||||
|
|
@ -349,6 +421,20 @@ check('researchComplete arms the sign-off', commsForTrigger(cfg, 'researchComple
|
|||
check('systemComplete arms the milestone comm', commsForTrigger(cfg, 'systemComplete').length === 1 && commsForTrigger(cfg, 'systemComplete')[0].id === 'oac-system-complete');
|
||||
check('minerals200 arms the 200-mineral beat', commsForTrigger(cfg, 'minerals200').length === 1 && commsForTrigger(cfg, 'minerals200')[0].id === 'oac-tether2');
|
||||
check('tetherL2Launch arms the launch-back pair in config order', JSON.stringify(commsForTrigger(cfg, 'tetherL2Launch').map((c) => c.id)) === JSON.stringify(['oac-relaunch', 'oac-explore']));
|
||||
check(
|
||||
'deliveredLaunch arms the payoff chain in config order (Carol ×3 → OAC)',
|
||||
JSON.stringify(commsForTrigger(cfg, 'deliveredLaunch').map((c) => c.id)) ===
|
||||
JSON.stringify(['carol-delivered-1', 'carol-delivered-2', 'carol-delivered-3', 'oac-delivered-1']),
|
||||
);
|
||||
check(
|
||||
'a played carol-delivered-1 does not re-arm (the rest of the chain still does)',
|
||||
JSON.stringify(commsForTrigger(cfg, 'deliveredLaunch', new Set(['carol-delivered-1'])).map((c) => c.id)) ===
|
||||
JSON.stringify(['carol-delivered-2', 'carol-delivered-3', 'oac-delivered-1']),
|
||||
);
|
||||
check(
|
||||
'a fully played payoff chain re-arms nothing (the one-shot)',
|
||||
commsForTrigger(cfg, 'deliveredLaunch', new Set(['carol-delivered-1', 'carol-delivered-2', 'carol-delivered-3', 'oac-delivered-1'])).length === 0,
|
||||
);
|
||||
check('firstWorld arms the trestle pair in config order', JSON.stringify(commsForTrigger(cfg, 'firstWorld').map((c) => c.id)) === JSON.stringify(['trestle-meetup', 'trestle-quest']));
|
||||
|
||||
check(
|
||||
|
|
@ -420,9 +506,9 @@ warnOn = true;
|
|||
// unknown senders are dropped by the ROSTER (listComms), not normalize
|
||||
const withGhost = { ...cfg, comms: [...cfg.comms, { ...base, id: 'ghost', from: 'wraith' }] };
|
||||
const ghostList = listComms(withGhost);
|
||||
check('drop: unknown sender (the roster keeps the rest)', ghostList.length === 20 && ghostList[0].id === 'oac-intro');
|
||||
check('drop: unknown sender (the roster keeps the rest)', ghostList.length === 24 && ghostList[0].id === 'oac-intro');
|
||||
const dupList = listComms({ ...cfg, comms: [...cfg.comms, { ...cfg.comms[0] }] });
|
||||
check('drop: duplicate ids (first wins)', dupList.length === 20 && dupList[0].id === 'oac-intro');
|
||||
check('drop: duplicate ids (first wins)', dupList.length === 24 && dupList[0].id === 'oac-intro');
|
||||
warnOn = true;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3088,9 +3088,12 @@ export class GameScene extends Phaser.Scene {
|
|||
// built out (the scene's effects) and the
|
||||
// special package lands in the hold;
|
||||
// · onSurfaceDeparture — SurfaceScene.finishTakeoff (a real
|
||||
// launch): the capital launch sets the
|
||||
// destination back to the HOME WORLD (the
|
||||
// delivery's route — the compass points it);
|
||||
// launch): after the package's sale it
|
||||
// arms the 'deliveredLaunch' comms chain
|
||||
// (Carol ×3 → OAC's sign-off, the payoff);
|
||||
// the capital launch sets the destination
|
||||
// back to the HOME WORLD (the delivery's
|
||||
// route — the compass points it);
|
||||
// · onQuestPackageSold — the shop's CONFIRM of the package's sale:
|
||||
// saleable on the home world ONLY (TradeGrid's
|
||||
// gate), at zero credits — that sale is
|
||||
|
|
@ -3226,17 +3229,36 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
/**
|
||||
* The surface TAKE OFF seam (SurfaceScene.finishTakeoff → here).
|
||||
* 'Special Delivery' step 3's leg: launching off the NEUTRAL CAPITAL
|
||||
* (the bar is done, the package is aboard) records the launch in the
|
||||
* story ledger and sets the run's destination back to the HOME WORLD
|
||||
* (the delivery's route — the compass marks it, and the REACHED toast
|
||||
* fires on the home world's arrival via _checkDestinationReached).
|
||||
* A launch off any other world is a no-op — the quest's auto-course
|
||||
* earns itself only from the capital's departure.
|
||||
* Two beats ride on it:
|
||||
*
|
||||
* 1 — the 'Special Delivery' PAYOFF (data/comms.json →
|
||||
* 'deliveredLaunch'): the launch back into space AFTER the
|
||||
* package's home-world sale — Carol's bar line set it up ('After
|
||||
* you've done that, launch back into space and I'll contact you',
|
||||
* data/vignettes.json → special_delivery_bar_meeting). Witnessed by
|
||||
* the run's story ledger (story.delivered — set by the shop's
|
||||
* CONFIRM of the package's sale, onQuestPackageSold), armed here
|
||||
* on any departure once it stands; the comms' once-ledger keeps
|
||||
* the four-beat chain (Carol ×3 → OAC's sign-off) a one-shot and
|
||||
* any later re-arm a no-op. Queued while this scene still owns the
|
||||
* run — the hub plays the chain once the flight world is back
|
||||
* (the FIFO, each ack holding until its own ACKNOWLEDGE).
|
||||
*
|
||||
* 2 — 'Special Delivery' step 3's leg: launching off the NEUTRAL
|
||||
* CAPITAL (the bar is done, the package is aboard) records the
|
||||
* launch in the story ledger and sets the run's destination back
|
||||
* to the HOME WORLD (the delivery's route — the compass marks it,
|
||||
* and the REACHED toast fires on the home world's arrival via
|
||||
* _checkDestinationReached). A launch off any other world is a
|
||||
* no-op for this leg — the quest's auto-course earns itself only
|
||||
* from the capital's departure.
|
||||
*/
|
||||
onSurfaceDeparture(worldName) {
|
||||
if (this.story?.delivered === true) {
|
||||
this.comms?.trigger('deliveredLaunch'); // the payoff chain — the once-ledger dedupes
|
||||
}
|
||||
if (!this.story || !this.isCapitalWorld(worldName)) return;
|
||||
this.story.launchedFromCapital = true;
|
||||
this.story.launchedFromCapital = true; // the leg is made
|
||||
this._questRefresh(); // the delivery leg may have just completed
|
||||
const homeId = this.galaxy?.homeSystemId;
|
||||
if (!homeId || this.systemRecord?.id === homeId) return; // already home (defensive)
|
||||
|
|
|
|||
Loading…
Reference in New Issue