feat: add spoiler-safe campaign objective journal
This commit is contained in:
@@ -1348,7 +1348,7 @@ async function setupBattle(page, battle) {
|
||||
return (
|
||||
state?.scene === 'BattleScene' &&
|
||||
state?.battleId === battleId &&
|
||||
(state?.phase === 'deployment' || state?.phase === 'idle') &&
|
||||
state?.phase === 'deployment' &&
|
||||
state?.mapBackgroundReady === true
|
||||
);
|
||||
},
|
||||
@@ -1536,7 +1536,7 @@ async function setupCumulativeBattle(page, battle, firstBattle, route) {
|
||||
return (
|
||||
state?.scene === 'BattleScene' &&
|
||||
state?.battleId === battleId &&
|
||||
(state?.phase === 'deployment' || state?.phase === 'idle') &&
|
||||
state?.phase === 'deployment' &&
|
||||
state?.mapBackgroundReady === true
|
||||
);
|
||||
},
|
||||
@@ -1649,7 +1649,9 @@ async function startDeploymentIfNeeded(page, battleId) {
|
||||
}, battleId);
|
||||
|
||||
if (state?.phase !== 'deployment') {
|
||||
return;
|
||||
throw new Error(
|
||||
`Expected ${battleId} to expose deployment before battle start: ${JSON.stringify(state)}`
|
||||
);
|
||||
}
|
||||
|
||||
await clickBattleDeploymentStart(page);
|
||||
@@ -1844,6 +1846,20 @@ async function enterCampaignCampFromBattleResult(page, battle, route) {
|
||||
settlement = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.resultSettlement);
|
||||
}
|
||||
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const resultSettlement = window.__HEROS_DEBUG__?.battle()?.resultSettlement;
|
||||
return (
|
||||
resultSettlement?.status === 'complete' &&
|
||||
resultSettlement?.cta?.interactive === true &&
|
||||
resultSettlement?.cta?.bounds
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
settlement = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.resultSettlement);
|
||||
|
||||
const expectedDestination = 'victory-story';
|
||||
if (settlement?.cta?.destination !== expectedDestination) {
|
||||
throw new Error(
|
||||
@@ -1915,6 +1931,21 @@ async function enterFinalCampFromBattleResult(page) {
|
||||
);
|
||||
settlement = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.resultSettlement);
|
||||
}
|
||||
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const resultSettlement = window.__HEROS_DEBUG__?.battle()?.resultSettlement;
|
||||
return (
|
||||
resultSettlement?.status === 'complete' &&
|
||||
resultSettlement?.cta?.interactive === true &&
|
||||
resultSettlement?.cta?.bounds
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
settlement = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.resultSettlement);
|
||||
|
||||
if (settlement?.cta?.destination !== 'victory-story') {
|
||||
throw new Error(`Final victory must continue through its aftermath before the final camp: ${JSON.stringify(settlement)}`);
|
||||
}
|
||||
|
||||
1540
scripts/verify-campaign-objective-journal-browser.mjs
Normal file
1540
scripts/verify-campaign-objective-journal-browser.mjs
Normal file
File diff suppressed because it is too large
Load Diff
619
scripts/verify-campaign-objective-journal.mjs
Normal file
619
scripts/verify-campaign-objective-journal.mjs
Normal file
@@ -0,0 +1,619 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createServer } from 'vite';
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
server: { middlewareMode: true, hmr: false },
|
||||
appType: 'custom'
|
||||
});
|
||||
|
||||
try {
|
||||
const journalModule = await server.ssrLoadModule(
|
||||
'/src/game/state/campaignObjectiveJournal.ts'
|
||||
);
|
||||
const campaignModule = await server.ssrLoadModule(
|
||||
'/src/game/state/campaignState.ts'
|
||||
);
|
||||
const routingModule = await server.ssrLoadModule(
|
||||
'/src/game/state/campaignRouting.ts'
|
||||
);
|
||||
const { battleScenarios } = await server.ssrLoadModule(
|
||||
'/src/game/data/battles.ts'
|
||||
);
|
||||
const { getSortieFlow } = await server.ssrLoadModule(
|
||||
'/src/game/data/campaignFlow.ts'
|
||||
);
|
||||
const secondReliefModule = await server.ssrLoadModule(
|
||||
'/src/game/data/secondBattleReliefExploration.ts'
|
||||
);
|
||||
const thirdExplorationModule = await server.ssrLoadModule(
|
||||
'/src/game/data/thirdCampExploration.ts'
|
||||
);
|
||||
|
||||
const resolve = journalModule.resolveCampaignObjectiveJournal;
|
||||
const source = await readFile(
|
||||
new URL(
|
||||
'../src/game/state/campaignObjectiveJournal.ts',
|
||||
import.meta.url
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
verifyPureModuleContract(journalModule, source);
|
||||
verifyInitialAndStoryStates(resolve, campaignModule);
|
||||
verifyPrologueVillageProgress(resolve, campaignModule);
|
||||
verifyPrologueCampProgress(resolve, campaignModule);
|
||||
verifyAftermathIsolation(
|
||||
resolve,
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
getSortieFlow
|
||||
);
|
||||
verifyCityStayIsolation(
|
||||
resolve,
|
||||
campaignModule,
|
||||
battleScenarios
|
||||
);
|
||||
verifyEarlyInterludes(
|
||||
resolve,
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
secondReliefModule,
|
||||
thirdExplorationModule
|
||||
);
|
||||
verifyAllBattleAndCampRoutesHideFutureDetails(
|
||||
resolve,
|
||||
campaignModule,
|
||||
routingModule,
|
||||
battleScenarios,
|
||||
getSortieFlow
|
||||
);
|
||||
|
||||
console.log(
|
||||
'Campaign objective journal verification passed (current/completed/clue triplet, known/open progress, short resume context, early exploration sequencing, and future battle/title/condition isolation).'
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
function verifyPureModuleContract(journalModule, source) {
|
||||
assert.deepEqual(journalModule.campaignObjectiveJournalStatusLabels, {
|
||||
current: '◆ 진행',
|
||||
completed: '✓ 완료',
|
||||
clue: '? 실마리'
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/(?:from|import\s+type)\s+['"]phaser['"]/i,
|
||||
'The objective journal must remain Phaser-independent.'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/\.(?:victoryConditionLabel|defeatConditionLabel|openingObjectiveLines|objectives|units|leaderUnitId)\b/,
|
||||
'The journal must not read battle win conditions, future units, or hidden tactical data.'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/getSortieFlow\([^)]*\)\.(?:title|description|rewardHint)/,
|
||||
'Campaign flow prose must not be copied into the spoiler-safe journal.'
|
||||
);
|
||||
}
|
||||
|
||||
function verifyInitialAndStoryStates(resolve, campaignModule) {
|
||||
const initial = campaignModule.createInitialCampaignState();
|
||||
const initialSnapshot = resolve(initial);
|
||||
assertSnapshotShape(initialSnapshot);
|
||||
assert.equal(initialSnapshot.contextKind, 'not-started');
|
||||
assert.equal(initialSnapshot.current.statusLabel, '◆ 진행');
|
||||
assert.equal(initialSnapshot.recentCompleted.statusLabel, '✓ 완료');
|
||||
assert.equal(initialSnapshot.nextClue.statusLabel, '? 실마리');
|
||||
assert.deepEqual(
|
||||
pickProgress(initialSnapshot.progress),
|
||||
{
|
||||
completed: 0,
|
||||
total: 1,
|
||||
knownTotal: true
|
||||
}
|
||||
);
|
||||
|
||||
const prologue = freshState(campaignModule, { step: 'prologue' });
|
||||
const prologueSnapshot = resolve(prologue);
|
||||
assert.equal(prologueSnapshot.contextKind, 'story');
|
||||
assert.match(prologueSnapshot.current.title, /서막/);
|
||||
assert.equal(prologueSnapshot.resumeContext.lines.length, 3);
|
||||
assert(
|
||||
prologueSnapshot.resumeContext.lines.every(
|
||||
(line) => line.length <= 110
|
||||
),
|
||||
'Resume context lines must stay short enough for a compact recap card.'
|
||||
);
|
||||
}
|
||||
|
||||
function verifyPrologueVillageProgress(resolve, campaignModule) {
|
||||
const ids = campaignModule.prologueVillageCampaignTutorialIds;
|
||||
const village = freshState(campaignModule, {
|
||||
step: 'prologue-town'
|
||||
});
|
||||
|
||||
let snapshot = resolve(village);
|
||||
assert.equal(snapshot.contextKind, 'village-exploration');
|
||||
assert.equal(snapshot.current.title, '격문 앞의 장비');
|
||||
assert.match(snapshot.nextClue.detail, /서쪽 모병 격문/);
|
||||
assert.deepEqual(pickProgress(snapshot.progress), {
|
||||
completed: 0,
|
||||
total: 4,
|
||||
knownTotal: true
|
||||
});
|
||||
|
||||
village.completedTutorialIds.push(ids.meetZhangFei);
|
||||
snapshot = resolve(village);
|
||||
assert.equal(snapshot.current.title, '주점의 관우');
|
||||
assert.equal(snapshot.recentCompleted.title, '격문 앞의 장비');
|
||||
assert.equal(snapshot.progress.completed, 1);
|
||||
|
||||
village.completedTutorialIds.push(
|
||||
ids.meetGuanYu,
|
||||
ids.registerVolunteers
|
||||
);
|
||||
snapshot = resolve(village);
|
||||
assert.equal(snapshot.current.title, '도원결의');
|
||||
assert.match(snapshot.nextClue.detail, /복숭아 동산/);
|
||||
assert.equal(snapshot.progress.completed, 3);
|
||||
|
||||
village.completedTutorialIds.push(ids.complete);
|
||||
snapshot = resolve(village);
|
||||
assert.match(snapshot.current.title, /결의 뒤 이야기/);
|
||||
assert.equal(snapshot.recentCompleted.title, '도원결의');
|
||||
assert.equal(snapshot.progress.completed, 4);
|
||||
}
|
||||
|
||||
function verifyPrologueCampProgress(resolve, campaignModule) {
|
||||
const ids = campaignModule.prologueMilitiaCampCampaignTutorialIds;
|
||||
const camp = freshState(campaignModule, {
|
||||
step: 'prologue-camp'
|
||||
});
|
||||
|
||||
camp.completedTutorialIds.push(ids.inspectArms);
|
||||
let snapshot = resolve(camp);
|
||||
assert.equal(snapshot.contextKind, 'militia-camp-exploration');
|
||||
assert.equal(snapshot.current.title, '관우의 정찰');
|
||||
assert.equal(
|
||||
snapshot.recentCompleted.title,
|
||||
'병기·보급 점검',
|
||||
'The recent completion must follow persisted insertion order, not fixed objective order.'
|
||||
);
|
||||
assert.equal(snapshot.progress.completed, 1);
|
||||
|
||||
camp.completedTutorialIds.push(
|
||||
ids.readyVanguard,
|
||||
ids.reviewScoutReport
|
||||
);
|
||||
snapshot = resolve(camp);
|
||||
assert.equal(snapshot.current.title, '첫 출전 명령');
|
||||
assert.match(snapshot.nextClue.detail, /북쪽 지휘 막사/);
|
||||
assert.equal(snapshot.progress.completed, 3);
|
||||
|
||||
camp.completedTutorialIds.push(ids.complete);
|
||||
snapshot = resolve(camp);
|
||||
assert.match(snapshot.current.title, /출정 이야기/);
|
||||
assert.equal(snapshot.recentCompleted.title, '첫 출전 군령');
|
||||
assert.equal(snapshot.progress.completed, 4);
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
/한석 선봉 격퇴/,
|
||||
'The first battle victory condition must not leak before the battlefield opens.'
|
||||
);
|
||||
}
|
||||
|
||||
function verifyAftermathIsolation(
|
||||
resolve,
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
getSortieFlow
|
||||
) {
|
||||
const battleId = 'first-battle-zhuo-commandery';
|
||||
const state = campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
battleId,
|
||||
'first-camp'
|
||||
);
|
||||
state.pendingAftermathBattleId = battleId;
|
||||
|
||||
const snapshot = resolve(state);
|
||||
const flow = getSortieFlow(state.latestBattleId, state.step);
|
||||
assert.equal(snapshot.contextKind, 'aftermath');
|
||||
assert.match(snapshot.current.title, /승리 후 이야기/);
|
||||
assert.equal(snapshot.recentCompleted.statusLabel, '✓ 완료');
|
||||
assertHiddenFlowDetailsAbsent(snapshot, flow, battleScenarios);
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
new RegExp(escapeRegExp(battleScenarios[battleId].victoryConditionLabel))
|
||||
);
|
||||
}
|
||||
|
||||
function verifyCityStayIsolation(
|
||||
resolve,
|
||||
campaignModule,
|
||||
battleScenarios
|
||||
) {
|
||||
const sourceBattleId = 'seventh-battle-xuzhou-rescue';
|
||||
const state = campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
sourceBattleId,
|
||||
'seventh-camp'
|
||||
);
|
||||
state.activeCityStayId = 'xuzhou';
|
||||
|
||||
let snapshot = resolve(state);
|
||||
assert.equal(snapshot.contextKind, 'city-stay');
|
||||
assert.match(snapshot.current.title, /서주.*현지 정보/);
|
||||
assert.deepEqual(pickProgress(snapshot.progress), {
|
||||
completed: 0,
|
||||
total: 2,
|
||||
knownTotal: true
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
new RegExp(
|
||||
escapeRegExp(
|
||||
battleScenarios['eighth-battle-xiaopei-supply-road'].title
|
||||
)
|
||||
),
|
||||
'A city stay must not expose the title of its future battle.'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
/소패 보급로 장부 확인|여포군 기병/,
|
||||
'A city stay journal must not copy future battle briefing prose.'
|
||||
);
|
||||
|
||||
state.completedCampVisits.push('city-xuzhou-xiaopei-ledger');
|
||||
snapshot = resolve(state);
|
||||
assert.match(snapshot.current.title, /동료와 대화/);
|
||||
assert.equal(snapshot.recentCompleted.title, '현지 정보 확보');
|
||||
assert.equal(snapshot.progress.completed, 1);
|
||||
|
||||
state.completedCampDialogues.push(
|
||||
'city-xuzhou-liu-mi-supply-trust'
|
||||
);
|
||||
snapshot = resolve(state);
|
||||
assert.match(snapshot.current.title, /군영으로 돌아가/);
|
||||
assert.equal(snapshot.progress.completed, 2);
|
||||
assert.match(snapshot.progress.label, /시장은 선택/);
|
||||
}
|
||||
|
||||
function verifyEarlyInterludes(
|
||||
resolve,
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
secondReliefModule,
|
||||
thirdExplorationModule
|
||||
) {
|
||||
const firstCamp = campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
'first-battle-zhuo-commandery',
|
||||
'first-camp'
|
||||
);
|
||||
let snapshot = resolve(firstCamp);
|
||||
assert.equal(snapshot.contextKind, 'interlude-exploration');
|
||||
assert.match(snapshot.current.title, /북문 정찰/);
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
new RegExp(
|
||||
escapeRegExp(
|
||||
battleScenarios['second-battle-yellow-turban-pursuit'].title
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
firstCamp.completedCampVisits.push('first-pursuit-scout-tent');
|
||||
firstCamp.campVisitChoiceIds['first-pursuit-scout-tent'] =
|
||||
'trace-river-ambush';
|
||||
snapshot = resolve(firstCamp);
|
||||
assert.equal(snapshot.progress.completed, 1);
|
||||
assert.equal(
|
||||
snapshot.recentCompleted.title,
|
||||
'강가 매복 흔적을 쫓는다'
|
||||
);
|
||||
|
||||
const secondCamp = campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
secondReliefModule.secondBattleReliefSourceBattleId,
|
||||
'second-camp'
|
||||
);
|
||||
snapshot = resolve(secondCamp);
|
||||
assert.match(snapshot.current.title, /북쪽 마을/);
|
||||
assert.equal(snapshot.progress.completed, 0);
|
||||
|
||||
const firstObjectiveId = 'reassure-northern-village';
|
||||
secondCamp.completedCampVisits.push(
|
||||
secondReliefModule.secondBattleReliefObjectiveProgressId(
|
||||
firstObjectiveId
|
||||
)
|
||||
);
|
||||
snapshot = resolve(secondCamp);
|
||||
assert.match(snapshot.current.title, /나루 사공/);
|
||||
assert.equal(snapshot.progress.completed, 1);
|
||||
assert.match(snapshot.recentCompleted.title, /북쪽 마을/);
|
||||
|
||||
[
|
||||
'question-ferry-witness',
|
||||
'inspect-messenger-trail'
|
||||
].forEach((objectiveId) => {
|
||||
secondCamp.completedCampVisits.push(
|
||||
secondReliefModule.secondBattleReliefObjectiveProgressId(
|
||||
objectiveId
|
||||
)
|
||||
);
|
||||
});
|
||||
snapshot = resolve(secondCamp);
|
||||
assert.match(snapshot.current.title, /다음 출정의 우선 방침/);
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
new RegExp(
|
||||
escapeRegExp(
|
||||
battleScenarios[
|
||||
secondReliefModule.secondBattleReliefTargetBattleId
|
||||
].title
|
||||
)
|
||||
),
|
||||
'The final relief decision must use generic current-action wording instead of the future battle title.'
|
||||
);
|
||||
|
||||
secondCamp.completedCampVisits.push(
|
||||
secondReliefModule.secondBattleReliefVisitId
|
||||
);
|
||||
secondCamp.campVisitChoiceIds[
|
||||
secondReliefModule.secondBattleReliefVisitId
|
||||
] = 'restore-northern-village';
|
||||
snapshot = resolve(secondCamp);
|
||||
assert.equal(snapshot.progress.completed, 4);
|
||||
assert.equal(
|
||||
snapshot.recentCompleted.title,
|
||||
'북쪽 마을 보급선을 먼저 세운다'
|
||||
);
|
||||
|
||||
const thirdCamp = campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
thirdExplorationModule.thirdCampExplorationSourceBattleId,
|
||||
'third-camp',
|
||||
{
|
||||
objectives: [
|
||||
{ id: 'fort', achieved: true, status: 'done' },
|
||||
{ id: 'quick', achieved: true, status: 'done' }
|
||||
],
|
||||
turnNumber: 22
|
||||
}
|
||||
);
|
||||
thirdCamp.firstBattleReport = {
|
||||
...minimalFirstBattleReport(
|
||||
thirdCamp.battleHistory[
|
||||
thirdExplorationModule.thirdCampExplorationSourceBattleId
|
||||
]
|
||||
)
|
||||
};
|
||||
snapshot = resolve(thirdCamp);
|
||||
assert.equal(snapshot.contextKind, 'interlude-exploration');
|
||||
assert.match(snapshot.current.detail, /모두 선택/);
|
||||
assert.equal(snapshot.progress.total, 3);
|
||||
assert.doesNotMatch(
|
||||
stringify(snapshot),
|
||||
new RegExp(
|
||||
escapeRegExp(
|
||||
battleScenarios[
|
||||
thirdExplorationModule.thirdCampExplorationTargetBattleId
|
||||
].title
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
thirdCamp.completedCampVisits.push(
|
||||
thirdExplorationModule.thirdCampExplorationActivityProgressId(
|
||||
'equipment'
|
||||
)
|
||||
);
|
||||
snapshot = resolve(thirdCamp);
|
||||
assert.equal(snapshot.progress.completed, 1);
|
||||
assert.equal(snapshot.recentCompleted.title, '장비 정비');
|
||||
}
|
||||
|
||||
function verifyAllBattleAndCampRoutesHideFutureDetails(
|
||||
resolve,
|
||||
campaignModule,
|
||||
routingModule,
|
||||
battleScenarios,
|
||||
getSortieFlow
|
||||
) {
|
||||
const routes = routingModule.campaignBattleRouteEntries();
|
||||
routes.forEach(({ step, battleId }, index) => {
|
||||
const battleState = freshState(campaignModule, { step });
|
||||
const battleSnapshot = resolve(battleState);
|
||||
assertSnapshotShape(battleSnapshot);
|
||||
assert.equal(
|
||||
battleSnapshot.current.title,
|
||||
`${battleScenarios[battleId].title} 진행`
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
stringify(battleSnapshot),
|
||||
new RegExp(
|
||||
escapeRegExp(battleScenarios[battleId].victoryConditionLabel)
|
||||
),
|
||||
`Current battle ${battleId} must not expose its victory condition through the campaign journal.`
|
||||
);
|
||||
|
||||
const nextRoute = routes[index + 1];
|
||||
if (nextRoute) {
|
||||
assert.doesNotMatch(
|
||||
stringify(battleSnapshot),
|
||||
new RegExp(
|
||||
escapeRegExp(battleScenarios[nextRoute.battleId].title)
|
||||
),
|
||||
`Battle step ${step} must not expose future title ${nextRoute.battleId}.`
|
||||
);
|
||||
}
|
||||
|
||||
const campStep = step.replace(/-battle$/, '-camp');
|
||||
const campState = campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
battleId,
|
||||
campStep
|
||||
);
|
||||
const campSnapshot = resolve(campState);
|
||||
assertSnapshotShape(campSnapshot);
|
||||
const flow = getSortieFlow(
|
||||
campState.latestBattleId,
|
||||
campState.step
|
||||
);
|
||||
assertHiddenFlowDetailsAbsent(
|
||||
campSnapshot,
|
||||
flow,
|
||||
battleScenarios
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function freshState(campaignModule, overrides = {}) {
|
||||
return Object.assign(
|
||||
campaignModule.createInitialCampaignState(),
|
||||
overrides
|
||||
);
|
||||
}
|
||||
|
||||
function campaignWithSettlement(
|
||||
campaignModule,
|
||||
battleScenarios,
|
||||
battleId,
|
||||
step,
|
||||
overrides = {}
|
||||
) {
|
||||
const state = freshState(campaignModule, { step });
|
||||
const settlement = minimalSettlement(
|
||||
battleScenarios[battleId],
|
||||
overrides
|
||||
);
|
||||
state.latestBattleId = battleId;
|
||||
state.battleHistory[battleId] = settlement;
|
||||
return state;
|
||||
}
|
||||
|
||||
function minimalSettlement(scenario, overrides = {}) {
|
||||
return {
|
||||
battleId: scenario.id,
|
||||
battleTitle: scenario.title,
|
||||
outcome: 'victory',
|
||||
turnNumber: 12,
|
||||
rewardGold: 0,
|
||||
itemRewards: [],
|
||||
objectives: [],
|
||||
units: [],
|
||||
bonds: [],
|
||||
completedAt: '2026-07-28T00:00:00.000Z',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function minimalFirstBattleReport(settlement) {
|
||||
return {
|
||||
battleId: settlement.battleId,
|
||||
battleTitle: settlement.battleTitle,
|
||||
outcome: settlement.outcome,
|
||||
turnNumber: settlement.turnNumber,
|
||||
rewardGold: settlement.rewardGold,
|
||||
defeatedEnemies: 1,
|
||||
totalEnemies: 1,
|
||||
objectives: structuredClone(settlement.objectives),
|
||||
units: [],
|
||||
bonds: [],
|
||||
itemRewards: [],
|
||||
completedCampDialogues: [],
|
||||
completedCampVisits: [],
|
||||
createdAt: settlement.completedAt
|
||||
};
|
||||
}
|
||||
|
||||
function assertSnapshotShape(snapshot) {
|
||||
assert(snapshot);
|
||||
assert.equal(typeof snapshot.contextKind, 'string');
|
||||
assertEntry(snapshot.current, '◆ 진행');
|
||||
assertEntry(snapshot.recentCompleted, '✓ 완료');
|
||||
assertEntry(snapshot.nextClue, '? 실마리');
|
||||
assert.equal(typeof snapshot.progress.completed, 'number');
|
||||
assert.equal(typeof snapshot.progress.knownTotal, 'boolean');
|
||||
assert.equal(typeof snapshot.progress.label, 'string');
|
||||
if (snapshot.progress.knownTotal) {
|
||||
assert.equal(typeof snapshot.progress.total, 'number');
|
||||
assert(snapshot.progress.completed <= snapshot.progress.total);
|
||||
} else {
|
||||
assert.equal(snapshot.progress.total, undefined);
|
||||
}
|
||||
assert.equal(typeof snapshot.resumeContext.title, 'string');
|
||||
assert.equal(typeof snapshot.resumeContext.summary, 'string');
|
||||
assert.equal(snapshot.resumeContext.lines.length, 3);
|
||||
}
|
||||
|
||||
function assertEntry(entry, expectedLabel) {
|
||||
assert(entry);
|
||||
assert.equal(entry.statusLabel, expectedLabel);
|
||||
assert.equal(typeof entry.id, 'string');
|
||||
assert(entry.id.length > 0);
|
||||
assert.equal(typeof entry.title, 'string');
|
||||
assert(entry.title.length > 0);
|
||||
assert.equal(typeof entry.detail, 'string');
|
||||
assert(entry.detail.length > 0);
|
||||
}
|
||||
|
||||
function assertHiddenFlowDetailsAbsent(
|
||||
snapshot,
|
||||
flow,
|
||||
battleScenarios
|
||||
) {
|
||||
const serialized = stringify(snapshot);
|
||||
if (!flow?.nextBattleId) {
|
||||
return;
|
||||
}
|
||||
const future = battleScenarios[flow.nextBattleId];
|
||||
assert(future);
|
||||
assert.doesNotMatch(
|
||||
serialized,
|
||||
new RegExp(escapeRegExp(future.title)),
|
||||
`Future battle title ${future.title} must remain hidden.`
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
serialized,
|
||||
new RegExp(escapeRegExp(future.victoryConditionLabel)),
|
||||
`Future victory condition ${future.victoryConditionLabel} must remain hidden.`
|
||||
);
|
||||
assert.equal(
|
||||
serialized.includes(flow.description),
|
||||
false,
|
||||
'Future campaign-flow description must not be copied into the journal.'
|
||||
);
|
||||
assert.equal(
|
||||
serialized.includes(flow.rewardHint),
|
||||
false,
|
||||
'Future reward/recruit hints must not be copied into the journal.'
|
||||
);
|
||||
}
|
||||
|
||||
function pickProgress(progress) {
|
||||
return {
|
||||
completed: progress.completed,
|
||||
total: progress.total,
|
||||
knownTotal: progress.knownTotal
|
||||
};
|
||||
}
|
||||
|
||||
function stringify(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ const checks = [
|
||||
'scripts/verify-campaign-save-normalization.mjs',
|
||||
'scripts/verify-campaign-completion.mjs',
|
||||
'scripts/verify-campaign-presentation-profiles.mjs',
|
||||
'scripts/verify-campaign-objective-journal.mjs',
|
||||
'scripts/verify-growth-consistency.mjs',
|
||||
'scripts/verify-combat-presentation-settings.mjs',
|
||||
'scripts/verify-visual-motion-settings.mjs',
|
||||
|
||||
Reference in New Issue
Block a user