fix: refine turn completion and interaction flow

This commit is contained in:
2026-07-20 12:32:17 +09:00
parent 31e1396ac8
commit 3961282d82
4 changed files with 449 additions and 96 deletions

View File

@@ -35,13 +35,14 @@ try {
validateNumericProperties('gold', { min: 1, max: 20000 });
validateNumericProperties('bondExp', { min: 0, max: 200 });
validateNumericProperties('rewardExp', { min: 1, max: 200 });
const interactionUxGuardCount = validateCampInteractionUxGuards();
if (errors.length > 0) {
throw new Error(`Camp reward data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`);
}
console.log(
`Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${campBondReferences} camp bond references, ${rewardBondLinks} reward-bond links, ${itemRewardArrays.length} camp item reward lists, and camp reward numeric fields.`
`Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${campBondReferences} camp bond references, ${rewardBondLinks} reward-bond links, ${itemRewardArrays.length} camp item reward lists, camp reward numeric fields, and ${interactionUxGuardCount} camp interaction UX guards.`
);
} finally {
await server.close();
@@ -87,6 +88,125 @@ function validateNumericProperties(propertyName, { min, max }) {
}
}
function validateCampInteractionUxGuards() {
const visitRewardMethod = extractPrivateMethod(source, 'showVisitReward');
const dialogueRewardMethod = extractPrivateMethod(source, 'showDialogueReward');
const noticeMethod = extractPrivateMethod(source, 'showCampNotice');
const noticeDurationMethod = extractPrivateMethod(source, 'campNoticeHoldDuration');
const enterKeyMethod = extractPrivateMethod(source, 'handleSortieEnterKey');
const navigationMethod = extractPrivateMethod(source, 'startCampNavigation');
const steppedNavigationMethod = extractPrivateMethod(source, 'startCampNavigationWithCampaignStep');
const startStoryMethod = extractPrivateMethod(source, 'startVictoryStory');
const saveMethod = extractPrivateMethod(source, 'saveCampToSlot');
let checkedGuardCount = 0;
expectCampUx(
visitRewardMethod.includes('획득 내역') &&
visitRewardMethod.includes('${this.visitRewardText(choice)}') &&
visitRewardMethod.includes('응답 · ${choice.response}') &&
!visitRewardMethod.includes('delayedCall'),
'visit rewards must display the acquisition details and response together without delayed replacement'
);
checkedGuardCount += 1;
expectCampUx(
dialogueRewardMethod.includes('획득 내역') &&
dialogueRewardMethod.includes('공명 +${rewardExp}') &&
dialogueRewardMethod.includes('응답 · ${choice.response}') &&
!dialogueRewardMethod.includes('delayedCall'),
'dialogue rewards must display the acquisition details and response together without delayed replacement'
);
checkedGuardCount += 1;
expectCampUx(
noticeMethod.includes('delay: this.campNoticeHoldDuration(message)') &&
noticeMethod.includes('text.height + 28'),
'camp notices must size multi-line content and use a message-aware hold duration'
);
checkedGuardCount += 1;
const minimumHoldMs = Number(/const campNoticeMinimumHoldMs = (\d+);/.exec(source)?.[1] ?? 0);
expectCampUx(
minimumHoldMs >= 2500 &&
noticeDurationMethod.includes('readableCharacterCount * campNoticeCharacterHoldMs') &&
noticeDurationMethod.includes('campNoticeMaximumHoldMs'),
`camp notice timing must retain a readable minimum and scale with body length (found ${minimumHoldMs}ms)`
);
checkedGuardCount += 1;
expectCampUx(
source.includes('private navigationPending = false;') &&
enterKeyMethod.includes('event.repeat || this.navigationPending'),
'sortie Enter handling must ignore key-repeat events and pending navigation'
);
checkedGuardCount += 1;
expectCampUx(
navigationMethod.includes('if (this.navigationPending)') &&
navigationMethod.includes('this.navigationPending = true;') &&
navigationMethod.includes('startLazyScene(this, key, data)') &&
navigationMethod.includes('this.navigationPending = false;'),
'lazy camp navigation must lock before loading and unlock after a failed transition'
);
checkedGuardCount += 1;
expectCampUx(
startStoryMethod.includes('if (this.navigationPending)') &&
!startStoryMethod.includes('startLazyScene(') &&
((startStoryMethod.match(/this\.startCampNavigation\(/g)?.length ?? 0) +
(startStoryMethod.match(/this\.startCampNavigationWithCampaignStep\(/g)?.length ?? 0)) >= 4,
'every camp story, battle, and ending transition must route through the scene navigation lock'
);
checkedGuardCount += 1;
expectCampUx(
steppedNavigationMethod.includes('const previousStep = getCampaignState().step') &&
steppedNavigationMethod.includes('markCampaignStep(step)') &&
steppedNavigationMethod.includes('getCampaignState().step === step') &&
steppedNavigationMethod.includes('markCampaignStep(previousStep)') &&
navigationMethod.includes('recoverState?.()'),
'campaign-step navigation must restore the previous step when lazy scene loading fails'
);
checkedGuardCount += 1;
const transientUndoFields = [
'sortieSwapUndoState',
'sortieRecommendationUndoState',
'sortiePresetUndoState',
'sortieGuidedImprovementUndoState',
'reportFormationHistoryUndoState'
];
const clearedUndoField = transientUndoFields.find((field) => new RegExp(`this\\.${field}\\s*=\\s*undefined`).test(saveMethod));
expectCampUx(
!clearedUndoField && !saveMethod.includes('persistSortieSelection('),
`manual camp saves must preserve applicable undo state${clearedUndoField ? ` (cleared ${clearedUndoField})` : ''}`
);
checkedGuardCount += 1;
return checkedGuardCount;
}
function expectCampUx(condition, message) {
if (!condition) {
errors.push(`${sourcePath}: ${message}`);
}
}
function extractPrivateMethod(text, methodName) {
const match = new RegExp(`\\bprivate\\s+${methodName}\\s*\\(`).exec(text);
if (!match) {
errors.push(`${sourcePath}: cannot find private method ${methodName}`);
return '';
}
const bodyStart = text.indexOf('{', match.index + match[0].length);
if (bodyStart < 0) {
errors.push(`${sourcePath}:${lineNumber(match.index)} cannot find body for private method ${methodName}`);
return '';
}
const bodyEnd = findMatchingDelimiter(text, bodyStart, '{', '}');
return text.slice(bodyStart, bodyEnd + 1);
}
function collectCampBattleIdEntries(text, defaultBattleScenarioId) {
const objectSource = extractConstObject(text, 'campBattleIds');
const objectOffset = text.indexOf(objectSource);