fix: refine turn completion and interaction flow
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user