diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index e84689a..2dc2852 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -471,10 +471,7 @@ try { await page.mouse.click(1120, 38); await page.waitForTimeout(120); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); for (let i = 0; i < 24; i += 1) { const isSecondBattleActive = await page.evaluate(() => { @@ -546,10 +543,7 @@ try { throw new Error(`Expected second camp sortie preparation overlay: ${JSON.stringify(secondCampSortieState)}`); } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); for (let i = 0; i < 24; i += 1) { const isThirdBattleActive = await page.evaluate(() => { @@ -618,10 +612,7 @@ try { throw new Error(`Expected third camp sortie preparation overlay: ${JSON.stringify(thirdCampSortieState)}`); } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); for (let i = 0; i < 26; i += 1) { const isFourthBattleActive = await page.evaluate(() => { @@ -690,10 +681,7 @@ try { throw new Error(`Expected fourth camp sortie preparation overlay: ${JSON.stringify(fourthCampSortieState)}`); } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); for (let i = 0; i < 26; i += 1) { const isFifthBattleActive = await page.evaluate(() => { @@ -772,10 +760,7 @@ try { throw new Error(`Expected fifth camp sortie preparation overlay: ${JSON.stringify(fifthCampSortieState)}`); } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); for (let i = 0; i < 26; i += 1) { const isSixthBattleActive = await page.evaluate(() => { @@ -870,10 +855,7 @@ try { throw new Error(`Expected sortie selection toggle to bench Guan Yu while keeping Liu Bei/Zhang Fei: ${JSON.stringify(sixthCampAfterSortieToggle)}`); } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-seventh-bridge-story.png', fullPage: true }); for (let i = 0; i < 30; i += 1) { @@ -967,10 +949,7 @@ try { } assertSortieTacticalRoster(seventhCampRecruitSortieState, ['liu-bei', 'zhang-fei', 'jian-yong', 'mi-zhu']); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xuzhou-entrust-story.png', fullPage: true }); for (let i = 0; i < 10; i += 1) { @@ -1050,10 +1029,7 @@ try { } assertSortieTacticalRoster(eighthCampMiZhuSortieState, ['liu-bei', 'zhang-fei', 'jian-yong', 'mi-zhu']); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-lubu-refuge-story.png', fullPage: true }); for (let i = 0; i < 10; i += 1) { @@ -1134,10 +1110,7 @@ try { } assertSortieTacticalRoster(ninthCampGuanYuSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xuzhou-loss-story.png', fullPage: true }); for (let i = 0; i < 14; i += 1) { @@ -1211,10 +1184,7 @@ try { } assertSortieTacticalRoster(tenthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-cao-cao-refuge-story.png', fullPage: true }); for (let i = 0; i < 16; i += 1) { @@ -1288,10 +1258,7 @@ try { } assertSortieTacticalRoster(eleventhCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xiapi-siege-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { @@ -1374,10 +1341,7 @@ try { throw new Error(`Expected recruit officers to be marked in sortie roster: ${JSON.stringify(twelfthCampSortieState.sortieRoster)}`); } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xiapi-final-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { @@ -1465,10 +1429,7 @@ try { await page.screenshot({ path: 'dist/verification-cao-break-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-cao-break-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { @@ -1543,10 +1504,7 @@ try { await page.screenshot({ path: 'dist/verification-yuan-refuge-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yuan-refuge-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { @@ -1648,10 +1606,7 @@ try { await page.screenshot({ path: 'dist/verification-liu-biao-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-liu-biao-story.png', fullPage: true }); for (let i = 0; i < 22; i += 1) { @@ -1762,10 +1717,7 @@ try { await page.screenshot({ path: 'dist/verification-wolong-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-wolong-story.png', fullPage: true }); for (let i = 0; i < 24; i += 1) { @@ -1888,10 +1840,7 @@ try { await page.screenshot({ path: 'dist/verification-bowang-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-bowang-story.png', fullPage: true }); for (let i = 0; i < 26; i += 1) { @@ -2049,10 +1998,7 @@ try { await page.screenshot({ path: 'dist/verification-changban-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-changban-story.png', fullPage: true }); for (let i = 0; i < 30; i += 1) { @@ -2187,10 +2133,7 @@ try { await page.screenshot({ path: 'dist/verification-jiangdong-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jiangdong-story.png', fullPage: true }); for (let i = 0; i < 32; i += 1) { @@ -2326,10 +2269,7 @@ try { await page.screenshot({ path: 'dist/verification-redcliffs-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-redcliffs-story.png', fullPage: true }); for (let i = 0; i < 34; i += 1) { @@ -2466,10 +2406,7 @@ try { await page.screenshot({ path: 'dist/verification-redcliffs-fire-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-redcliffs-fire-story.png', fullPage: true }); for (let i = 0; i < 36; i += 1) { @@ -2608,10 +2545,7 @@ try { await page.screenshot({ path: 'dist/verification-jingzhou-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jingzhou-story.png', fullPage: true }); for (let i = 0; i < 40; i += 1) { @@ -2752,10 +2686,7 @@ try { await page.screenshot({ path: 'dist/verification-guiyang-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-guiyang-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -2895,10 +2826,7 @@ try { await page.screenshot({ path: 'dist/verification-wuling-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-wuling-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3040,10 +2968,7 @@ try { await page.screenshot({ path: 'dist/verification-changsha-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-changsha-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3190,10 +3115,7 @@ try { await page.screenshot({ path: 'dist/verification-yizhou-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yizhou-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3336,10 +3258,7 @@ try { await page.screenshot({ path: 'dist/verification-fupass-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fupass-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3484,10 +3403,7 @@ try { await page.screenshot({ path: 'dist/verification-luo-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-luo-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3633,10 +3549,7 @@ try { await page.screenshot({ path: 'dist/verification-luofeng-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-luofeng-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3780,10 +3693,7 @@ try { await page.screenshot({ path: 'dist/verification-luo-main-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-luo-main-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -3933,10 +3843,7 @@ try { await page.screenshot({ path: 'dist/verification-mianzhu-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-mianzhu-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -4087,10 +3994,7 @@ try { await page.screenshot({ path: 'dist/verification-chengdu-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-chengdu-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -4242,10 +4146,7 @@ try { await page.screenshot({ path: 'dist/verification-jiameng-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jiameng-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -4398,10 +4299,7 @@ try { await page.screenshot({ path: 'dist/verification-yangping-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yangping-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -4555,10 +4453,7 @@ try { await page.screenshot({ path: 'dist/verification-dingjun-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-dingjun-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -4708,10 +4603,7 @@ try { await page.screenshot({ path: 'dist/verification-hanzhong-decisive-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-hanzhong-decisive-story.png', fullPage: true }); for (let i = 0; i < 55; i += 1) { @@ -4809,10 +4701,7 @@ try { } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-hanzhong-king-council-story.png', fullPage: true }); for (let i = 0; i < 16; i += 1) { @@ -4869,10 +4758,7 @@ try { } await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-shu-han-foundation-story.png', fullPage: true }); for (let i = 0; i < 16; i += 1) { @@ -4982,10 +4868,7 @@ try { await page.screenshot({ path: 'dist/verification-jing-defense-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jing-defense-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { @@ -5135,10 +5018,7 @@ try { await page.screenshot({ path: 'dist/verification-fan-castle-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fan-castle-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { @@ -5288,10 +5168,7 @@ try { await page.screenshot({ path: 'dist/verification-han-river-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-han-river-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { @@ -5442,10 +5319,7 @@ try { await page.screenshot({ path: 'dist/verification-fan-siege-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fan-siege-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { @@ -5600,10 +5474,7 @@ try { await page.screenshot({ path: 'dist/verification-jing-rear-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jing-rear-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { @@ -5758,10 +5629,7 @@ try { await page.screenshot({ path: 'dist/verification-gongan-collapse-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-gongan-collapse-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { @@ -5915,10 +5783,7 @@ try { await page.screenshot({ path: 'dist/verification-maicheng-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-maicheng-story.png', fullPage: true }); for (let i = 0; i < 55; i += 1) { @@ -6072,10 +5937,7 @@ try { await page.screenshot({ path: 'dist/verification-yiling-vanguard-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yiling-vanguard-story.png', fullPage: true }); for (let i = 0; i < 55; i += 1) { @@ -6229,10 +6091,7 @@ try { await page.screenshot({ path: 'dist/verification-yiling-fire-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yiling-fire-story.png', fullPage: true }); for (let i = 0; i < 55; i += 1) { @@ -6332,10 +6191,7 @@ try { await page.screenshot({ path: 'dist/verification-baidi-entrustment-prep.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-baidi-entrustment-story.png', fullPage: true }); for (let i = 0; i < 30; i += 1) { @@ -6455,10 +6311,7 @@ try { await page.screenshot({ path: 'dist/verification-nanzhong-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-nanzhong-story.png', fullPage: true }); for (let i = 0; i < 55; i += 1) { @@ -6616,10 +6469,7 @@ try { await page.screenshot({ path: 'dist/verification-meng-huo-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-meng-huo-story.png', fullPage: true }); for (let i = 0; i < 60; i += 1) { @@ -6778,10 +6628,7 @@ try { await page.screenshot({ path: 'dist/verification-second-capture-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-second-capture-story.png', fullPage: true }); for (let i = 0; i < 60; i += 1) { @@ -6942,10 +6789,7 @@ try { await page.screenshot({ path: 'dist/verification-third-capture-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-third-capture-story.png', fullPage: true }); for (let i = 0; i < 60; i += 1) { @@ -7107,10 +6951,7 @@ try { await page.screenshot({ path: 'dist/verification-fourth-capture-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fourth-capture-story.png', fullPage: true }); for (let i = 0; i < 70; i += 1) { @@ -7273,10 +7114,7 @@ try { await page.screenshot({ path: 'dist/verification-fifth-capture-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifth-capture-story.png', fullPage: true }); for (let i = 0; i < 70; i += 1) { @@ -7438,10 +7276,7 @@ try { await page.screenshot({ path: 'dist/verification-sixth-capture-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixth-capture-story.png', fullPage: true }); for (let i = 0; i < 70; i += 1) { @@ -7603,10 +7438,7 @@ try { await page.screenshot({ path: 'dist/verification-final-capture-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-final-capture-story.png', fullPage: true }); for (let i = 0; i < 70; i += 1) { @@ -7720,10 +7552,7 @@ try { await page.screenshot({ path: 'dist/verification-northern-prep-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-northern-prep-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { @@ -7827,10 +7656,7 @@ try { await page.screenshot({ path: 'dist/verification-fifty-fifth-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-fifth-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -7977,10 +7803,7 @@ try { await page.screenshot({ path: 'dist/verification-fifty-sixth-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-sixth-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -8129,10 +7952,7 @@ try { await page.screenshot({ path: 'dist/verification-fifty-seventh-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-seventh-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -8282,10 +8102,7 @@ try { await page.screenshot({ path: 'dist/verification-fifty-eighth-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-eighth-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -8433,10 +8250,7 @@ try { await page.screenshot({ path: 'dist/verification-fifty-ninth-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-ninth-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -8587,10 +8401,7 @@ try { await page.screenshot({ path: 'dist/verification-sixtieth-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixtieth-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -8746,10 +8557,7 @@ try { await page.screenshot({ path: 'dist/verification-sixty-first-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixty-first-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -8906,10 +8714,7 @@ try { await page.screenshot({ path: 'dist/verification-sixty-second-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixty-second-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { @@ -9110,6 +8915,19 @@ async function canReach(url) { } } +async function waitForStoryReady(page) { + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + const storyState = window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.(); + return ( + activeScenes.includes('StoryScene') && + storyState?.ready === true && + storyState?.backgroundReady === true && + storyState?.activeTexture === storyState?.background + ); + }, undefined, { timeout: 90000 }); +} + function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/assets/images/story/17-tianshui-front.png b/src/assets/images/story/17-tianshui-front.png new file mode 100644 index 0000000..30502b5 Binary files /dev/null and b/src/assets/images/story/17-tianshui-front.png differ diff --git a/src/assets/images/story/18-jieting-crisis.png b/src/assets/images/story/18-jieting-crisis.png new file mode 100644 index 0000000..984ec03 Binary files /dev/null and b/src/assets/images/story/18-jieting-crisis.png differ diff --git a/src/assets/images/story/19-chencang-siege.png b/src/assets/images/story/19-chencang-siege.png new file mode 100644 index 0000000..548ed8e Binary files /dev/null and b/src/assets/images/story/19-chencang-siege.png differ diff --git a/src/assets/images/story/20-wudu-yinping.png b/src/assets/images/story/20-wudu-yinping.png new file mode 100644 index 0000000..915e9f0 Binary files /dev/null and b/src/assets/images/story/20-wudu-yinping.png differ diff --git a/src/assets/images/story/21-hanzhong-rain-defense.png b/src/assets/images/story/21-hanzhong-rain-defense.png new file mode 100644 index 0000000..310fe26 Binary files /dev/null and b/src/assets/images/story/21-hanzhong-rain-defense.png differ diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index f6224ec..1ac7874 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3594,7 +3594,7 @@ export const fiftySixthBattleIntroPages: StoryPage[] = [ id: 'fifty-sixth-tianshui-rumor', bgm: 'story-dark', chapter: '천수 방면', - background: 'story-northern', + background: 'story-tianshui-front', speaker: '마량', text: '기산 길목을 확보하자 천수와 가정 사이의 성들이 급히 봉화를 올렸습니다. 위군은 조진의 전초대가 물러난 틈을 곽회에게 맡기고, 젊은 장수 강유가 산길의 허점을 살피고 있다고 합니다.' }, @@ -3602,7 +3602,7 @@ export const fiftySixthBattleIntroPages: StoryPage[] = [ id: 'fifty-sixth-qishan-council', bgm: 'battle-prep', chapter: '진군로 재편', - background: 'story-northern', + background: 'story-tianshui-front', speaker: '제갈량', text: '북벌은 빠르게 밀어붙이는 것보다 끊기지 않는 것이 중요합니다. 조운은 우회로를 보고, 황권과 마량은 마을과 창고를 묶고, 왕평은 가정으로 이어지는 고개를 살펴 주십시오.' }, @@ -3610,7 +3610,7 @@ export const fiftySixthBattleIntroPages: StoryPage[] = [ id: 'fifty-sixth-jiang-wei-shadow', bgm: 'battle-prep', chapter: '젊은 장수의 그림자', - background: 'story-northern', + background: 'story-tianshui-front', speaker: '조운', text: '강유라는 장수는 평범한 수비장이 아닙니다. 정면에서만 치면 곽회의 방어선에 묶이고, 너무 깊이 들어가면 가정 방면의 퇴로가 끊깁니다. 이번 싸움은 적장 하나보다 전선 전체를 읽는 싸움이 되겠습니다.' } @@ -3621,14 +3621,14 @@ export const fiftySixthBattleVictoryPages: StoryPage[] = [ id: 'fifty-sixth-victory-tianshui-road', bgm: 'militia-theme', chapter: '천수 진군로 확보', - background: 'story-northern', + background: 'story-tianshui-front', text: '곽회의 방어선은 물러났고, 천수로 향하는 길에는 촉한군의 표식이 세워졌습니다. 그러나 강유의 움직임은 끝내 가볍지 않았고, 위군은 가정 방면으로 더 넓은 방어선을 펼치기 시작했습니다.' }, { id: 'fifty-sixth-victory-jiang-wei', bgm: 'story-dark', chapter: '강유의 이름', - background: 'story-northern', + background: 'story-tianshui-front', speaker: '제갈량', text: '강유는 아직 우리의 사람이 아니지만, 그 재능은 전장 위에 분명히 드러났습니다. 억지로 꺾기보다 길을 남겨 두어야 합니다. 다음 전선에서는 사람을 얻는 일과 땅을 얻는 일이 함께 시험받을 것입니다.' }, @@ -3636,7 +3636,7 @@ export const fiftySixthBattleVictoryPages: StoryPage[] = [ id: 'fifty-sixth-next-jieting', bgm: 'story-dark', chapter: '가정의 불안', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '왕평', text: '가정으로 이어지는 고개는 생각보다 좁고 물길도 멀리 있습니다. 진영을 어디에 세우느냐에 따라 북벌 전체가 흔들릴 수 있습니다. 다음 전장은 승리보다 배치의 판단이 더 무겁게 다가올 것입니다.' } @@ -3647,7 +3647,7 @@ export const fiftySeventhBattleIntroPages: StoryPage[] = [ id: 'fifty-seventh-jieting-order', bgm: 'story-dark', chapter: '가정 방면', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '제갈량', text: '천수 길목은 열렸으나 가정의 고개가 흔들리면 북벌의 군량과 퇴로가 한꺼번에 끊깁니다. 높은 곳만 바라보는 진영은 오래 버티지 못합니다. 물길과 길목을 함께 지키십시오.' }, @@ -3655,7 +3655,7 @@ export const fiftySeventhBattleIntroPages: StoryPage[] = [ id: 'fifty-seventh-ma-su-insists', bgm: 'battle-prep', chapter: '마속의 주장', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '왕평', text: '마속은 고지를 잡으면 적이 감히 오르지 못한다고 말합니다. 그러나 고개 아래 물길과 수레 길을 잃으면 위군은 우리를 치지 않고도 말릴 수 있습니다. 이번 출전은 강한 장수보다 지형을 읽을 장수가 필요합니다.' }, @@ -3663,7 +3663,7 @@ export const fiftySeventhBattleIntroPages: StoryPage[] = [ id: 'fifty-seventh-jiang-wei-contact', bgm: 'battle-prep', chapter: '강유 접촉', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '마량', text: '천수에서 남긴 글이 강유에게 닿았습니다. 그는 아직 위군의 장수로 서 있지만, 싸움이 명분을 잃는 순간 마음이 흔들릴 것입니다. 가정의 길을 지키며 강유에게 돌아올 길도 남겨 두어야 합니다.' } @@ -3674,14 +3674,14 @@ export const fiftySeventhBattleVictoryPages: StoryPage[] = [ id: 'fifty-seventh-victory-jieting-held', bgm: 'militia-theme', chapter: '가정 방면 수습', - background: 'story-northern', + background: 'story-jieting-crisis', text: '장합의 선봉은 고개 아래에서 멈췄고, 촉한군은 가정의 물길과 수레 길을 간신히 잇는 데 성공했습니다. 마속의 고집은 큰 위험을 남겼지만, 왕평의 지형 판단이 북벌의 퇴로를 붙잡았습니다.' }, { id: 'fifty-seventh-victory-jiang-wei-joins', bgm: 'story-dark', chapter: '새 장수', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '강유', text: '강유는 위군의 의심과 백성의 말 사이에서 끝내 마음을 정했습니다. 오늘부터 그는 촉한의 깃발 아래 서지만, 새로 얻은 장수 하나가 곧바로 모든 길을 열어 주지는 않습니다. 다음 출전부터는 더 넓어진 장수진 안에서 누구를 고를지 판단해야 합니다.' }, @@ -3689,7 +3689,7 @@ export const fiftySeventhBattleVictoryPages: StoryPage[] = [ id: 'fifty-seventh-next-front', bgm: 'story-dark', chapter: '북벌의 다음 부담', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '제갈량', text: '가정의 위기는 수습되었으나 위군은 이제 우리의 길과 사람을 모두 보았습니다. 강유를 얻은 기쁨과 마속의 실책을 함께 품고, 다음 북벌에서는 더 엄격한 편성과 보급 판단이 필요할 것입니다.' } @@ -3700,7 +3700,7 @@ export const fiftyEighthBattleIntroPages: StoryPage[] = [ id: 'fifty-eighth-qishan-retreat-council', bgm: 'story-dark', chapter: '기산 퇴각 명령', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '제갈량', text: '가정의 물길은 가까스로 지켰지만 북방의 군량 길은 이미 오래 늘어졌습니다. 지금 억지로 깊게 밀고 들어가면 승리가 아니라 고립을 얻게 됩니다. 기산으로 돌아가는 수레 길을 열고, 병사와 장부를 먼저 살려야 합니다.' }, @@ -3708,7 +3708,7 @@ export const fiftyEighthBattleIntroPages: StoryPage[] = [ id: 'fifty-eighth-jiang-wei-first-sortie', bgm: 'battle-prep', chapter: '강유의 첫 출전', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '강유', text: '제가 알던 천수의 낮은 길이 이제 촉한의 퇴로가 되었습니다. 아직 모두가 저를 믿지는 못하겠지만, 오늘은 말보다 길로 증명하겠습니다. 조운과 마대가 회수 길을 지키고, 왕평이 물길을 보면 위군의 추격을 끊을 수 있습니다.' }, @@ -3716,7 +3716,7 @@ export const fiftyEighthBattleIntroPages: StoryPage[] = [ id: 'fifty-eighth-sima-yi-shadow', bgm: 'story-dark', chapter: '새로운 그림자', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '마량', text: '장합의 추격만 막으면 끝나는 전장이 아닙니다. 위군 본영 뒤에서 사마의가 군량 수레가 어디로 빠지는지 보고 있습니다. 강유를 내보내면 길은 넓어지지만, 누군가는 조운이나 왕평의 자리를 양보해야 합니다.' } @@ -3727,14 +3727,14 @@ export const fiftyEighthBattleVictoryPages: StoryPage[] = [ id: 'fifty-eighth-victory-retreat-road', bgm: 'militia-theme', chapter: '기산 퇴로 확보', - background: 'story-northern', + background: 'story-jieting-crisis', text: '촉한군은 군량 수레를 잃지 않고 기산의 낮은 길로 물러났습니다. 장합의 추격은 물길 앞에서 꺾였고, 사마의의 본대는 더 깊게 들어오지 못했습니다. 패배를 승리로 꾸밀 수는 없지만, 다음 북벌을 위한 병사와 장부는 지켜냈습니다.' }, { id: 'fifty-eighth-victory-jiang-wei-reviewed', bgm: 'story-dark', chapter: '강유의 첫 평가', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '제갈량', text: '강유는 천수의 길을 숨기지 않았고, 조운과 마대는 병사들이 돌아올 시간을 벌었습니다. 왕평의 물길 판단도 여전히 군의 목숨줄입니다. 이제부터 출전 명단은 단순히 강한 장수를 고르는 일이 아니라, 어떤 길을 택할지 정하는 일이 됩니다.' }, @@ -3742,7 +3742,7 @@ export const fiftyEighthBattleVictoryPages: StoryPage[] = [ id: 'fifty-eighth-next-northern-plan', bgm: 'story-dark', chapter: '다음 북벌을 위하여', - background: 'story-northern', + background: 'story-jieting-crisis', speaker: '강유', text: '이번 후퇴로 저는 촉한의 군율과 병사들의 숨을 배웠습니다. 다음에는 물러나는 길이 아니라 나아가는 길을 열겠습니다. 승상께서 다시 북쪽을 보실 때, 제가 먼저 그 길을 살피겠습니다.' } @@ -3753,7 +3753,7 @@ export const fiftyNinthBattleIntroPages: StoryPage[] = [ id: 'fifty-ninth-chencang-order', bgm: 'story-dark', chapter: '진창 성채', - background: 'story-northern', + background: 'story-chencang-siege', speaker: '제갈량', text: '기산에서 병사를 살렸기에 다시 북쪽을 볼 수 있습니다. 그러나 이번 길은 넓은 들이 아니라 진창의 굳은 성채입니다. 성문을 무리하게 깨기보다 공성 진영, 군량 수레, 퇴로를 함께 보아야 합니다.' }, @@ -3761,7 +3761,7 @@ export const fiftyNinthBattleIntroPages: StoryPage[] = [ id: 'fifty-ninth-hao-zhao-wall', bgm: 'battle-prep', chapter: '학소의 성벽', - background: 'story-northern', + background: 'story-chencang-siege', speaker: '강유', text: '진창의 수비장 학소는 성을 버리지 않을 사람입니다. 제가 앞길을 살피겠습니다만, 성벽만 바라보면 왕쌍의 기병이 뒤를 물 것입니다. 조운, 마대, 왕평 중 누구를 앞으로 세우느냐가 이번 편성의 무게입니다.' }, @@ -3769,7 +3769,7 @@ export const fiftyNinthBattleIntroPages: StoryPage[] = [ id: 'fifty-ninth-supply-limit', bgm: 'story-dark', chapter: '군량의 시간', - background: 'story-northern', + background: 'story-chencang-siege', speaker: '황권', text: '성채 하나에 오래 묶이면 북벌 전체가 굶습니다. 법정의 계책, 마량의 문서, 황권의 장부가 함께 있어야 공성전이 단순한 힘겨루기로 흐르지 않습니다. 진창을 압박하되 돌아갈 수 있는 시간을 남기십시오.' } @@ -3780,14 +3780,14 @@ export const fiftyNinthBattleVictoryPages: StoryPage[] = [ id: 'fifty-ninth-victory-chencang-pressure', bgm: 'militia-theme', chapter: '진창 압박', - background: 'story-northern', + background: 'story-chencang-siege', text: '촉한군은 진창의 외곽 진영과 보급로를 흔들었지만, 학소의 성벽은 끝내 쉽게 열리지 않았습니다. 승리라 부르기에는 조심스럽고 패배라 덮기에는 얻은 길이 분명한 전투였습니다.' }, { id: 'fifty-ninth-victory-wang-shuang-cut', bgm: 'story-dark', chapter: '왕쌍 추격 차단', - background: 'story-northern', + background: 'story-chencang-siege', speaker: '조운', text: '왕쌍의 기병은 공성 진영 뒤를 물려 했으나, 강유의 길 안내와 마대의 우회, 왕평의 진영 표식이 먼저 움직였습니다. 오래 버티는 성을 상대로도 물러날 때를 정하는 장수가 필요합니다.' }, @@ -3795,7 +3795,7 @@ export const fiftyNinthBattleVictoryPages: StoryPage[] = [ id: 'fifty-ninth-next-northern-plan', bgm: 'story-dark', chapter: '다음 북방 계산', - background: 'story-northern', + background: 'story-chencang-siege', speaker: '제갈량', text: '진창은 북벌의 끝이 아니라, 무리한 공세가 어디서 멈춰야 하는지를 보여 준 성입니다. 다음 출정에서는 성을 치는 힘뿐 아니라 군량이 버틸 날, 돌아올 길, 장수의 고집을 모두 함께 계산해야 합니다.' } @@ -3806,7 +3806,7 @@ export const sixtiethBattleIntroPages: StoryPage[] = [ id: 'sixtieth-wudu-yinping-order', bgm: 'story-dark', chapter: '무도와 음평', - background: 'story-northern', + background: 'story-wudu-yinping', speaker: '제갈량', text: '진창의 성벽은 오래 물고 늘어질 곳이 아니었습니다. 이번에는 성을 치기보다 무도와 음평의 산길을 나누어 잡아, 북방의 두 고을을 촉한의 숨통으로 만들겠습니다.' }, @@ -3814,7 +3814,7 @@ export const sixtiethBattleIntroPages: StoryPage[] = [ id: 'sixtieth-guo-huai-relief', bgm: 'battle-prep', chapter: '곽회의 구원로', - background: 'story-northern', + background: 'story-wudu-yinping', speaker: '강유', text: '곽회는 성을 지키는 장수가 아니라 길을 끊는 장수입니다. 제가 아는 천수 길로 앞을 보겠습니다. 다만 위연 장군의 돌파, 조운과 마대의 기병, 왕평의 산길 판단을 모두 한꺼번에 쓸 수는 없습니다.' }, @@ -3822,7 +3822,7 @@ export const sixtiethBattleIntroPages: StoryPage[] = [ id: 'sixtieth-two-commandery-supply', bgm: 'story-dark', chapter: '둘로 갈라진 보급', - background: 'story-northern', + background: 'story-wudu-yinping', speaker: '황권', text: '무도와 음평을 동시에 안정시키려면 장부가 둘로 갈라집니다. 마량이 마을을 달래고, 왕평이 산길 표식을 세우며, 누가 빠르게 치고 누가 멈출지 출전 전에 정해야 합니다.' } @@ -3833,14 +3833,14 @@ export const sixtiethBattleVictoryPages: StoryPage[] = [ id: 'sixtieth-victory-commanderies-secured', bgm: 'militia-theme', chapter: '두 고을 확보', - background: 'story-northern', + background: 'story-wudu-yinping', text: '촉한군은 무도와 음평의 길목을 차례로 눌러 곽회의 구원로를 늦췄습니다. 큰 성 하나를 무너뜨린 승리가 아니라, 북벌이 다시 숨 쉴 수 있는 산길을 확보한 승리였습니다.' }, { id: 'sixtieth-victory-jiangwei-weiyan-choice', bgm: 'story-dark', chapter: '돌파 뒤의 지도', - background: 'story-northern', + background: 'story-wudu-yinping', speaker: '제갈량', text: '강유는 길을 읽었고 위연은 길이 열릴 때 망설이지 않았습니다. 하지만 둘이 함께 앞서갈수록 조운과 마대의 회수선, 왕평과 황권의 제동 장치가 더 중요해졌습니다.' }, @@ -3848,7 +3848,7 @@ export const sixtiethBattleVictoryPages: StoryPage[] = [ id: 'sixtieth-next-northern-plan', bgm: 'story-dark', chapter: '다음 북방 계산', - background: 'story-northern', + background: 'story-wudu-yinping', speaker: '마량', text: '무도와 음평의 백성에게는 오늘의 승리가 끝이 아니라 약속의 시작입니다. 다음 북벌에서는 얻은 땅을 지키는 장부와 다시 나아갈 장수의 마음을 함께 보아야 합니다.' } @@ -3859,7 +3859,7 @@ export const sixtyFirstBattleIntroPages: StoryPage[] = [ id: 'sixty-first-hanzhong-rain-warning', bgm: 'story-dark', chapter: '한중의 빗길', - background: 'story-northern', + background: 'story-hanzhong-rain', speaker: '황권', text: '무도와 음평을 얻은 뒤부터 한중의 장부는 더 두꺼워졌습니다. 조진이 북문으로 내려오고, 사마의가 빗길을 따라 별동대를 움직이면 새 고을은 승리의 이름이 아니라 지켜야 할 짐이 됩니다.' }, @@ -3867,7 +3867,7 @@ export const sixtyFirstBattleIntroPages: StoryPage[] = [ id: 'sixty-first-cao-zhen-counteroffensive', bgm: 'battle-prep', chapter: '조진의 역공', - background: 'story-northern', + background: 'story-hanzhong-rain', speaker: '제갈량', text: '이번에는 성을 치지 않습니다. 한중 북문의 창고, 무도와 음평으로 이어지는 수레 길, 그리고 비에 무너지는 고개를 함께 지켜야 합니다. 위연의 돌파와 강유의 길눈, 이엄과 황권의 보급 장부 중 무엇을 앞세울지 정하십시오.' }, @@ -3875,7 +3875,7 @@ export const sixtyFirstBattleIntroPages: StoryPage[] = [ id: 'sixty-first-two-commandery-burden', bgm: 'story-dark', chapter: '넓어진 책임', - background: 'story-northern', + background: 'story-hanzhong-rain', speaker: '마량', text: '백성은 촉한의 깃발만 보고 안심하지 않습니다. 비가 오면 수레는 늦고, 군문이 흔들리면 새 고을은 먼저 두려워합니다. 출전 명단에서 누가 길을 잡고 누가 장부를 지킬지 분명히 해야 합니다.' } @@ -3886,14 +3886,14 @@ export const sixtyFirstBattleVictoryPages: StoryPage[] = [ id: 'sixty-first-victory-rain-defense', bgm: 'militia-theme', chapter: '빗길을 막다', - background: 'story-northern', + background: 'story-hanzhong-rain', text: '촉한군은 한중 북문의 창고를 비워 내지 않고 지켜 냈고, 무도와 음평으로 향하는 수레 길도 끊기지 않게 붙들었습니다. 조진의 역공은 빗길과 산길 사이에서 힘을 잃었습니다.' }, { id: 'sixty-first-victory-supply-responsibility', bgm: 'story-dark', chapter: '장부의 무게', - background: 'story-northern', + background: 'story-hanzhong-rain', speaker: '이엄', text: '고을을 얻는 일보다 고을을 먹여 살리는 일이 더 오래 갑니다. 황권의 장부와 마량의 문서가 늦어지면, 강유와 위연이 아무리 앞에서 길을 열어도 뒤의 비명은 전선으로 돌아옵니다.' }, @@ -3901,7 +3901,7 @@ export const sixtyFirstBattleVictoryPages: StoryPage[] = [ id: 'sixty-first-next-northern-plan', bgm: 'story-dark', chapter: '다시 북쪽을 보다', - background: 'story-northern', + background: 'story-hanzhong-rain', speaker: '강유', text: '조진을 막아 냈지만 위의 북방은 한 번 물러난 것으로 끝나지 않습니다. 다음 출정에서는 지켜 낸 길을 다시 공격의 길로 바꿀 수 있는지, 그리고 그때 누구를 뒤에 남길지까지 따져야 합니다.' } diff --git a/src/game/data/storyAssets.ts b/src/game/data/storyAssets.ts new file mode 100644 index 0000000..41c45f3 --- /dev/null +++ b/src/game/data/storyAssets.ts @@ -0,0 +1,46 @@ +import storyChaosUrl from '../../assets/images/story/01-yellow-turban-chaos.png'; +import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png'; +import storyThreeHeroesUrl from '../../assets/images/story/03-three-heroes-meet.png'; +import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png'; +import storySortieUrl from '../../assets/images/story/05-first-sortie.png'; +import storyYellowPursuitUrl from '../../assets/images/story/06-yellow-turban-pursuit.png'; +import storyAntiDongPassUrl from '../../assets/images/story/07-anti-dong-pass.png'; +import storyXuzhouHandoverUrl from '../../assets/images/story/08-xuzhou-handover.png'; +import storyWanderingRoadUrl from '../../assets/images/story/09-wandering-road.png'; +import storyWolongCottageUrl from '../../assets/images/story/10-wolong-cottage.png'; +import storyRedCliffsFireUrl from '../../assets/images/story/11-red-cliffs-fire.png'; +import storyYizhouMountainPassUrl from '../../assets/images/story/12-yizhou-mountain-pass.png'; +import storyNorthernExpeditionUrl from '../../assets/images/story/13-northern-expedition.png'; +import storyNanzhongPacificationUrl from '../../assets/images/story/14-nanzhong-pacification.png'; +import storyYilingBaidiAftermathUrl from '../../assets/images/story/15-yiling-baidi-aftermath.png'; +import storyQishanRenewedOffensiveUrl from '../../assets/images/story/16-qishan-renewed-offensive.png'; +import storyTianshuiFrontUrl from '../../assets/images/story/17-tianshui-front.png'; +import storyJietingCrisisUrl from '../../assets/images/story/18-jieting-crisis.png'; +import storyChencangSiegeUrl from '../../assets/images/story/19-chencang-siege.png'; +import storyWuduYinpingUrl from '../../assets/images/story/20-wudu-yinping.png'; +import storyHanzhongRainDefenseUrl from '../../assets/images/story/21-hanzhong-rain-defense.png'; + +export const storyBackgroundAssets: Record = { + 'story-chaos': storyChaosUrl, + 'story-liu-bei': storyLiuBeiUrl, + 'story-three-heroes': storyThreeHeroesUrl, + 'story-militia': storyMilitiaUrl, + 'story-sortie': storySortieUrl, + 'story-yellow-pursuit': storyYellowPursuitUrl, + 'story-anti-dong': storyAntiDongPassUrl, + 'story-xuzhou': storyXuzhouHandoverUrl, + 'story-wandering': storyWanderingRoadUrl, + 'story-wolong': storyWolongCottageUrl, + 'story-red-cliffs': storyRedCliffsFireUrl, + 'story-yizhou': storyYizhouMountainPassUrl, + 'story-northern': storyNorthernExpeditionUrl, + 'story-nanzhong': storyNanzhongPacificationUrl, + 'story-yiling-baidi': storyYilingBaidiAftermathUrl, + 'story-qishan-renewed': storyQishanRenewedOffensiveUrl, + 'story-tianshui-front': storyTianshuiFrontUrl, + 'story-jieting-crisis': storyJietingCrisisUrl, + 'story-chencang-siege': storyChencangSiegeUrl, + 'story-wudu-yinping': storyWuduYinpingUrl, + 'story-hanzhong-rain': storyHanzhongRainDefenseUrl, + 'story-resolve': storyLiuBeiUrl +}; diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 6fb90bd..88dab36 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -2,23 +2,6 @@ import Phaser from 'phaser'; import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png'; import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png'; -import { battleMapAssets } from '../data/battleMapAssets'; -import storyChaosUrl from '../../assets/images/story/01-yellow-turban-chaos.png'; -import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png'; -import storyThreeHeroesUrl from '../../assets/images/story/03-three-heroes-meet.png'; -import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png'; -import storySortieUrl from '../../assets/images/story/05-first-sortie.png'; -import storyYellowPursuitUrl from '../../assets/images/story/06-yellow-turban-pursuit.png'; -import storyAntiDongPassUrl from '../../assets/images/story/07-anti-dong-pass.png'; -import storyXuzhouHandoverUrl from '../../assets/images/story/08-xuzhou-handover.png'; -import storyWanderingRoadUrl from '../../assets/images/story/09-wandering-road.png'; -import storyWolongCottageUrl from '../../assets/images/story/10-wolong-cottage.png'; -import storyRedCliffsFireUrl from '../../assets/images/story/11-red-cliffs-fire.png'; -import storyYizhouMountainPassUrl from '../../assets/images/story/12-yizhou-mountain-pass.png'; -import storyNorthernExpeditionUrl from '../../assets/images/story/13-northern-expedition.png'; -import storyNanzhongPacificationUrl from '../../assets/images/story/14-nanzhong-pacification.png'; -import storyYilingBaidiAftermathUrl from '../../assets/images/story/15-yiling-baidi-aftermath.png'; -import storyQishanRenewedOffensiveUrl from '../../assets/images/story/16-qishan-renewed-offensive.png'; import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png'; import guanYuActionSheetUrl from '../../assets/images/units/unit-guan-yu-actions.png'; import guanYuUnitSheetUrl from '../../assets/images/units/unit-guan-yu.png'; @@ -34,6 +17,7 @@ import rebelActionSheetUrl from '../../assets/images/units/unit-rebel-actions.pn import rebelUnitSheetUrl from '../../assets/images/units/unit-rebel.png'; import zhangFeiActionSheetUrl from '../../assets/images/units/unit-zhang-fei-actions.png'; import zhangFeiUnitSheetUrl from '../../assets/images/units/unit-zhang-fei.png'; +import { storyBackgroundAssets } from '../data/storyAssets'; type UnitDirection = 'south' | 'east' | 'north' | 'west'; @@ -89,31 +73,11 @@ export class BootScene extends Phaser.Scene { preload() { this.load.image('title-taoyuan', titleBackgroundUrl); - this.load.image('story-chaos', storyChaosUrl); - this.load.image('story-liu-bei', storyLiuBeiUrl); - this.load.image('story-three-heroes', storyThreeHeroesUrl); - this.load.image('story-militia', storyMilitiaUrl); - this.load.image('story-sortie', storySortieUrl); - this.load.image('story-yellow-pursuit', storyYellowPursuitUrl); - this.load.image('story-anti-dong', storyAntiDongPassUrl); - this.load.image('story-xuzhou', storyXuzhouHandoverUrl); - this.load.image('story-wandering', storyWanderingRoadUrl); - this.load.image('story-wolong', storyWolongCottageUrl); - this.load.image('story-red-cliffs', storyRedCliffsFireUrl); - this.load.image('story-yizhou', storyYizhouMountainPassUrl); - this.load.image('story-northern', storyNorthernExpeditionUrl); - this.load.image('story-nanzhong', storyNanzhongPacificationUrl); - this.load.image('story-yiling-baidi', storyYilingBaidiAftermathUrl); - this.load.image('story-qishan-renewed', storyQishanRenewedOffensiveUrl); - this.load.image('story-resolve', storyLiuBeiUrl); + this.load.image('story-militia', storyBackgroundAssets['story-militia']); this.load.image('portrait-liu-bei', liuBeiPortraitUrl); this.load.image('portrait-guan-yu', guanYuPortraitUrl); this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl); - Object.entries(battleMapAssets).forEach(([key, url]) => { - this.load.image(key, url); - }); - sourceUnitSheets.forEach(({ key, url, actionKey, actionUrl }) => { this.load.spritesheet(key, url, { frameWidth: unitSheetFrameSize, diff --git a/src/game/scenes/StoryScene.ts b/src/game/scenes/StoryScene.ts index 2643d01..cd73a7e 100644 --- a/src/game/scenes/StoryScene.ts +++ b/src/game/scenes/StoryScene.ts @@ -1,5 +1,6 @@ import Phaser from 'phaser'; import { soundDirector } from '../audio/SoundDirector'; +import { storyBackgroundAssets } from '../data/storyAssets'; import { prologuePages, type PortraitKey, type StoryPage } from '../data/scenario'; import { palette } from '../ui/palette'; @@ -31,6 +32,7 @@ export class StoryScene extends Phaser.Scene { private nameText?: Phaser.GameObjects.Text; private bodyText?: Phaser.GameObjects.Text; private progressText?: Phaser.GameObjects.Text; + private loadingText?: Phaser.GameObjects.Text; constructor() { super('StoryScene'); @@ -44,17 +46,86 @@ export class StoryScene extends Phaser.Scene { this.transitioning = false; } + getDebugState() { + const page = this.pages[this.pageIndex]; + + return { + scene: this.scene.key, + ready: this.loadingText === undefined && this.background !== undefined, + pageIndex: this.pageIndex, + totalPages: this.pages.length, + currentPageId: page?.id, + background: page?.background, + backgroundReady: page ? this.textures.exists(page.background) : false, + activeTexture: this.background?.texture.key ?? null + }; + } + create() { const { width, height } = this.scale; this.add.rectangle(0, 0, width, height, 0x0b0d12).setOrigin(0); - this.background = this.add.image(width / 2, height / 2, this.pages[0].background); - this.drawSceneShade(width, height); - this.drawDialogPanel(width, height); - this.showPage(0, true); + this.createFallbackStoryTexture(); + this.loadingText = this.add.text(width / 2, height / 2, '장면을 준비하는 중...', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '22px', + color: '#f5e6b8', + stroke: '#05070a', + strokeThickness: 4 + }); + this.loadingText.setOrigin(0.5); - this.input.on('pointerdown', () => this.advance()); - this.input.keyboard?.on('keydown-SPACE', () => this.advance()); - this.input.keyboard?.on('keydown-ENTER', () => this.advance()); + this.ensureStoryBackgroundsLoaded(() => { + this.loadingText?.destroy(); + this.loadingText = undefined; + this.background = this.add.image(width / 2, height / 2, this.resolveBackgroundKey(this.pages[0].background)); + this.drawSceneShade(width, height); + this.drawDialogPanel(width, height); + this.showPage(0, true); + + this.input.on('pointerdown', () => this.advance()); + this.input.keyboard?.on('keydown-SPACE', () => this.advance()); + this.input.keyboard?.on('keydown-ENTER', () => this.advance()); + }); + } + + private ensureStoryBackgroundsLoaded(onReady: () => void) { + const missingKeys = Array.from(new Set(this.pages.map((page) => page.background))).filter((key) => !this.textures.exists(key)); + const loadableKeys = missingKeys.filter((key) => storyBackgroundAssets[key]); + + missingKeys + .filter((key) => !storyBackgroundAssets[key]) + .forEach((key) => console.warn(`Missing story background asset for ${key}`)); + + if (loadableKeys.length === 0) { + onReady(); + return; + } + + this.load.once('complete', onReady); + this.load.once('loaderror', (file: Phaser.Loader.File) => { + console.warn(`Failed to load story background ${file.key}`); + }); + loadableKeys.forEach((key) => this.load.image(key, storyBackgroundAssets[key])); + this.load.start(); + } + + private createFallbackStoryTexture() { + if (this.textures.exists('story-fallback')) { + return; + } + + const graphics = this.make.graphics({ x: 0, y: 0 }); + graphics.fillStyle(0x10131a, 1); + graphics.fillRect(0, 0, 1280, 720); + graphics.fillStyle(0x1d2530, 1); + graphics.fillRect(0, 0, 1280, 180); + graphics.lineStyle(4, palette.gold, 0.5); + graphics.beginPath(); + graphics.moveTo(120, 600); + graphics.lineTo(1160, 120); + graphics.strokePath(); + graphics.generateTexture('story-fallback', 1280, 720); + graphics.destroy(); } private drawSceneShade(width: number, height: number) { @@ -197,13 +268,14 @@ export class StoryScene extends Phaser.Scene { } const { width, height } = this.scale; - const source = this.textures.get(textureKey).getSourceImage(); + const safeTextureKey = this.resolveBackgroundKey(textureKey); + const source = this.textures.get(safeTextureKey).getSourceImage(); const imageWidth = source.width; const imageHeight = source.height; const coverScale = Math.max(width / imageWidth, height / imageHeight); this.tweens.killTweensOf(this.background); - this.background.setTexture(textureKey); + this.background.setTexture(safeTextureKey); this.background.setAlpha(1); this.background.setScale(coverScale * 1.07); this.background.setPosition(width / 2 - 16, height / 2); @@ -220,6 +292,10 @@ export class StoryScene extends Phaser.Scene { }); } + private resolveBackgroundKey(textureKey: string) { + return this.textures.exists(textureKey) ? textureKey : 'story-fallback'; + } + private advance() { if (this.transitioning) { return;