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
130 changes: 128 additions & 2 deletions scripts/smoke-secondary-journeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const CAMPAIGN_MOBILE_RENDER_BUDGETS = Object.freeze({
}),
crystalCaves: Object.freeze({
displayCount: 225,
activeTweenCount: 60,
activeTweenCount: 30,
performanceTier: 'mobile'
}),
reef: Object.freeze({
Expand Down Expand Up @@ -2511,7 +2511,18 @@ async function smokeLevel(session, route, sceneName, exceptions, {
state.caveCrystalRendering?.layerCount !== 1 ||
state.caveAmbientRendering?.parallaxLayerCount !== 2 ||
state.caveAmbientRendering?.storyDecorationTweenCount !== 0 ||
state.caveAmbientRendering?.coinLayerTweenCount !== 0
state.caveAmbientRendering?.coinLayerTweenCount !== 0 ||
state.currentEcologyPlacement?.supportId !==
'caves-chamber-bridge' ||
state.currentEcologyPlacement?.x <
state.currentEcologyPlacement?.supportLeft ||
state.currentEcologyPlacement?.x >
state.currentEcologyPlacement?.supportRight ||
Math.abs(
state.currentEcologyPlacement?.y -
state.currentEcologyPlacement?.supportTop
) > 8 ||
state.currentEcologyPlacement?.spawnDistance < 1000
)
) {
throw new Error(
Expand Down Expand Up @@ -2650,6 +2661,21 @@ async function smokeLevel(session, route, sceneName, exceptions, {
JSON.stringify({ renderBudget, framePacing })
);
}
if (
route === 'crystalCaves' &&
(state.canvasWidth <= 480 || state.canvasHeight < 620) &&
[
'depth:-5:visible',
'depth:44:visible',
'depth:45:visible',
'depth:84:visible'
].some(depth => framePacing.graphicsTweenDepths?.[depth])
) {
throw new Error(
`${sceneName} kept offscreen cave guidance animating on mobile: ` +
JSON.stringify(framePacing.graphicsTweenDepths)
);
}
if (route === 'mythicalForest') {
if (
!framePacing.forestEnemyOverlapActive ||
Expand Down Expand Up @@ -3649,6 +3675,106 @@ async function smokeLevel(session, route, sceneName, exceptions, {
await startCampaignScene(session, { route, sceneName });
await delay(400);
}
if (route === 'crystalCaves') {
const mainRouteStaged = await evaluate(session, `(() => {
const scene = window.mythicalGame.scene.getScene(
'CrystalCavesLevel'
);
const routeState = scene?.optionalRouteRewards?.get?.(
'caves_secret_slide'
);
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 lower passage`);
}
mainRouteEffect = await waitFor(
() => evaluate(session, `(() => {
const scene = window.mythicalGame.scene.getScene(
'CrystalCavesLevel'
);
const routeState = scene?.optionalRouteRewards?.get?.(
'caves_secret_slide'
);
if (routeState?.choice?.selectedPath !== 'main') return null;
const optionalAccepted = scene.recordOptionalRouteProgress(
'caves_secret_slide',
{ x: scene.player.x, y: scene.player.y }
);
const energyBefore = scene.crystalEnergy;
const multiplierBefore = scene.nextRangedDamageMultiplier;
const objectiveBefore = scene.getCrystalObjectiveText?.() || '';
const persistedBefore = scene.getExpeditionRouteState?.()
.crystalFocusReady;
scene.performRangedAttack();
return {
selectedPath: routeState.choice.selectedPath,
crystalChamberRoute: scene.crystalChamberRoute,
optionalAccepted,
progress: routeState.progress,
completed: routeState.completed,
wardPickupActive: scene.crystalWardPickup?.active === true,
optionalPickupsRemaining: (
scene.collectibles?.getChildren?.() || []
).filter(item => (
item?.optionalRouteId === 'caves_secret_slide' &&
item.active !== false
)).length,
objective: scene.getCrystalObjectiveText?.() || '',
objectiveBefore,
energyBefore,
energyAfter: scene.crystalEnergy,
multiplierBefore,
multiplierAfter: scene.nextRangedDamageMultiplier,
persistedBefore,
persistedAfter: scene.getExpeditionRouteState?.()
.crystalFocusReady
};
})()`),
{ timeoutMs: 2500, message: `${sceneName} lower passage selection` }
);
if (
mainRouteEffect.selectedPath !== 'main' ||
mainRouteEffect.crystalChamberRoute !== 'main' ||
mainRouteEffect.optionalAccepted !== false ||
mainRouteEffect.progress !== 0 ||
mainRouteEffect.completed !== false ||
mainRouteEffect.wardPickupActive !== false ||
mainRouteEffect.optionalPickupsRemaining !== 0 ||
mainRouteEffect.multiplierBefore !== 2 ||
mainRouteEffect.multiplierAfter !== 1 ||
mainRouteEffect.energyAfter !== mainRouteEffect.energyBefore ||
mainRouteEffect.persistedBefore !== true ||
mainRouteEffect.persistedAfter !== false ||
!mainRouteEffect.objectiveBefore.includes(
'CRYSTAL FOCUS x2 READY'
) ||
!mainRouteEffect.objective.includes('CRYSTAL FOCUS SPENT') ||
!choicePresentation.mainTradeoff.includes('NEXT SHOT x2') ||
!choicePresentation.challengeLabel.includes('1-HIT WARD') ||
!choicePresentation.rewardLabel.includes('1 HIT')
) {
throw new Error(
`${sceneName} route rewards contradicted their promise: ` +
JSON.stringify({ choicePresentation, mainRouteEffect })
);
}
// The Spider Walk must be proven from an independent clean scene;
// choosing the lower passage intentionally retires its Ward.
await startCampaignScene(session, { route, sceneName });
await delay(400);
}
if (route === 'voidPeaks') {
const mainRouteStaged = await evaluate(session, `(() => {
const scene = window.mythicalGame.scene.getScene(
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/CurrentEcologyGameplayContract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('Current ecology gameplay contract', () => {
expect(source).toContain('aurora_depths_1: Object.freeze({');
expect(source).toContain('final_void_1: Object.freeze({');
expect(source).toContain('this.createCurrentEcologyNode();');
expect(source).toContain("supportId: 'caves-chamber-bridge'");
});

test('applies and explains one bounded upstream consequence', () => {
Expand Down
32 changes: 30 additions & 2 deletions src/__tests__/LevelTraversalQualityContract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,11 @@ describe('campaign traversal quality contracts', () => {

test('Crystal Spider Walk is a persistent, mutually exclusive chamber route', () => {
const source = read('levels/CrystalCavesLevel.js');
const platformerSource = read('PlatformerLevelScene.js');
const smoke = fs.readFileSync(
path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'),
'utf8'
);

expect(source).toContain('const CAVE_ENCOUNTER_PLAN = Object.freeze([');
expect(source).toContain("beat: 'opening-stomp-lesson'");
Expand All @@ -1426,11 +1431,34 @@ describe('campaign traversal quality contracts', () => {
expect(source).toContain("shield.optionalRouteId = 'caves_secret_slide'");
expect(source).toContain("onMainSelected: () => this.selectCrystalChamberRoute('main')");
expect(source).toContain("onOptionalSelected: () => this.selectCrystalChamberRoute('optional')");
expect(source).toContain(
"mainTradeoff: 'SHORT // ARMORED CRAWLER\\nEARNS: CRYSTAL FOCUS // NEXT SHOT x2'"
);
expect(source).toContain(
"challengeLabel: 'SPIDER + SLIDE // EARN 1-HIT WARD'"
);
expect(source).toContain("'CALM THE CRYSTAL SPIDER FIRST'");
expect(source).toContain('restoreCrystalChamberRoute(resume.routeState');
expect(source).toContain('crystalWardGuardCharges');
expect(source).toContain('crystalFocusReady');
expect(source).toContain('onNextRangedDamageConsumed()');
expect(source).toContain('shouldAnimateCrystalRouteDecorations()');
expect(source).toContain('{ depth: 84, animate: animateRouteDecorations }');
expect(source).toContain('!this.bossFightActive');
expect(source).toContain("this.grantOptionalRouteGuard('CRYSTAL WARD', 1)");
expect(source).not.toContain('this.activateShield();');
expect(platformerSource).toContain(
'this.onNextRangedDamageConsumed?.(rangedDamage);'
);
expect(smoke).toContain('lower passage selection');
expect(smoke).toContain('CRYSTAL FOCUS x2 READY');
expect(smoke).toContain(
'The Spider Walk must be proven from an independent clean scene'
);
expect(smoke).toContain(
'kept offscreen cave guidance animating on mobile'
);
expect(smoke).toContain("'caves-chamber-bridge'");
});

test('Final Void rewards the Trust Bridge with one reliable rescue', () => {
Expand Down Expand Up @@ -1525,8 +1553,8 @@ describe('campaign traversal quality contracts', () => {
'levels/CrystalCavesLevel.js',
"id: 'caves_secret_slide'",
"mainLabel: 'LOWER PASSAGE →'",
"mainTradeoff: 'SHORT // ARMORED CRAWLER'",
"challengeLabel: 'SPIDER + CRYSTAL SLIDE'"
"mainTradeoff: 'SHORT // ARMORED CRAWLER\\nEARNS: CRYSTAL FOCUS // NEXT SHOT x2'",
"challengeLabel: 'SPIDER + SLIDE // EARN 1-HIT WARD'"
],
[
'levels/ReefLevel.js',
Expand Down
56 changes: 56 additions & 0 deletions src/__tests__/SecondExpeditionRescueLoop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,62 @@ describe('second expedition rescue loop', () => {
);
});

test('gives the armored lower passage a persisted Crystal Focus shot', () => {
const CrystalCavesLevel = loadLevelClass();
const scene = new CrystalCavesLevel();
const choice = {};
scene.optionalRouteRewards = new Map([
['caves_secret_slide', {
completed: false,
choice
}]
]);
scene.nextRangedDamageMultiplier = 1;
scene.clearCrystalWardPickup = jest.fn();
scene.refreshPersistedExpeditionRouteState = jest.fn();

expect(scene.selectCrystalChamberRoute('main')).toBe(true);
expect(scene.crystalChamberRoute).toBe('main');
expect(scene.crystalFocusReady).toBe(true);
expect(scene.nextRangedDamageMultiplier).toBe(2);
expect(scene.getCrystalChamberStatusText('fallback')).toContain(
'CRYSTAL FOCUS x2 READY'
);
expect(scene.getExpeditionRouteState().crystalFocusReady).toBe(true);
expect(scene.selectCrystalChamberRoute('optional')).toBe(false);

scene.onNextRangedDamageConsumed(2);
expect(scene.crystalFocusReady).toBe(false);
expect(scene.getCrystalChamberStatusText('fallback')).toContain(
'CRYSTAL FOCUS SPENT'
);
expect(scene.getExpeditionRouteState().crystalFocusReady).toBe(false);
});

test('restores Crystal Focus and keeps compact route decoration static', () => {
const CrystalCavesLevel = loadLevelClass();
const scene = new CrystalCavesLevel();
scene.optionalRouteRewards = new Map([
['caves_secret_slide', { completed: false, choice: {} }]
]);
scene.nextRangedDamageMultiplier = 1;
scene.clearCrystalWardPickup = jest.fn();

expect(scene.restoreCrystalChamberRoute({
crystalChamberRoute: 'main',
crystalFocusReady: true
})).toBe(true);
expect(scene.crystalFocusReady).toBe(true);
expect(scene.nextRangedDamageMultiplier).toBe(2);

scene.isMobile = true;
scene.cameras = { main: { width: 390, height: 844 } };
expect(scene.shouldAnimateCrystalRouteDecorations()).toBe(false);
scene.isMobile = false;
scene.cameras.main = { width: 1280, height: 720 };
expect(scene.shouldAnimateCrystalRouteDecorations()).toBe(true);
});

test('changes the live objective from route-finding to companion rescue and guardian contact', () => {
const CrystalCavesLevel = loadLevelClass();
const scene = new CrystalCavesLevel();
Expand Down
4 changes: 3 additions & 1 deletion src/scenes/PlatformerLevelScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ const CURRENT_NODE_LEVEL_CONFIG = Object.freeze({
label: 'ROOT CURRENT'
}),
crystal_caves_1: Object.freeze({
x: 760,
x: 1500,
groundOffset: 125,
supportId: 'caves-chamber-bridge',
label: 'CRYSTAL CURRENT'
}),
reef_1: Object.freeze({
Expand Down Expand Up @@ -5470,6 +5471,7 @@ class PlatformerLevelScene extends Phaser.Scene {
);
this.nextRangedDamageMultiplier = 1;
if (rangedDamage > 1) {
this.onNextRangedDamageConsumed?.(rangedDamage);
this.showFloatingText(
`POWER SHOT x${rangedDamage}`,
this.player.x,
Expand Down
Loading