Add combat comms, block jump/landing during fights, and retime pirate am
- Add OAC "enemies detected" / "enemies defeated" visual comms with random voice pools, fired on CombatSystem active-flag flips - Deny jumping or landing while a fight is live so the wave run can't be escaped or hidden on a surface - Move the 'Pirate Problems' ambush trigger to the midpoint of the entry-gate → capital-world stretch, persisting lastJumpFrom through saves for a stable midpoint
This commit is contained in:
parent
63bb6508bb
commit
d61f2d6934
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -56,7 +56,8 @@
|
||||||
"jumpDelayMs": 420,
|
"jumpDelayMs": 420,
|
||||||
"toast": "JUMP — {dest}",
|
"toast": "JUMP — {dest}",
|
||||||
"dormantToast": "JUMP GATE DORMANT — CHART {system} TO ACTIVATE ITS JUMP GATES",
|
"dormantToast": "JUMP GATE DORMANT — CHART {system} TO ACTIVATE ITS JUMP GATES",
|
||||||
"miningToast": "CANNOT JUMP WHILE MINING"
|
"miningToast": "CANNOT JUMP WHILE MINING",
|
||||||
|
"combatToast": "CANNOT JUMP WHILE UNDER ATTACK"
|
||||||
},
|
},
|
||||||
"route": {
|
"route": {
|
||||||
"_comment": "ROUTE (js/galaxy/Route.js + GameScene): the player's plotted course to a SET DESTINATION (SYSTEM tab of the MAP console). The run persists only the destination (systemId + objectId); the path from the current system is derived (planRoute — the jump network is a spanning tree, so it is the unique route). While a route is active, the compass marks the NEXT STOP in ORANGE (compassColor), replacing that object's ordinary arrow, and shows it even while still UNDISCOVERED (it is the thing to find): in an intermediate system the next stop is that system's jump gate toward the destination (typeLabel 'Route Gate'); in the DESTINATION system the next stop is the destination OBJECT itself (destTypeLabel) — the last hop is across the system to the world, and it stays orange until the ship actually reaches it (GameScene._checkDestinationReached fires reachedToast, then the route clears). The NEXT STOP in this system ALSO wears an in-world orange pulse the moment it is on screen (js/entities/RoutePulse.js — GameScene marks it each frame): the FULL pulse on the route GATE (pulse), the SAME pulse SUBDUED on the destination OBJECT in the destination system (objectPulse). A jump to any other system re-plots from the detour (replotToast). A destination with NO specific object clears the moment its system is entered. setToast fires when the destination is set ({dest}/{hops}).",
|
"_comment": "ROUTE (js/galaxy/Route.js + GameScene): the player's plotted course to a SET DESTINATION (SYSTEM tab of the MAP console). The run persists only the destination (systemId + objectId); the path from the current system is derived (planRoute — the jump network is a spanning tree, so it is the unique route). While a route is active, the compass marks the NEXT STOP in ORANGE (compassColor), replacing that object's ordinary arrow, and shows it even while still UNDISCOVERED (it is the thing to find): in an intermediate system the next stop is that system's jump gate toward the destination (typeLabel 'Route Gate'); in the DESTINATION system the next stop is the destination OBJECT itself (destTypeLabel) — the last hop is across the system to the world, and it stays orange until the ship actually reaches it (GameScene._checkDestinationReached fires reachedToast, then the route clears). The NEXT STOP in this system ALSO wears an in-world orange pulse the moment it is on screen (js/entities/RoutePulse.js — GameScene marks it each frame): the FULL pulse on the route GATE (pulse), the SAME pulse SUBDUED on the destination OBJECT in the destination system (objectPulse). A jump to any other system re-plots from the detour (replotToast). A destination with NO specific object clears the moment its system is entered. setToast fires when the destination is set ({dest}/{hops}).",
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@
|
||||||
"greyed out in comms, same as a reputation ban)."
|
"greyed out in comms, same as a reputation ban)."
|
||||||
],
|
],
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"combatDenyToast": {
|
||||||
|
"_comment": "The COMBAT gate (js/scenes/GameScene.js → startLanding): a live fight (CombatSystem.active — a wave run in flight or hostiles up) holds the ship in the air — landing would sleep the flight scene under the wave and let the player hide on the surface, so a landing request mid-combat is a flat denial on the console (the comms panel already closed — the toast is the ack; the ✗ mark reads the deny). Same shape as the capital's claim gate (data/quests.json → capitalDenyToast).",
|
||||||
|
"glyph": "✗",
|
||||||
|
"color": "#ff5c77",
|
||||||
|
"text": "CANNOT LAND WHILE UNDER ATTACK"
|
||||||
|
},
|
||||||
"videoBase": "assets/videos/",
|
"videoBase": "assets/videos/",
|
||||||
"volume": 1,
|
"volume": 1,
|
||||||
"_hud_comment": "The upper-left HUD while the surface clip loops: the planet's NAME (Ethnocentric, the SAME shade the menu's game title uses — data/menu.json → colors.title — with a black stroke behind it so it holds over any clip) and, beneath it in the body face (Centauri), the planet TYPE + TETHER LEVEL (\"Terran World, Tether Level 1\"). Both decode in with the shared scramble (the name first). marginX/marginY = corner offsets (px); name/line = font sizes, letter spacing (px), stroke color/thickness, and line color (the theme's dim by default).",
|
"_hud_comment": "The upper-left HUD while the surface clip loops: the planet's NAME (Ethnocentric, the SAME shade the menu's game title uses — data/menu.json → colors.title — with a black stroke behind it so it holds over any clip) and, beneath it in the body face (Centauri), the planet TYPE + TETHER LEVEL (\"Terran World, Tether Level 1\"). Both decode in with the shared scramble (the name first). marginX/marginY = corner offsets (px); name/line = font sizes, letter spacing (px), stroke color/thickness, and line color (the theme's dim by default).",
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "THE FOURTH QUEST — 'Pirate Problems' (the run's third link after 'Special Delivery'). It GRANTS the moment 'Special Delivery' is COMPLETED AND CLAIMED AND its payoff chain is HEARD ALL THE WAY THROUGH (unlocks.claimed + unlocks.acked — the deliveredLaunch four-beat: carol-delivered-1/2/3 + oac-delivered-1, every one of them ACKed, not merely started — the run's comms acked-ledger, registry 'commsAcked', owned by js/comms/CommsHub.js's onAck hook; GameScene._grantUnlockedQuests supplies it to QuestState.isUnlockMet, so the grant lands on whichever of the two conditions completes last: the claim (claimQuest) or the final ACK (_commEnded's grant pass)). On its grant the run turns: the player's DESTINATION is set to the Neutral Capital world (GameScene._questStartBeats) and Trestle's three-beat warning plays (data/comms.json → trigger 'trestleQ4' — trestle-q4-1/2/3 on trestle-q4-01/02/03.mp3; the 'Speak with Trestle' step's witness is that sequence in the played-ledger, the same _trestleCheck seam as 'Leaving the Nest'). Then the run's story turns on the pirates: ENTERING the capital's system with the quest live, the ambush fires — ONE rolling wave of SIX frame-0 raiders (data/combat.json → waves.scripts.pirate_ambush; EnemyTypes: raider = frame 0), the wave ships materializing OFF-SCREEN around the player, each from a random direction (CombatSystem._spawnWaveEnemy — the same off-screen materialization as the ?combat pack), streaming in on their own chase AI (GameScene._maybeStartPirateAmbush fires the run); the wave's completion marks the 'combatDone' ledger (registry, SaveData-captured) — the 'Defend your ship from pirates' step's monotonic witness. 'Return to the Neutral Capital World' completes on the ARRIVAL itself — the ship within the capital world's keep-out rim with the quest held + unclaimed sets story.capitalReturned (GameScene._maybeWitnessCapitalReturn; the story ledger is SaveData-captured, monotonic). The bar's follow-up conversation is a later layer (the arrival is the step; the scene lands on it).",
|
"_comment": "THE FOURTH QUEST — 'Pirate Problems' (the run's third link after 'Special Delivery'). It GRANTS the moment 'Special Delivery' is COMPLETED AND CLAIMED AND its payoff chain is HEARD ALL THE WAY THROUGH (unlocks.claimed + unlocks.acked — the deliveredLaunch four-beat: carol-delivered-1/2/3 + oac-delivered-1, every one of them ACKed, not merely started — the run's comms acked-ledger, registry 'commsAcked', owned by js/comms/CommsHub.js's onAck hook; GameScene._grantUnlockedQuests supplies it to QuestState.isUnlockMet, so the grant lands on whichever of the two conditions completes last: the claim (claimQuest) or the final ACK (_commEnded's grant pass)). On its grant the run turns: the player's DESTINATION is set to the Neutral Capital world (GameScene._questStartBeats) and Trestle's three-beat warning plays (data/comms.json → trigger 'trestleQ4' — trestle-q4-1/2/3 on trestle-q4-01/02/03.mp3; the 'Speak with Trestle' step's witness is that sequence in the played-ledger, the same _trestleCheck seam as 'Leaving the Nest'). Then the run's story turns on the pirates: with the quest live, the ambush fires at the MIDPOINT of the entry-jumpgate → capital-world stretch — the player ENTERS the capital's system at the gate (a full stretch from the world), and the raiders materialize once the ship is within HALF that gate-to-world distance of the capital (GameScene._maybeStartPirateAmbush: the entry gate is the destination's return gate for the last jump's source system — the restore's lastJumpFrom, staged in jumpThroughGate and saved in captureState — falling back to the gate nearest the spawn, then the spawn itself; the trigger is dist(gate, capital)/2 via JumpTravel.ambushTriggerDistance, and update() arms it the frame the ship crosses the halfway point) — ONE rolling wave of SIX frame-0 raiders (data/combat.json → waves.scripts.pirate_ambush; EnemyTypes: raider = frame 0), the wave ships materializing OFF-SCREEN around the player, each from a random direction (CombatSystem._spawnWaveEnemy — the same off-screen materialization as the ?combat pack), streaming in on their own chase AI (GameScene._maybeStartPirateAmbush fires the run); the wave's completion marks the 'combatDone' ledger (registry, SaveData-captured) — the 'Defend your ship from pirates' step's monotonic witness. 'Return to the Neutral Capital World' completes on the ARRIVAL itself — the ship within the capital world's keep-out rim with the quest held + unclaimed sets story.capitalReturned (GameScene._maybeWitnessCapitalReturn; the story ledger is SaveData-captured, monotonic). The bar's follow-up conversation is a later layer (the arrival is the step; the scene lands on it).",
|
||||||
"id": "pirate_problems",
|
"id": "pirate_problems",
|
||||||
"category": "main",
|
"category": "main",
|
||||||
"icon": "diamond",
|
"icon": "diamond",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@
|
||||||
* carol-delivered-1 → carol-delivered-2 → carol-delivered-3 →
|
* carol-delivered-1 → carol-delivered-2 → carol-delivered-3 →
|
||||||
* oac-delivered-1 (the 'Special Delivery' payoff — the launch back
|
* oac-delivered-1 (the 'Special Delivery' payoff — the launch back
|
||||||
* into space after the package's home-world sale, ack) on
|
* into space after the package's home-world sale, ack) on
|
||||||
* deliveredLaunch, with Carol registered as a sender);
|
* deliveredLaunch, oac-enemies-detected (visual, once:false,
|
||||||
|
* audioPool) on combatStart, and oac-enemies-defeated (visual,
|
||||||
|
* once:false, audioPool, the 1 s delay) on combatEnd, with Carol
|
||||||
|
* registered as a sender);
|
||||||
* - the reserved triggers are RESERVED (no comm uses them yet);
|
* - the reserved triggers are RESERVED (no comm uses them yet);
|
||||||
* - normalizeComm drops what can't be shown (bad type / no trigger /
|
* - normalizeComm drops what can't be shown (bad type / no trigger /
|
||||||
* no text for a box / no speech for an audio comm / unknown sender),
|
* no text for a box / no speech for an audio comm / unknown sender),
|
||||||
|
|
@ -90,8 +93,9 @@ const roster = listComms(cfg);
|
||||||
// The shipped roster count (the data pins it: a stray unvalidated comm
|
// The shipped roster count (the data pins it: a stray unvalidated comm
|
||||||
// landing in data/comms.json changes the number and fails this line —
|
// landing in data/comms.json changes the number and fails this line —
|
||||||
// 25 pre-'Pirate Problems' + the trestleQ4 warning trio (trestle-q4-1/
|
// 25 pre-'Pirate Problems' + the trestleQ4 warning trio (trestle-q4-1/
|
||||||
// 2/3, data/comms.json) = 28).
|
// 2/3, data/comms.json) + the combat pair (oac-enemies-detected /
|
||||||
check('the roster holds exactly the shipped comms', roster.length === 28);
|
// oac-enemies-defeated) = 30).
|
||||||
|
check('the roster holds exactly the shipped comms', roster.length === 30);
|
||||||
const byId = Object.fromEntries(roster.map((c) => [c.id, c]));
|
const byId = Object.fromEntries(roster.map((c) => [c.id, c]));
|
||||||
const intro = byId['oac-intro'];
|
const intro = byId['oac-intro'];
|
||||||
check(
|
check(
|
||||||
|
|
@ -300,6 +304,75 @@ check('oac-quest-new: speaks oac/quest-new.mp3', byId['oac-quest-new']?.audio ==
|
||||||
check('oac-quest-new: the clip ships on disk', existsSync(join(root, byId['oac-quest-new']?.audio ?? '')));
|
check('oac-quest-new: the clip ships on disk', existsSync(join(root, byId['oac-quest-new']?.audio ?? '')));
|
||||||
check('oac-quest-new: beats the grant SFX (delayMs > 0)', (byId['oac-quest-new']?.delayMs ?? 0) > 0);
|
check('oac-quest-new: beats the grant SFX (delayMs > 0)', (byId['oac-quest-new']?.delayMs ?? 0) > 0);
|
||||||
|
|
||||||
|
// The combat pair — OAC's contact call the moment the fight goes live
|
||||||
|
// and the sign-off the moment the last hostile is down (GameScene
|
||||||
|
// ._setCombatBar — the CombatSystem active-flag flips; both once:false,
|
||||||
|
// both voiced by an audioPool — the same contract as oac-system-complete).
|
||||||
|
check(
|
||||||
|
'oac-enemies-detected: the combat-start call (visual, combatStart, oac, once:false)',
|
||||||
|
!!byId['oac-enemies-detected'] &&
|
||||||
|
byId['oac-enemies-detected'].from === 'oac' &&
|
||||||
|
byId['oac-enemies-detected'].type === 'visual' &&
|
||||||
|
byId['oac-enemies-detected'].trigger === 'combatStart' &&
|
||||||
|
byId['oac-enemies-detected'].once === false,
|
||||||
|
);
|
||||||
|
const detPool = byId['oac-enemies-detected']?.audioPool ?? [];
|
||||||
|
check(
|
||||||
|
'oac-enemies-detected: the three detected clips (audioPool, no fixed audio)',
|
||||||
|
byId['oac-enemies-detected']?.audio === null &&
|
||||||
|
JSON.stringify(detPool) ===
|
||||||
|
JSON.stringify([
|
||||||
|
'assets/speech/oac/enemies-detected-01.mp3',
|
||||||
|
'assets/speech/oac/enemies-detected-02.mp3',
|
||||||
|
'assets/speech/oac/enemies-detected-03.mp3',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
check('oac-enemies-detected: every pool clip ships on disk', detPool.every((f) => existsSync(join(root, f))));
|
||||||
|
check('oac-enemies-detected: fires AT the fight (delayMs === 0)', (byId['oac-enemies-detected']?.delayMs ?? -1) === 0);
|
||||||
|
check(
|
||||||
|
'oac-enemies-detected: a played call RE-arms (once:false — every combat start)',
|
||||||
|
commsForTrigger(cfg, 'combatStart', new Set(['oac-enemies-detected'])).length === 1,
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'oac-enemies-defeated: the combat-end sign-off (visual, combatEnd, oac, once:false)',
|
||||||
|
!!byId['oac-enemies-defeated'] &&
|
||||||
|
byId['oac-enemies-defeated'].from === 'oac' &&
|
||||||
|
byId['oac-enemies-defeated'].type === 'visual' &&
|
||||||
|
byId['oac-enemies-defeated'].trigger === 'combatEnd' &&
|
||||||
|
byId['oac-enemies-defeated'].once === false,
|
||||||
|
);
|
||||||
|
const defPool = byId['oac-enemies-defeated']?.audioPool ?? [];
|
||||||
|
check(
|
||||||
|
'oac-enemies-defeated: the three defeated clips (audioPool, no fixed audio)',
|
||||||
|
byId['oac-enemies-defeated']?.audio === null &&
|
||||||
|
JSON.stringify(defPool) ===
|
||||||
|
JSON.stringify([
|
||||||
|
'assets/speech/oac/enemies-defeated-01.mp3',
|
||||||
|
'assets/speech/oac/enemies-defeated-02.mp3',
|
||||||
|
'assets/speech/oac/enemies-defeated-03.mp3',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
check('oac-enemies-defeated: every pool clip ships on disk', defPool.every((f) => existsSync(join(root, f))));
|
||||||
|
check('oac-enemies-defeated: waits 1 s for the last kill to land (delayMs === 1000)', (byId['oac-enemies-defeated']?.delayMs ?? -1) === 1000);
|
||||||
|
check(
|
||||||
|
'oac-enemies-defeated: a played sign-off RE-arms (once:false — every combat end)',
|
||||||
|
commsForTrigger(cfg, 'combatEnd', new Set(['oac-enemies-defeated'])).length === 1,
|
||||||
|
);
|
||||||
|
const detAssets = commAssets(cfg, byId['oac-enemies-detected']);
|
||||||
|
check(
|
||||||
|
'oac-enemies-detected assets: the pool rolls to a shipped clip, keyed',
|
||||||
|
detAssets.video?.key === 'comms_vid_oac' &&
|
||||||
|
detPool.includes(detAssets.audio?.url) &&
|
||||||
|
detAssets.audio?.key === audioKeyFor(detAssets.audio.url),
|
||||||
|
);
|
||||||
|
const defAssets = commAssets(cfg, byId['oac-enemies-defeated']);
|
||||||
|
check(
|
||||||
|
'oac-enemies-defeated assets: the pool rolls to a shipped clip, keyed',
|
||||||
|
defAssets.video?.key === 'comms_vid_oac' &&
|
||||||
|
defPool.includes(defAssets.audio?.url) &&
|
||||||
|
defAssets.audio?.key === audioKeyFor(defAssets.audio.url),
|
||||||
|
);
|
||||||
|
|
||||||
// The 'Leaving the Nest' beats — Trestle's THREE-BRIEF sequence (the
|
// The 'Leaving the Nest' beats — Trestle's THREE-BRIEF sequence (the
|
||||||
// home gates go online; ALL THREE in the run's comms played-ledger are
|
// home gates go online; ALL THREE in the run's comms played-ledger are
|
||||||
// the quest's 'Talk to Trestle' witness — GameScene._trestleCheck) and
|
// the quest's 'Talk to Trestle' witness — GameScene._trestleCheck) and
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ const check = (label, cond) => {
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
const gates = read('gates.json');
|
const gates = read('gates.json');
|
||||||
const tether = read('tether.json');
|
const tether = read('tether.json');
|
||||||
|
const landing = read('landing.json');
|
||||||
const jump = gates.jump;
|
const jump = gates.jump;
|
||||||
|
|
||||||
check('jump: section present', !!jump && typeof jump === 'object');
|
check('jump: section present', !!jump && typeof jump === 'object');
|
||||||
|
|
@ -55,13 +56,24 @@ check('jump: the toast templates keep their placeholders',
|
||||||
typeof jump?.toast === 'string' && jump.toast.includes('{dest}') &&
|
typeof jump?.toast === 'string' && jump.toast.includes('{dest}') &&
|
||||||
typeof jump?.dormantToast === 'string' && jump.dormantToast.includes('{system}') &&
|
typeof jump?.dormantToast === 'string' && jump.dormantToast.includes('{system}') &&
|
||||||
typeof jump?.miningToast === 'string' && jump.miningToast.length > 0);
|
typeof jump?.miningToast === 'string' && jump.miningToast.length > 0);
|
||||||
|
check('jump: combatToast is the live-fight denial (non-empty string)',
|
||||||
|
typeof jump?.combatToast === 'string' && jump.combatToast.length > 0);
|
||||||
check('jump: the gap keeps the ship outside the keepout with room to spare',
|
check('jump: the gap keeps the ship outside the keepout with room to spare',
|
||||||
Number(jump?.arrivalGap ?? -1) + Number(gates.size ?? 0) + Number(gates.shipClearance ?? 0) > Number(gates.size ?? 0) + Number(gates.shipClearance ?? 0));
|
Number(jump?.arrivalGap ?? -1) + Number(gates.size ?? 0) + Number(gates.shipClearance ?? 0) > Number(gates.size ?? 0) + Number(gates.shipClearance ?? 0));
|
||||||
|
|
||||||
|
// The COMBAT gates (GameScene.jumpThroughGate / startLanding — a live
|
||||||
|
// fight, CombatSystem.active, denies the jump AND the landing: the run
|
||||||
|
// can't leave the system it's fighting in, and can't hide on a surface
|
||||||
|
// while the wave runs): the toasts' shapes.
|
||||||
|
check('landing: combatDenyToast configured (glyph + text + color)',
|
||||||
|
!!landing.combatDenyToast && typeof landing.combatDenyToast.glyph === 'string' &&
|
||||||
|
typeof landing.combatDenyToast.text === 'string' && landing.combatDenyToast.text.length > 0 &&
|
||||||
|
/^#[0-9a-fA-F]{6}$/.test(landing.combatDenyToast.color ?? ''));
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// The pure geometry (js/galaxy/JumpTravel.js)
|
// The pure geometry (js/galaxy/JumpTravel.js)
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
const { returnGateFor, arrivalPoint, jumpArrival } = await import('../js/galaxy/JumpTravel.js');
|
const { returnGateFor, arrivalPoint, jumpArrival, ambushTriggerDistance } = await import('../js/galaxy/JumpTravel.js');
|
||||||
|
|
||||||
const contentA = {
|
const contentA = {
|
||||||
jumps: [
|
jumps: [
|
||||||
|
|
@ -100,6 +112,23 @@ check('jumpArrival: { x, y, heading, gateId } for a tree-edge jump',
|
||||||
check('jumpArrival: null for a one-way shortcut (the scene falls back to the origin)',
|
check('jumpArrival: null for a one-way shortcut (the scene falls back to the origin)',
|
||||||
jumpArrival(contentA, 'D', cfg) === null);
|
jumpArrival(contentA, 'D', cfg) === null);
|
||||||
|
|
||||||
|
// The 'Pirate Problems' ambush's trigger — HALF the gate→capital stretch
|
||||||
|
// (GameScene._maybeStartPirateAmbush fires the wave once the ship is
|
||||||
|
// within that distance of the capital world: the midpoint of the
|
||||||
|
// jumpgate→world line).
|
||||||
|
const gate0 = { x: 1000, y: 2000 };
|
||||||
|
const cap0 = { x: 4000, y: 6000 }; // 3000, 4000 apart → 5000
|
||||||
|
check('ambushTriggerDistance: half the gate→world stretch (the ambush midpoint)',
|
||||||
|
ambushTriggerDistance(gate0, cap0) === 2500);
|
||||||
|
check('ambushTriggerDistance: symmetric (gate/world order is immaterial)',
|
||||||
|
ambushTriggerDistance(cap0, gate0) === 2500);
|
||||||
|
check('ambushTriggerDistance: null on a missing point (no gate / no capital)',
|
||||||
|
ambushTriggerDistance(null, cap0) === null &&
|
||||||
|
ambushTriggerDistance(gate0, null) === null &&
|
||||||
|
ambushTriggerDistance({ x: 1 }, { y: 1 }) === null);
|
||||||
|
check('ambushTriggerDistance: null on a zero stretch (gate on the world)',
|
||||||
|
ambushTriggerDistance({ x: 5, y: 5 }, { x: 5, y: 5 }) === null);
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// A REAL galaxy (bootstrap config the way main.js does, from the data dir)
|
// A REAL galaxy (bootstrap config the way main.js does, from the data dir)
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -82,3 +82,26 @@ export function jumpArrival(content, fromId, cfg = {}) {
|
||||||
if (!gate) return null;
|
if (!gate) return null;
|
||||||
return { ...arrivalPoint(gate, cfg), gateId: gate.id };
|
return { ...arrivalPoint(gate, cfg), gateId: gate.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 'Pirate Problems' ambush's TRIGGER DISTANCE — half the
|
||||||
|
* gate→capital-world stretch (GameScene._maybeStartPirateAmbush): the
|
||||||
|
* raiders materialise once the player is that far from the capital
|
||||||
|
* world, i.e. at the MIDPOINT of the entry-jumpgate → world line, so
|
||||||
|
* the conflict starts halfway to the capital from the jumpgate.
|
||||||
|
*
|
||||||
|
* @param {{x:number,y:number}|null} gate the entry gate (a content
|
||||||
|
* record, or any {x, y} anchor — the scene's spawn fallback)
|
||||||
|
* @param {{x:number,y:number}|null} world the capital world
|
||||||
|
* @returns {number|null} dist(gate, world) / 2, or null when either
|
||||||
|
* point is missing or the stretch is degenerate (zero)
|
||||||
|
*/
|
||||||
|
export function ambushTriggerDistance(gate, world) {
|
||||||
|
const gx = Number(gate?.x);
|
||||||
|
const gy = Number(gate?.y);
|
||||||
|
const wx = Number(world?.x);
|
||||||
|
const wy = Number(world?.y);
|
||||||
|
if (![gx, gy, wx, wy].every(Number.isFinite)) return null;
|
||||||
|
const d = Math.hypot(gx - wx, gy - wy);
|
||||||
|
return d > 0 ? d / 2 : null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,17 @@ export function captureState(scene, now) {
|
||||||
galaxyName: galaxy.name,
|
galaxyName: galaxy.name,
|
||||||
currentSystemId: galaxy.currentSystemId,
|
currentSystemId: galaxy.currentSystemId,
|
||||||
systemName: scene.systemRecord?.name ?? null,
|
systemName: scene.systemRecord?.name ?? null,
|
||||||
|
// The last JUMP's source system (GameScene.lastJumpFromSystem —
|
||||||
|
// jumpThroughGate stages it into the restore record; the scene
|
||||||
|
// re-reads it from its own restore). The 'Pirate Problems'
|
||||||
|
// ambush's midpoint is measured from the ENTRY GATE
|
||||||
|
// (returnGateFor(content, lastJumpFrom) —
|
||||||
|
// GameScene._pirateAmbushEntryGate), so the field rides saves and a
|
||||||
|
// reload keeps the same midpoint. A save predating it has no field;
|
||||||
|
// the restore treats the absence as a fresh run (the ambush's
|
||||||
|
// nearest-gate fallback).
|
||||||
|
lastJumpFrom:
|
||||||
|
typeof scene.lastJumpFromSystem === 'string' ? scene.lastJumpFromSystem : null,
|
||||||
ship: {
|
ship: {
|
||||||
x: Number(scene.ship.x),
|
x: Number(scene.ship.x),
|
||||||
y: Number(scene.ship.y),
|
y: Number(scene.ship.y),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { Planet } from '../entities/Planet.js';
|
||||||
import { AsteroidCluster } from '../entities/AsteroidCluster.js';
|
import { AsteroidCluster } from '../entities/AsteroidCluster.js';
|
||||||
import { Station } from '../entities/Station.js';
|
import { Station } from '../entities/Station.js';
|
||||||
import { JumpGate } from '../entities/JumpGate.js';
|
import { JumpGate } from '../entities/JumpGate.js';
|
||||||
import { jumpArrival } from '../galaxy/JumpTravel.js';
|
import { jumpArrival, returnGateFor, ambushTriggerDistance } from '../galaxy/JumpTravel.js';
|
||||||
import { Starfield } from '../visuals/Starfield.js';
|
import { Starfield } from '../visuals/Starfield.js';
|
||||||
import { NebulaAtmosphere } from '../visuals/NebulaAtmosphere.js';
|
import { NebulaAtmosphere } from '../visuals/NebulaAtmosphere.js';
|
||||||
import { NebulaLightning } from '../visuals/NebulaLightning.js';
|
import { NebulaLightning } from '../visuals/NebulaLightning.js';
|
||||||
|
|
@ -448,6 +448,17 @@ export class GameScene extends Phaser.Scene {
|
||||||
// playtime) in the registry (js/save/SaveData.js) — the ship and the
|
// playtime) in the registry (js/save/SaveData.js) — the ship and the
|
||||||
// tether field don't exist yet, so apply it once they do (below).
|
// tether field don't exist yet, so apply it once they do (below).
|
||||||
this._pendingRestore = consumeRestore(this.registry);
|
this._pendingRestore = consumeRestore(this.registry);
|
||||||
|
// The last JUMP's source system (jumpThroughGate stages it into the
|
||||||
|
// restore record; captureState carries it through a panel save too):
|
||||||
|
// the 'Pirate Problems' ambush measures its midpoint from the gate
|
||||||
|
// the player came in through — returnGateFor(content, lastJumpFrom)
|
||||||
|
// (GameScene._pirateAmbushEntryGate). Null on a fresh run or a save
|
||||||
|
// predating the field — the ambush's nearest-gate fallback stands
|
||||||
|
// in for it.
|
||||||
|
this.lastJumpFromSystem =
|
||||||
|
this._pendingRestore && typeof this._pendingRestore.lastJumpFrom === 'string'
|
||||||
|
? this._pendingRestore.lastJumpFrom
|
||||||
|
: null;
|
||||||
// A FRESH run (no staged restore) is what the 'newGame' comms fire
|
// A FRESH run (no staged restore) is what the 'newGame' comms fire
|
||||||
// for (the welcome plays on a new game, never on a load) — and a
|
// for (the welcome plays on a new game, never on a load) — and a
|
||||||
// jump-cut restart (scene.restart, no resetRunState) is NOT one: its
|
// jump-cut restart (scene.restart, no resetRunState) is NOT one: its
|
||||||
|
|
@ -908,8 +919,11 @@ export class GameScene extends Phaser.Scene {
|
||||||
|
|
||||||
// ---- COMBAT — the hostile pack (the ?combat boot) + the combat bar --
|
// ---- COMBAT — the hostile pack (the ?combat boot) + the combat bar --
|
||||||
// COMBAT IS LIVE IN THE MAIN GAME: the 'Pirate Problems' quest
|
// COMBAT IS LIVE IN THE MAIN GAME: the 'Pirate Problems' quest
|
||||||
// (data/quests.json → pirate_problems) ambushes the player on the
|
// (data/quests.json → pirate_problems) ambushes the player at the
|
||||||
// ENTER of the Neutral Capital's system — GameScene.
|
// MIDPOINT of the entry-jumpgate → Neutral-Capital-world stretch —
|
||||||
|
// the enter lands the ship at the gate (a full stretch out); the
|
||||||
|
// wave starts when it reaches half that distance from the world —
|
||||||
|
// GameScene.
|
||||||
// _maybeStartPirateAmbush fires the pirate_ambush wave run (data/
|
// _maybeStartPirateAmbush fires the pirate_ambush wave run (data/
|
||||||
// combat.json → waves.scripts.pirate_ambush — one rolling wave of six
|
// combat.json → waves.scripts.pirate_ambush — one rolling wave of six
|
||||||
// frame-0 raiders, materializing off-screen around the player from a
|
// frame-0 raiders, materializing off-screen around the player from a
|
||||||
|
|
@ -1782,15 +1796,19 @@ export class GameScene extends Phaser.Scene {
|
||||||
// and the quest guard (_trestleQ4Beats) keeps the beats off the
|
// and the quest guard (_trestleQ4Beats) keeps the beats off the
|
||||||
// player before the grant.
|
// player before the grant.
|
||||||
this._trestleQ4Beats();
|
this._trestleQ4Beats();
|
||||||
// The 'Pirate Problems' AMBUSH — this is the ENTER of the Neutral
|
// The 'Pirate Problems' AMBUSH — the ENTER of the Neutral Capital's
|
||||||
// Capital's system (the scene was just built for it): with the quest
|
// system (the scene was just built for it): the ship is at the entry
|
||||||
// live and its combat step open, the pirate wave starts — its ships
|
// gate (a full gate→capital stretch from the world), so the wave
|
||||||
// materialize OFF-SCREEN around the player, each from a random
|
// starts here only when the spawn already sits within the MIDPOINT
|
||||||
// direction (CombatSystem._spawnWaveEnemy), and converge on their
|
// (a degenerate short stretch) — otherwise update() arms it the
|
||||||
// own chase AI. The other arm is the grant itself (a save whose
|
// frame the flight crosses the halfway point (the same method's
|
||||||
// grant landed while the player was already IN the system — the
|
// guards are no-op-gated either way). The third arm is the grant
|
||||||
// same method is a no-op there only when the quest isn't live or
|
// itself (_questStartBeats — a save whose grant landed while the
|
||||||
// the wave already ran).
|
// player was already IN the system, past the midpoint or not).
|
||||||
|
// When it fires, its ships materialize OFF-SCREEN around the player,
|
||||||
|
// each from a random direction (CombatSystem._spawnWaveEnemy), and
|
||||||
|
// converge on their own chase AI.
|
||||||
|
this._ambushSpawn = this.ship ? { x: this.ship.x, y: this.ship.y } : null;
|
||||||
this._maybeStartPirateAmbush();
|
this._maybeStartPirateAmbush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1845,11 +1863,20 @@ export class GameScene extends Phaser.Scene {
|
||||||
* The action-deck swap (combat mode, Diablo 4 style — one bar in the
|
* The action-deck swap (combat mode, Diablo 4 style — one bar in the
|
||||||
* same place, depending on the mode): combat LIVE → the combat bar up
|
* same place, depending on the mode): combat LIVE → the combat bar up
|
||||||
* and the deck down; combat over → back to the deck.
|
* and the deck down; combat over → back to the deck.
|
||||||
|
* The COMBAT COMMS ride this same flip (data/comms.json →
|
||||||
|
* oac-enemies-detected / oac-enemies-defeated, both visual, both
|
||||||
|
* once:false): live → OAC's contact call (the wave run's lead — before
|
||||||
|
* the first hostile lands — or the ?combat pack's spawn); over (the
|
||||||
|
* last hostile down) → OAC's sign-off (its delayMs, 1 s, lands it
|
||||||
|
* after the final explosion's read). The hub's in-flight guard + the
|
||||||
|
* played ledger keep each flip to one voice.
|
||||||
* @param {boolean} active
|
* @param {boolean} active
|
||||||
*/
|
*/
|
||||||
_setCombatBar(active) {
|
_setCombatBar(active) {
|
||||||
if (this.actionBar) this.actionBar.setVisible(!active);
|
if (this.actionBar) this.actionBar.setVisible(!active);
|
||||||
if (this.combatBar) this.combatBar.setVisible(active);
|
if (this.combatBar) this.combatBar.setVisible(active);
|
||||||
|
if (active) this.comms?.trigger('combatStart');
|
||||||
|
else this.comms?.trigger('combatEnd');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2368,6 +2395,13 @@ export class GameScene extends Phaser.Scene {
|
||||||
this.tetherField.draw(_time); // the barrier (on-screen dots only)
|
this.tetherField.draw(_time); // the barrier (on-screen dots only)
|
||||||
this.actionBar?.update(_time, delta); // the deck's living details
|
this.actionBar?.update(_time, delta); // the deck's living details
|
||||||
this._checkDestinationReached(); // last leg done? clear the destination (REACHED)
|
this._checkDestinationReached(); // last leg done? clear the destination (REACHED)
|
||||||
|
// 'Pirate Problems' step 2 — the AMBUSH's arm: with the quest live
|
||||||
|
// and its combat step open, the pirate wave starts the moment the
|
||||||
|
// ship is within HALF the entry-gate → capital-world stretch — the
|
||||||
|
// conflict begins halfway to the world from the jumpgate (the
|
||||||
|
// method is no-op-gated, so the per-frame cost is a few ledger
|
||||||
|
// reads + one distance check).
|
||||||
|
this._maybeStartPirateAmbush();
|
||||||
// 'Pirate Problems' step 3 — the RETURN's witness: the ship at the
|
// 'Pirate Problems' step 3 — the RETURN's witness: the ship at the
|
||||||
// capital world's rim with the quest held + unclaimed sets
|
// capital world's rim with the quest held + unclaimed sets
|
||||||
// story.capitalReturned (monotonic; _maybeWitnessCapitalReturn is a
|
// story.capitalReturned (monotonic; _maybeWitnessCapitalReturn is a
|
||||||
|
|
@ -2856,6 +2890,20 @@ export class GameScene extends Phaser.Scene {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// COMBAT — a live fight (CombatSystem.active — a wave run in flight,
|
||||||
|
// the 'Pirate Problems' ambush included, or hostiles up) holds the
|
||||||
|
// ship in the system: a jump mid-wave would carry the run out of its
|
||||||
|
// own fight (the wave run is scene-local — the enemies and the
|
||||||
|
// fight's momentum would die with the scene, the run would simply
|
||||||
|
// vanish with the threat). Same flat denial shape as the mining
|
||||||
|
// guard above.
|
||||||
|
if (this.combat?.active) {
|
||||||
|
this.consoleToast(
|
||||||
|
String(config.get('gates.jump.combatToast', 'CANNOT JUMP WHILE UNDER ATTACK')),
|
||||||
|
{ glyph: '⌁', glyphColor: toCss(themeColor('neon2', 0xff9b9b)) },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Where we arrive: just past the return gate's keepout (JumpTravel).
|
// Where we arrive: just past the return gate's keepout (JumpTravel).
|
||||||
// Fallback — no return gate (a one-way shortcut, shortcuts are OFF in
|
// Fallback — no return gate (a one-way shortcut, shortcuts are OFF in
|
||||||
// the current config): the destination's origin (its center is empty —
|
// the current config): the destination's origin (its center is empty —
|
||||||
|
|
@ -2881,6 +2929,14 @@ export class GameScene extends Phaser.Scene {
|
||||||
|
|
||||||
const rec = captureState(this, this.time.now);
|
const rec = captureState(this, this.time.now);
|
||||||
rec.currentSystemId = destId;
|
rec.currentSystemId = destId;
|
||||||
|
// The AMBUSH's bookkeeping — which gate the player came in through:
|
||||||
|
// the new scene resolves its entry gate as
|
||||||
|
// returnGateFor(content, lastJumpFrom) and measures the 'Pirate
|
||||||
|
// Problems' midpoint from it (GameScene._pirateAmbushEntryGate).
|
||||||
|
// A panel save carries the field through (captureState), so a
|
||||||
|
// reload keeps the same midpoint; a save predating it simply lacks
|
||||||
|
// the field (the nearest-gate fallback).
|
||||||
|
rec.lastJumpFrom = from.id;
|
||||||
rec.systemName = dest.name;
|
rec.systemName = dest.name;
|
||||||
rec.ship = {
|
rec.ship = {
|
||||||
x: arrival.x,
|
x: arrival.x,
|
||||||
|
|
@ -3164,6 +3220,10 @@ export class GameScene extends Phaser.Scene {
|
||||||
* 'land' — an unsettled world → land (same gate for the
|
* 'land' — an unsettled world → land (same gate for the
|
||||||
* capital world, were it ever unsettled)
|
* capital world, were it ever unsettled)
|
||||||
* 'cancel' — just close (the console log is the ack)
|
* 'cancel' — just close (the console log is the ack)
|
||||||
|
* Both transports keep their OWN combat guard on top of the panel's
|
||||||
|
* (GameScene.jumpThroughGate / startLanding — a live fight,
|
||||||
|
* CombatSystem.active, denies the jump and the landing: the run can't
|
||||||
|
* leave the system it's fighting in).
|
||||||
*/
|
*/
|
||||||
commsAction(id, target) {
|
commsAction(id, target) {
|
||||||
this.commsPanel.close(); // the buttons close the panel (like miningAction)
|
this.commsPanel.close(); // the buttons close the panel (like miningAction)
|
||||||
|
|
@ -3171,7 +3231,7 @@ export class GameScene extends Phaser.Scene {
|
||||||
if (id === 'request-jump') {
|
if (id === 'request-jump') {
|
||||||
// The panel only offers this when the gate is active and
|
// The panel only offers this when the gate is active and
|
||||||
// gates.jump.enabled — jumpThroughGate keeps its own guards
|
// gates.jump.enabled — jumpThroughGate keeps its own guards
|
||||||
// (mining in progress, a missing destination).
|
// (mining in progress, a live combat, a missing destination).
|
||||||
const gt = target.gate?.entity;
|
const gt = target.gate?.entity;
|
||||||
if (gt) this.jumpThroughGate(gt);
|
if (gt) this.jumpThroughGate(gt);
|
||||||
return;
|
return;
|
||||||
|
|
@ -3219,6 +3279,26 @@ export class GameScene extends Phaser.Scene {
|
||||||
console.info(`[orbit] comms: landing disabled for ${target.name}`);
|
console.info(`[orbit] comms: landing disabled for ${target.name}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// COMBAT — a live fight (CombatSystem.active — a wave run in flight,
|
||||||
|
// the 'Pirate Problems' ambush included, or hostiles up) holds the
|
||||||
|
// ship in the air: landing would SLEEP the flight scene under the
|
||||||
|
// wave — the player would hide on the surface while the raiders
|
||||||
|
// wait, and the fight's momentum (the wave run is scene-local) would
|
||||||
|
// die with the sleep. The fight ends where it started, in the air.
|
||||||
|
// Same flat denial shape as the capital's claim gate below.
|
||||||
|
if (this.combat?.active) {
|
||||||
|
const t = config.get('landing.combatDenyToast', {});
|
||||||
|
this.playSfx('ui_close');
|
||||||
|
this.consoleToast(
|
||||||
|
String(t.text ?? 'CANNOT LAND WHILE UNDER ATTACK'),
|
||||||
|
{
|
||||||
|
glyph: t.glyph ?? '✗',
|
||||||
|
glyphColor: t.color ? toCss(t.color) : toCss(themeColor('red', 0xff5c77)),
|
||||||
|
durationMs: 3200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// The CAPITAL WORLD holds its door until the 'Leaving the Nest'
|
// The CAPITAL WORLD holds its door until the 'Leaving the Nest'
|
||||||
// reward is CLAIMED (the quest's 200 credits — the quest itself
|
// reward is CLAIMED (the quest's 200 credits — the quest itself
|
||||||
// completes when the ship arrives at this planet; the claim is the
|
// completes when the ship arrives at this planet; the claim is the
|
||||||
|
|
@ -4490,10 +4570,12 @@ export class GameScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
this._trestleQ4Beats();
|
this._trestleQ4Beats();
|
||||||
// The AMBUSH's other arm: the grant may have landed while the player
|
// The AMBUSH's other arm: the grant may have landed while the player
|
||||||
// was already IN the capital's system (a late claim out there) — the
|
// was already IN the capital's system (a late claim out there) — if
|
||||||
// create() arm only covers the enter. _maybeStartPirateAmbush is
|
// the ship is already within the midpoint the wave starts here; if
|
||||||
// no-op-gated (quest live + step open + system match + not already
|
// it's still gate-side, update()'s arm arms it once the flight
|
||||||
// running/done), so re-calling it here is safe.
|
// crosses the halfway point. _maybeStartPirateAmbush is no-op-gated
|
||||||
|
// (quest live + step open + system match + not already running/done
|
||||||
|
// + within the midpoint trigger), so re-calling it here is safe.
|
||||||
this._maybeStartPirateAmbush();
|
this._maybeStartPirateAmbush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4665,23 +4747,30 @@ export class GameScene extends Phaser.Scene {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'Pirate Problems' AMBUSH — the quest's 'Defend your ship from
|
* The 'Pirate Problems' AMBUSH — the quest's 'Defend your ship from
|
||||||
* pirates' step: when the player ENTERS the Neutral Capital's system
|
* pirates' step: with the quest live (held + unclaimed) and its
|
||||||
* (the gate-to-capital stretch — 'all bets are off' out here) with the
|
* combat step still open, the pirate wave starts the moment the ship
|
||||||
* quest live (held + unclaimed) and its combat step still open, the
|
* reaches the MIDPOINT of the entry-gate → capital-world stretch —
|
||||||
* pirate wave starts — ONE rolling wave of six frame-0 raiders
|
* HALF the jumpgate-to-world distance from the capital (the quest's
|
||||||
* (data/combat.json → waves.scripts.pirate_ambush), three at a time,
|
* destination is the capital, so the run's flight crosses it), NOT
|
||||||
* each kill feeding a replacement until all six are down. The wave
|
* on the system's enter (the gate-side spawn is a full stretch out).
|
||||||
* ships materialize OFF-SCREEN around the player, each from a random
|
* ONE rolling wave of six frame-0 raiders (data/combat.json →
|
||||||
* direction (CombatSystem._spawnWaveEnemy), and stream in on their
|
* waves.scripts.pirate_ambush), three at a time, each kill feeding a
|
||||||
* own chase AI. Two arms call this:
|
* replacement until all six are down. The wave ships materialize
|
||||||
* · create()'s tail — the ENTER of the system (the normal case);
|
* OFF-SCREEN around the player, each from a random direction
|
||||||
|
* (CombatSystem._spawnWaveEnemy), and stream in on their own chase
|
||||||
|
* AI. Three arms call it (all no-op-gated by the guards below):
|
||||||
|
* · update() — the per-frame arm: the flight crosses the midpoint;
|
||||||
|
* · create()'s tail — the ENTER, when the spawn already sits
|
||||||
|
* within the midpoint (a degenerate short stretch);
|
||||||
* · _questStartBeats — a grant that lands while the player is
|
* · _questStartBeats — a grant that lands while the player is
|
||||||
* already in the system (a late claim out there).
|
* already in the system (a late claim out there).
|
||||||
* Guards (all must hold, in order): combat enabled + built; the
|
* Guards (all must hold, in order): combat enabled + built; the
|
||||||
* quest held + unclaimed; the combat step not yet cleared (the
|
* quest held + unclaimed; the combat step not yet cleared (the
|
||||||
* monotonic done-ledger — a cleared ambush never re-fires); no wave
|
* monotonic done-ledger — a cleared ambush never re-fires); no wave
|
||||||
* run in flight (a live fight is the fight); the capital target
|
* run in flight (a live fight is the fight); the capital target
|
||||||
* resolves in THIS system (the ambush lives where Trestle is).
|
* resolves in THIS system (the ambush lives where Trestle is); the
|
||||||
|
* midpoint resolves (entry gate + capital world both found) and the
|
||||||
|
* ship is within it of the capital world.
|
||||||
* A mid-fight quit re-arms the ambush on load (the in-flight run is
|
* A mid-fight quit re-arms the ambush on load (the in-flight run is
|
||||||
* scene-local) — the step's witness is the completion, not the
|
* scene-local) — the step's witness is the completion, not the
|
||||||
* attempt, so the re-fire is the fair read.
|
* attempt, so the re-fire is the fair read.
|
||||||
|
|
@ -4698,12 +4787,55 @@ export class GameScene extends Phaser.Scene {
|
||||||
if (this._combatDone().has(script)) return; // cleared once — the step stands
|
if (this._combatDone().has(script)) return; // cleared once — the step stands
|
||||||
const run = this.combat.waveRun;
|
const run = this.combat.waveRun;
|
||||||
if (run && run.state !== 'done' && run.state !== 'cancelled') return; // in flight
|
if (run && run.state !== 'done' && run.state !== 'cancelled') return; // in flight
|
||||||
if (this.galaxy?.neutralCapitalTarget?.()?.systemId !== this.systemRecord?.id) return;
|
const t = this.galaxy?.neutralCapitalTarget?.();
|
||||||
|
if (!t?.objectId || t.systemId !== this.systemRecord?.id) return; // not this system
|
||||||
|
// The MIDPOINT TRIGGER — half the entry-gate → capital stretch:
|
||||||
|
// the raiders materialise at the halfway point, not at the gate.
|
||||||
|
const cap = (this.systemPlanets ?? []).find((p) => p.discoveryId === t.objectId);
|
||||||
|
const trigger = ambushTriggerDistance(this._pirateAmbushEntryGate(), cap);
|
||||||
|
if (trigger == null || !this.ship) return; // no stretch to halve
|
||||||
|
if (Math.hypot(this.ship.x - cap.x, this.ship.y - cap.y) > trigger) return; // not halfway yet
|
||||||
this.combat.startWaves(script, {
|
this.combat.startWaves(script, {
|
||||||
onComplete: () => this._recordCombatDone(script),
|
onComplete: () => this._recordCombatDone(script),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AMBUSH's entry gate — the gate the player CAME IN through (the
|
||||||
|
* midpoint is measured from it): the destination's return gate for
|
||||||
|
* the last jump's source system first (jumpThroughGate stages
|
||||||
|
* lastJumpFrom into the restore record — exact, and stable across a
|
||||||
|
* save parked mid-stretch), then the system's gate NEAREST the spawn
|
||||||
|
* point (a jump materialises the ship just past its return gate's
|
||||||
|
* keepout, so the nearest gate is the one they used — the fallback
|
||||||
|
* for a fresh run or a save that predates lastJumpFrom), and
|
||||||
|
* finally the spawn point itself (a gate-less system — the spawn IS
|
||||||
|
* where the ship came from).
|
||||||
|
* @returns {{x:number,y:number}|null} the gate record, or null
|
||||||
|
*/
|
||||||
|
_pirateAmbushEntryGate() {
|
||||||
|
if (typeof this.lastJumpFromSystem === 'string') {
|
||||||
|
const g = returnGateFor(this.systemContent, this.lastJumpFromSystem);
|
||||||
|
if (g && typeof g.x === 'number' && typeof g.y === 'number') return g;
|
||||||
|
}
|
||||||
|
const anchor = this._ambushSpawn ?? this.ship;
|
||||||
|
if (anchor && Array.isArray(this.systemGates)) {
|
||||||
|
let best = null;
|
||||||
|
let bestD = Infinity;
|
||||||
|
for (const gt of this.systemGates) {
|
||||||
|
const d = Math.hypot(gt.x - anchor.x, gt.y - anchor.y);
|
||||||
|
if (d < bestD) {
|
||||||
|
bestD = d;
|
||||||
|
best = gt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best) return best;
|
||||||
|
}
|
||||||
|
return anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number'
|
||||||
|
? { x: anchor.x, y: anchor.y }
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'Pirate Problems' RETURN witness — the step's 'Return to the
|
* The 'Pirate Problems' RETURN witness — the step's 'Return to the
|
||||||
* Neutral Capital World': the moment the ship is at the capital
|
* Neutral Capital World': the moment the ship is at the capital
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue