Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions scripts/smoke-secondary-journeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,27 @@ async function smokeLevel(session, route, sceneName, exceptions, {
`${sceneName} did not keep Peaks ambience batched: ${JSON.stringify(state)}`
);
}
if (
route === 'finalVoid' &&
(
state.currentEcologyPlacement?.supportId !==
'final-return-approach' ||
state.currentEcologyPlacement?.x <
state.currentEcologyPlacement?.supportLeft ||
state.currentEcologyPlacement?.x >
state.currentEcologyPlacement?.supportRight ||
Math.abs(
state.currentEcologyPlacement?.y -
state.currentEcologyPlacement?.supportTop
) > 8 ||
state.currentEcologyPlacement?.spawnDistance < 850
)
) {
throw new Error(
`${sceneName} placed the Current Heart inside its opening viewport: ` +
JSON.stringify(state.currentEcologyPlacement)
);
}
const renderBudget = CAMPAIGN_MOBILE_RENDER_BUDGETS[route];
if (!renderBudget) {
throw new Error(`${sceneName} has no authored mobile render budget`);
Expand Down Expand Up @@ -2654,6 +2675,20 @@ async function smokeLevel(session, route, sceneName, exceptions, {
);
}
}
if (
route === 'finalVoid' &&
(state.canvasWidth <= 480 || state.canvasHeight < 620) &&
[
'depth:106:visible',
'depth:115:visible',
'depth:179:visible'
].some(depth => framePacing.graphicsTweenDepths?.[depth])
) {
throw new Error(
`${sceneName} kept offscreen route decorations animating on mobile: ` +
JSON.stringify(framePacing.graphicsTweenDepths)
);
}
const forestEnemyScheduler = route === 'mythicalForest'
? await smokeForestSharedEnemyScheduler(session)
: null;
Expand Down Expand Up @@ -3588,6 +3623,78 @@ async function smokeLevel(session, route, sceneName, exceptions, {
await startCampaignScene(session, { route, sceneName });
await delay(400);
}
if (route === 'finalVoid') {
const mainRouteStaged = await evaluate(session, `(() => {
const scene = window.mythicalGame.scene.getScene(
'FinalVoidLevel'
);
const routeState = scene?.optionalRouteRewards?.get?.(
'final_trust_bridge'
);
const support = scene?.getTraversalSupport?.(
routeState?.choice?.mainSupportIds?.[0]
);
if (!scene?.player?.body || !routeState?.choice || !support?.body) {
return null;
}
scene.player.body.reset(
support.x,
support.body.top - scene.player.body.height - 4
);
scene.player.setVelocity?.(0, 0);
return { supportId: support.traversalId };
})()`);
if (!mainRouteStaged) {
throw new Error(`${sceneName} could not stage its low crossing`);
}
mainRouteEffect = await waitFor(
() => evaluate(session, `(() => {
const scene = window.mythicalGame.scene.getScene(
'FinalVoidLevel'
);
const routeState = scene?.optionalRouteRewards?.get?.(
'final_trust_bridge'
);
if (routeState?.choice?.selectedPath !== 'main') return null;
const optionalAccepted = scene.recordOptionalRouteProgress(
'final_trust_bridge',
{ x: scene.player.x, y: scene.player.y }
);
return {
selectedPath: routeState.choice.selectedPath,
finalRouteChoice: scene.finalRouteChoice,
optionalAccepted,
progress: routeState.progress,
completed: routeState.completed,
bondReserveReady: scene.bondReserveReady === true,
pickupActive: scene.optionalRoutePickup?.active === true
};
})()`),
{ timeoutMs: 2500, message: `${sceneName} low crossing selection` }
);
if (
mainRouteEffect.selectedPath !== 'main' ||
mainRouteEffect.finalRouteChoice !== 'main' ||
mainRouteEffect.optionalAccepted !== false ||
mainRouteEffect.progress !== 0 ||
mainRouteEffect.completed !== false ||
mainRouteEffect.bondReserveReady !== false ||
mainRouteEffect.pickupActive !== false ||
!choicePresentation.mainTradeoff.includes(
'RIFT DAMAGE + 2 GUARDS'
) ||
!choicePresentation.challengeLabel.includes('EARN RESCUE')
) {
throw new Error(
`${sceneName} low crossing failed to lock out its optional rescue: ` +
JSON.stringify({ choicePresentation, mainRouteEffect })
);
}
// Trust Bridge must be proven from an independent clean scene;
// selecting the low crossing intentionally locks out its reward.
await startCampaignScene(session, { route, sceneName });
await delay(400);
}

let rejectedOptionalPickup = null;
if (route === 'mythicalForest') {
Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/LevelTraversalQualityContract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,27 @@ describe('campaign traversal quality contracts', () => {
expect(source).toContain('this.refreshPersistedExpeditionRouteState();');
expect(source).toContain('incomingDamage = Math.max(0, this.health - 1);');
expect(source).toContain('this.bondReserveEcho?.destroy?.();');
expect(source).toContain('shouldAnimateFinalRouteDecorations()');
expect(source).toContain('{ animate: animateRouteDecorations }');

const smoke = fs.readFileSync(
path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'),
'utf8'
);
expect(smoke).toContain(
"mainRouteEffect.finalRouteChoice !== 'main'"
);
expect(smoke).toContain(
'low crossing failed to lock out its optional rescue'
);
expect(smoke).toContain(
'Trust Bridge must be proven from an independent clean scene'
);
expect(smoke).toContain(
'kept offscreen route decorations animating on mobile'
);
expect(smoke).toContain("'depth:115:visible'");
expect(smoke).toContain("'depth:179:visible'");
});

test.each([
Expand Down Expand Up @@ -2384,6 +2405,21 @@ describe('campaign traversal quality contracts', () => {
expect(smoke).toContain('departureCueX > 360');
});

test('Final Void keeps the optional Current Heart beyond its first bond signal', () => {
const shared = read('PlatformerLevelScene.js');
const smoke = fs.readFileSync(
path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'),
'utf8'
);

expect(shared).toContain("supportId: 'final-return-approach'");
expect(shared).toContain('x: 1230');
expect(smoke).toContain('spawnDistance < 850');
expect(smoke).toContain(
'placed the Current Heart inside its opening viewport'
);
});

test('Stellar Reef binds every required waypoint to a distinct relay support', () => {
const source = read('levels/ReefLevel.js');

Expand Down
28 changes: 17 additions & 11 deletions src/scenes/PlatformerLevelScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ const CURRENT_NODE_LEVEL_CONFIG = Object.freeze({
label: 'AURORA CURRENT'
}),
final_void_1: Object.freeze({
x: 420,
groundOffset: 130,
x: 1230,
groundOffset: 190,
supportId: 'final-return-approach',
label: 'CURRENT HEART'
})
});
Expand Down Expand Up @@ -1877,7 +1878,10 @@ class PlatformerLevelScene extends Phaser.Scene {
};
}

createTraversalLandingGuide(id, color = 0x7FFFD4, { depth = 179 } = {}) {
createTraversalLandingGuide(id, color = 0x7FFFD4, {
depth = 179,
animate = true
} = {}) {
const support = this.getTraversalSupport(id);
if (!support?.body) return null;

Expand All @@ -1896,14 +1900,16 @@ class PlatformerLevelScene extends Phaser.Scene {
support.x,
top - 2
);
const tween = this.tweens.add({
targets: visual,
alpha: { from: 0.55, to: 1 },
duration: 720,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
const tween = animate
? this.tweens.add({
targets: visual,
alpha: { from: 0.55, to: 1 },
duration: 720,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
})
: null;
const guide = { id, visual, tween };
this.traversalLandingGuides.push(guide);
return guide;
Expand Down
46 changes: 31 additions & 15 deletions src/scenes/levels/FinalVoidLevel.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,13 +650,15 @@ class FinalVoidLevel extends PlatformerLevelScene {
route.strokeCircle(1900, groundY - 430, 17);
route.strokeCircle(2270, groundY - 460, 12);

this.tweens.add({
targets: route,
alpha: { from: 0.5, to: 1 },
duration: 950,
yoyo: true,
repeat: -1
});
if (this.shouldAnimateFinalRouteDecorations()) {
this.tweens.add({
targets: route,
alpha: { from: 0.5, to: 1 },
duration: 950,
yoyo: true,
repeat: -1
});
}

const fractureRouteMarker = this.add.text(1690, groundY - 82, '', {
fontSize: '11px',
Expand Down Expand Up @@ -883,6 +885,15 @@ class FinalVoidLevel extends PlatformerLevelScene {
});
}

shouldAnimateFinalRouteDecorations() {
const { width, height } = this.cameras.main;
return !(
this.isMobile ||
width <= 480 ||
height < 620
);
}

createVoidFractures() {
const fractures = [
{ x: 930, width: 120, label: 'JUMP THE RIFT →' },
Expand Down Expand Up @@ -917,13 +928,15 @@ class FinalVoidLevel extends PlatformerLevelScene {
}
).setOrigin(0.5).setDepth(182);

this.tweens.add({
targets: visual,
alpha: { from: 0.5, to: 1 },
duration: 850,
yoyo: true,
repeat: -1
});
if (this.shouldAnimateFinalRouteDecorations()) {
this.tweens.add({
targets: visual,
alpha: { from: 0.5, to: 1 },
duration: 850,
yoyo: true,
repeat: -1
});
}

this.physics.add.overlap(this.player, zone, () => {
if (!this.isInvincible && !this.bossDefeated) {
Expand All @@ -935,6 +948,8 @@ class FinalVoidLevel extends PlatformerLevelScene {
}

createBondAnchors() {
const animateRouteDecorations =
this.shouldAnimateFinalRouteDecorations();
const anchors = [
{
id: 'final_bond_1',
Expand Down Expand Up @@ -987,7 +1002,8 @@ class FinalVoidLevel extends PlatformerLevelScene {
zone,
landingGuide: this.createTraversalLandingGuide(
anchor.activationSupportIds[0],
0xA9F3E4
0xA9F3E4,
{ animate: animateRouteDecorations }
),
activated: false
};
Expand Down