From d064964c27992fa8469d49698eb15fc7d3f9e93f Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 4 Jul 2026 03:34:07 +0900 Subject: [PATCH] Stabilize early campaign QA flow --- scripts/verify-release-candidate.mjs | 59 ++++++++++++++++++++++++--- src/game/scenes/BattleScene.ts | 61 ++++++++++++++++++---------- 2 files changed, 94 insertions(+), 26 deletions(-) diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index 34a1850..88e137d 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -52,10 +52,11 @@ try { assert(firstBattleProbe.battleId === 'first-battle-zhuo-commandery', `Expected first battle: ${JSON.stringify(firstBattleProbe)}`); assert(firstBattleProbe.objectiveText?.startsWith('승리 목표:'), `Expected clear victory objective label: ${JSON.stringify(firstBattleProbe)}`); assert(firstBattleProbe.objectiveSubText?.includes('패배 조건:'), `Expected clear defeat condition label: ${JSON.stringify(firstBattleProbe)}`); + assert(firstBattleProbe.objectiveSubText?.includes('진군:'), `Expected tactical route in objective tracker: ${JSON.stringify(firstBattleProbe)}`); assert( - firstBattleProbe.sideTexts.some((text) => text.includes('황건적의 전열이 무너집니다.')) && + firstBattleProbe.sideTexts.some((text) => text.includes('작전 목표')) && !firstBattleProbe.sideTexts.some((text) => text.includes('...')), - `Expected opening objective text without ASCII truncation: ${JSON.stringify(firstBattleProbe.sideTexts)}` + `Expected opening tactical message without ASCII truncation: ${JSON.stringify(firstBattleProbe.sideTexts)}` ); await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -80,7 +81,7 @@ try { assert(!resultProbe.resultTexts.some((text) => text.includes('미달')), `Expected result screen to avoid harsh optional-goal wording: ${JSON.stringify(resultProbe.resultTexts)}`); await page.mouse.click(738, 642); - await waitForCamp(page); + await waitForCampAfterBattleResult(page); await page.screenshot({ path: `${screenshotDir}/rc-first-camp.png`, fullPage: true }); const firstCampProbe = await page.evaluate(() => { @@ -211,10 +212,16 @@ async function advanceUntilBattle(page, battleId) { for (let i = 0; i < 48; i += 1) { const ready = await page.evaluate((expectedBattleId) => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === expectedBattleId && state?.phase === 'idle' && state?.mapBackgroundReady === true; + return ( + state?.scene === 'BattleScene' && + state?.battleId === expectedBattleId && + (state?.phase === 'deployment' || state?.phase === 'idle') && + state?.mapBackgroundReady === true + ); }, battleId); if (ready) { + await startDeploymentIfNeeded(page, battleId); return; } @@ -228,8 +235,31 @@ async function advanceUntilBattle(page, battleId) { async function waitForBattleReady(page, battleId) { await page.waitForFunction((expectedBattleId) => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === expectedBattleId && state?.phase === 'idle' && state?.mapBackgroundReady === true; + return ( + state?.scene === 'BattleScene' && + state?.battleId === expectedBattleId && + (state?.phase === 'deployment' || state?.phase === 'idle') && + state?.mapBackgroundReady === true + ); }, battleId, { timeout: 90000 }); + await startDeploymentIfNeeded(page, battleId); +} + +async function startDeploymentIfNeeded(page, battleId) { + const state = await page.evaluate((expectedBattleId) => { + const battle = window.__HEROS_DEBUG__?.battle(); + return battle?.battleId === expectedBattleId ? battle : undefined; + }, battleId); + if (state?.phase !== 'deployment') { + return; + } + + assert(state.deployedAllyIds?.length >= 3, `Expected deployment to show the selected ally formation: ${JSON.stringify(state)}`); + await page.mouse.click(1085, 637); + await page.waitForFunction((expectedBattleId) => { + const battle = window.__HEROS_DEBUG__?.battle(); + return battle?.battleId === expectedBattleId && battle?.phase === 'idle' && battle?.triggeredBattleEvents?.includes('opening'); + }, battleId, { timeout: 30000 }); } async function waitForBattleOutcome(page, outcome) { @@ -251,6 +281,25 @@ async function waitForCamp(page) { }, undefined, { timeout: 90000 }); } +async function waitForCampAfterBattleResult(page) { + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene') || activeScenes.includes('StoryScene'); + }, undefined, { timeout: 90000 }); + + for (let i = 0; i < 48; i += 1) { + const activeScenes = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []); + if (activeScenes.includes('CampScene')) { + await waitForCamp(page); + return; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(240); + } + + await waitForCamp(page); +} + async function advanceStoryUntilEnding(page) { for (let i = 0; i < 48; i += 1) { const activeScenes = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []); diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 74b57b0..7cb3e8f 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -4072,16 +4072,17 @@ export class BattleScene extends Phaser.Scene { } const visible = this.isTileVisible(unit.x, unit.y); - view.sprite.postFX.clear(); - view.sprite.clearTint(); + this.clearUnitSpriteEffects(view.sprite); view.sprite.setAlpha(1); view.sprite.setVisible(visible); this.setUnitHitZoneEnabled(view, visible); - view.label.setVisible(visible); - view.label.setAlpha(1); - view.label.setColor(unit.faction === 'ally' ? '#e7edf7' : '#ffebe7'); - view.label.setBackgroundColor(''); + this.updateUnitLabel(view, (label) => { + label.setVisible(visible); + label.setAlpha(1); + label.setColor(unit.faction === 'ally' ? '#e7edf7' : '#ffebe7'); + label.setBackgroundColor(''); + }); } private isTileVisible(x: number, y: number) { @@ -7549,7 +7550,7 @@ export class BattleScene extends Phaser.Scene { } private truncateUiText(text: string, maxLength: number) { - return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text; + return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text; } private previewActionLabel(preview: CombatPreview) { @@ -13640,8 +13641,7 @@ export class BattleScene extends Phaser.Scene { this.applyDefeatedStyle(unit); return; } - view.sprite.postFX.clear(); - view.sprite.clearTint(); + this.clearUnitSpriteEffects(view.sprite); view.sprite.setAlpha(1); if (unit) { this.syncUnitMotion(unit, view); @@ -13649,9 +13649,11 @@ export class BattleScene extends Phaser.Scene { } else { this.stopUnitWalk(view, view.direction); } - view.label.setAlpha(1); - view.label.setColor(unit?.faction === 'ally' ? '#e7edf7' : '#ffebe7'); - view.label.setBackgroundColor(''); + this.updateUnitLabel(view, (label) => { + label.setAlpha(1); + label.setColor(unit?.faction === 'ally' ? '#e7edf7' : '#ffebe7'); + label.setBackgroundColor(''); + }); }); } @@ -14657,6 +14659,23 @@ export class BattleScene extends Phaser.Scene { sprite.setBlendMode(Phaser.BlendModes.NORMAL); } + private clearUnitSpriteEffects(sprite: Phaser.GameObjects.Sprite) { + sprite.postFX?.clear(); + sprite.clearTint(); + } + + private updateUnitLabel(view: UnitView, update: (label: Phaser.GameObjects.Text) => void) { + if (!view.label.active || view.label.scene !== this) { + return; + } + + try { + update(view.label); + } catch { + // Canvas text internals can already be disposed during fast scene transitions. + } + } + private playUnitActionFrames( sprite: Phaser.GameObjects.Sprite, unit: UnitData, @@ -14700,8 +14719,7 @@ export class BattleScene extends Phaser.Scene { return; } - view.sprite.postFX.clear(); - view.sprite.clearTint(); + this.clearUnitSpriteEffects(view.sprite); this.syncUnitMotion(unit, view); this.applyUnitLegibilityStyle(unit, view); } @@ -14715,15 +14733,16 @@ export class BattleScene extends Phaser.Scene { view.sprite.stop(); view.sprite.disableInteractive(); - view.sprite.postFX.clear(); - view.sprite.clearTint(); + this.clearUnitSpriteEffects(view.sprite); view.sprite.setAlpha(0); view.sprite.setVisible(false); this.setUnitHitZoneEnabled(view, false); - view.label.setAlpha(0); - view.label.setVisible(false); - view.label.setColor('#b6b6b6'); - view.label.setBackgroundColor(''); + this.updateUnitLabel(view, (label) => { + label.setAlpha(0); + label.setVisible(false); + label.setColor('#b6b6b6'); + label.setBackgroundColor(''); + }); } private faceUnitToward(unit: UnitData, target: UnitData) { @@ -15008,7 +15027,7 @@ export class BattleScene extends Phaser.Scene { } private truncateBattleLogText(text: string, maxLength: number) { - return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text; + return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text; } private battleLogDisplayParts(entry: string) {