feat: prioritize third-victory return dialogue
This commit is contained in:
@@ -172,9 +172,24 @@ RPG로 다듬는다.
|
|||||||
- 낮 강변 환경음, 지면별 발소리, 목표 완료의 시각·음향 신호를 함께
|
- 낮 강변 환경음, 지면별 발소리, 목표 완료의 시각·음향 신호를 함께
|
||||||
사용해 소리를 듣지 못해도 진행 상태를 이해할 수 있게 한다.
|
사용해 소리를 듣지 못해도 진행 상태를 이해할 수 있게 한다.
|
||||||
|
|
||||||
다음 초반 묶음은 세 번째 군영부터 반복되는 방문·대화·상점 흐름을 감사해
|
### 완료한 8차 묶음: 세 번째 승리의 결과별 우선 귀환 대화
|
||||||
정보 수집, 장비 구매, 동료 대화 가운데 무엇을 먼저 택했는지가 출전
|
|
||||||
브리핑과 첫 턴의 전술 선택에 짧고 명확하게 되돌아오게 한다.
|
- 세 번째 전투에서 실제로 강가 요새를 확보했는지, 30턴 안에 승리했는지,
|
||||||
|
몇 턴이 걸렸는지를 승리 기록에서 함께 읽는다.
|
||||||
|
- 요새를 놓쳤다면 유비·관우의 방어선 회고, 요새는 지켰지만 늦었다면
|
||||||
|
유비·장비의 추격 속도 회고, 둘 다 달성했다면 관우·장비의 본영 진입
|
||||||
|
대화를 `새 이야기 · 우선`으로 한 곳만 제시한다.
|
||||||
|
- 전투 결과와 반대되는 칭찬을 하지 않으며, 오래되거나 손상되어 목표와
|
||||||
|
턴을 신뢰할 수 없는 저장은 기존 정적 대사를 그대로 사용한다.
|
||||||
|
- 출진 준비의 `공명 대화` 권장은 우선 귀환 대화 한 건만 완료하면 충족된다.
|
||||||
|
나머지 두 대화와 공명 보상은 사라지지 않고 자유 선택으로 남는다.
|
||||||
|
- 우선 대사를 완료하면 대화 탭과 카드의 새 이야기 표식이 사라지고,
|
||||||
|
다시 군영에 들어와도 완료 상태를 유지한다.
|
||||||
|
- 네 가지 목표 조합과 Canvas·WebGL의 1920x1080 진행을 자동 검증한다.
|
||||||
|
|
||||||
|
다음 초반 묶음은 세 번째 군영에서 정보 수집, 장비 정비, 동료 대화 가운데
|
||||||
|
이번 출진의 준비 우선순위 하나를 직접 정하고, 그 선택이 네 번째 전투
|
||||||
|
브리핑과 첫 3턴 안에 짧고 명확하게 되돌아오게 한다.
|
||||||
|
|
||||||
## 장기 개선 순서
|
## 장기 개선 순서
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
"verify:first-pursuit-camp:browser": "node scripts/verify-first-pursuit-camp-exploration-browser.mjs",
|
"verify:first-pursuit-camp:browser": "node scripts/verify-first-pursuit-camp-exploration-browser.mjs",
|
||||||
"verify:second-battle-relief": "node scripts/verify-second-battle-relief-exploration.mjs",
|
"verify:second-battle-relief": "node scripts/verify-second-battle-relief-exploration.mjs",
|
||||||
"verify:second-battle-relief:browser": "node scripts/verify-second-battle-relief-exploration-browser.mjs",
|
"verify:second-battle-relief:browser": "node scripts/verify-second-battle-relief-exploration-browser.mjs",
|
||||||
|
"verify:third-battle-return": "node scripts/verify-third-battle-return-dialogue.mjs",
|
||||||
|
"verify:third-battle-return:browser": "node scripts/verify-third-battle-priority-return-browser.mjs",
|
||||||
"verify:prologue-exploration-assets": "node scripts/verify-prologue-exploration-asset-data.mjs",
|
"verify:prologue-exploration-assets": "node scripts/verify-prologue-exploration-asset-data.mjs",
|
||||||
"verify:exploration-backgrounds": "node scripts/verify-prologue-exploration-asset-data.mjs",
|
"verify:exploration-backgrounds": "node scripts/verify-prologue-exploration-asset-data.mjs",
|
||||||
"verify:exploration-characters": "node scripts/verify-exploration-character-asset-data.mjs",
|
"verify:exploration-characters": "node scripts/verify-exploration-character-asset-data.mjs",
|
||||||
@@ -73,7 +75,7 @@
|
|||||||
"verify:interaction-ux": "node scripts/verify-interaction-ux.mjs",
|
"verify:interaction-ux": "node scripts/verify-interaction-ux.mjs",
|
||||||
"verify:save-flow": "node scripts/verify-save-retry-flow.mjs",
|
"verify:save-flow": "node scripts/verify-save-retry-flow.mjs",
|
||||||
"verify:release": "node scripts/verify-release-candidate.mjs",
|
"verify:release": "node scripts/verify-release-candidate.mjs",
|
||||||
"verify:local-release": "pnpm run verify:static-data && pnpm run verify:unit-action-assets && pnpm run verify:enemy-intent && tsc --noEmit && pnpm run build && pnpm run verify:unit-action-assets:dist && pnpm run verify:loader-lifecycle && pnpm run verify:interaction-ux && pnpm run verify:city-stays:browser && pnpm run verify:prologue-village:browser && pnpm run verify:first-pursuit-camp:browser && pnpm run verify:second-battle-relief:browser && pnpm run verify:av-feedback:browser && pnpm run verify:release && pnpm run verify:performance",
|
"verify:local-release": "pnpm run verify:static-data && pnpm run verify:unit-action-assets && pnpm run verify:enemy-intent && tsc --noEmit && pnpm run build && pnpm run verify:unit-action-assets:dist && pnpm run verify:loader-lifecycle && pnpm run verify:interaction-ux && pnpm run verify:city-stays:browser && pnpm run verify:prologue-village:browser && pnpm run verify:first-pursuit-camp:browser && pnpm run verify:second-battle-relief:browser && pnpm run verify:third-battle-return:browser && pnpm run verify:av-feedback:browser && pnpm run verify:release && pnpm run verify:performance",
|
||||||
"verify:1.0": "pnpm run verify:local-release && pnpm run verify:flow && pnpm run qa:campaign-complete && pnpm run qa:battle-maps:webgl",
|
"verify:1.0": "pnpm run verify:local-release && pnpm run verify:flow && pnpm run qa:campaign-complete && pnpm run qa:battle-maps:webgl",
|
||||||
"verify:public-deploy": "node scripts/verify-public-deploy.mjs",
|
"verify:public-deploy": "node scripts/verify-public-deploy.mjs",
|
||||||
"qa:representative": "node scripts/qa-representative-battles.mjs",
|
"qa:representative": "node scripts/qa-representative-battles.mjs",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const checks = [
|
|||||||
'scripts/verify-first-battle-camaraderie-memory.mjs',
|
'scripts/verify-first-battle-camaraderie-memory.mjs',
|
||||||
'scripts/verify-first-pursuit-scout-memory.mjs',
|
'scripts/verify-first-pursuit-scout-memory.mjs',
|
||||||
'scripts/verify-second-battle-relief-exploration.mjs',
|
'scripts/verify-second-battle-relief-exploration.mjs',
|
||||||
|
'scripts/verify-third-battle-return-dialogue.mjs',
|
||||||
'scripts/verify-prologue-exploration-asset-data.mjs',
|
'scripts/verify-prologue-exploration-asset-data.mjs',
|
||||||
'scripts/verify-exploration-character-asset-data.mjs',
|
'scripts/verify-exploration-character-asset-data.mjs',
|
||||||
'scripts/verify-prologue-dialogue-portrait-data.mjs',
|
'scripts/verify-prologue-dialogue-portrait-data.mjs',
|
||||||
|
|||||||
1111
scripts/verify-third-battle-priority-return-browser.mjs
Normal file
1111
scripts/verify-third-battle-priority-return-browser.mjs
Normal file
File diff suppressed because it is too large
Load Diff
422
scripts/verify-third-battle-return-dialogue.mjs
Normal file
422
scripts/verify-third-battle-return-dialogue.mjs
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createServer } from 'vite';
|
||||||
|
|
||||||
|
const server = await createServer({
|
||||||
|
logLevel: 'error',
|
||||||
|
server: { middlewareMode: true, hmr: false },
|
||||||
|
appType: 'custom'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dialogueModule = await server.ssrLoadModule(
|
||||||
|
'/src/game/data/thirdBattleReturnDialogue.ts'
|
||||||
|
);
|
||||||
|
const { battleScenarios } = await server.ssrLoadModule(
|
||||||
|
'/src/game/data/battles.ts'
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyCanonicalBattleReferences(dialogueModule, battleScenarios);
|
||||||
|
verifyAllObjectiveCombinations(dialogueModule);
|
||||||
|
verifyTargetBattleIsolation(dialogueModule);
|
||||||
|
verifyLegacyReportFallback(dialogueModule);
|
||||||
|
verifyInvalidLegacyDataFallsBackToStaticDialogue(dialogueModule);
|
||||||
|
verifyReturnedDataIsCloned(dialogueModule);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Third-battle priority return dialogue verification passed (four objective combinations, result-specific priority targets, exact battle isolation, and static-dialogue fallback for invalid legacy data).'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyCanonicalBattleReferences(dialogueModule, battleScenarios) {
|
||||||
|
const scenario =
|
||||||
|
battleScenarios[dialogueModule.thirdBattleReturnSourceBattleId];
|
||||||
|
assert(scenario, 'The return dialogue must reference a canonical battle.');
|
||||||
|
assert.equal(scenario.id, 'third-battle-guangzong-road');
|
||||||
|
assert.equal(
|
||||||
|
scenario.quickVictoryTurnLimit,
|
||||||
|
dialogueModule.thirdBattleReturnQuickVictoryTurnLimit
|
||||||
|
);
|
||||||
|
|
||||||
|
const fortObjective = scenario.objectives.find(
|
||||||
|
({ id }) => id === dialogueModule.thirdBattleReturnObjectiveIds.fort
|
||||||
|
);
|
||||||
|
const quickObjective = scenario.objectives.find(
|
||||||
|
({ id }) => id === dialogueModule.thirdBattleReturnObjectiveIds.quick
|
||||||
|
);
|
||||||
|
assert.equal(fortObjective?.kind, 'secure-terrain');
|
||||||
|
assert.equal(fortObjective?.label, '강가 요새 확보');
|
||||||
|
assert.equal(quickObjective?.kind, 'quick-victory');
|
||||||
|
assert.equal(
|
||||||
|
quickObjective?.maxTurn,
|
||||||
|
dialogueModule.thirdBattleReturnQuickVictoryTurnLimit
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
dialogueModule.thirdBattleReturnTargetDialogueIds,
|
||||||
|
{
|
||||||
|
fortRecovery: 'liu-guan-after-guangzong-road',
|
||||||
|
tempoRecovery: 'liu-zhang-after-guangzong-road',
|
||||||
|
decisiveAdvance: 'guan-zhang-after-guangzong-road'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dialogueModule.thirdBattleReturnPriorityLabel,
|
||||||
|
'새 이야기 · 우선'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyAllObjectiveCombinations(dialogueModule) {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
fortAchieved: false,
|
||||||
|
quickAchieved: false,
|
||||||
|
turnNumber: 35,
|
||||||
|
expectedVariantId: 'fort-recovery',
|
||||||
|
expectedTargetDialogueId: 'liu-guan-after-guangzong-road',
|
||||||
|
expectedSummary: /강가 요새 미확보 · 35턴 · 30턴 제한 초과/,
|
||||||
|
expectedLine: /35턴 만에/,
|
||||||
|
forbiddenText: /강가 요새까지 확보했습니다/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fortAchieved: false,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 24,
|
||||||
|
expectedVariantId: 'fort-recovery',
|
||||||
|
expectedTargetDialogueId: 'liu-guan-after-guangzong-road',
|
||||||
|
expectedSummary: /강가 요새 미확보 · 24턴 신속 승리/,
|
||||||
|
expectedLine: /전령을 쫓는 속도는 늦지 않았지만/,
|
||||||
|
forbiddenText: /강가 요새까지 확보했습니다/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: false,
|
||||||
|
turnNumber: 34,
|
||||||
|
expectedVariantId: 'tempo-recovery',
|
||||||
|
expectedTargetDialogueId: 'liu-zhang-after-guangzong-road',
|
||||||
|
expectedSummary: /강가 요새 확보 · 34턴 · 30턴 제한 초과/,
|
||||||
|
expectedLine: /34턴이 걸려/,
|
||||||
|
forbiddenText: /신속 승리/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 22,
|
||||||
|
expectedVariantId: 'decisive-advance',
|
||||||
|
expectedTargetDialogueId: 'guan-zhang-after-guangzong-road',
|
||||||
|
expectedSummary: /강가 요새 확보 · 22턴 신속 승리/,
|
||||||
|
expectedLine: /22턴 만에/,
|
||||||
|
forbiddenText: /확보하지 못했습니다/
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const resolved =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue(
|
||||||
|
campaignWithHistory(dialogueModule, testCase)
|
||||||
|
);
|
||||||
|
assert(resolved, `Expected a dialogue for ${JSON.stringify(testCase)}.`);
|
||||||
|
assert.equal(resolved.reportSource, 'battle-history');
|
||||||
|
assert.equal(
|
||||||
|
resolved.sourceBattleId,
|
||||||
|
dialogueModule.thirdBattleReturnSourceBattleId
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolved.targetDialogueId,
|
||||||
|
testCase.expectedTargetDialogueId
|
||||||
|
);
|
||||||
|
assert.equal(resolved.priorityLabel, '새 이야기 · 우선');
|
||||||
|
assert.equal(resolved.variantId, testCase.expectedVariantId);
|
||||||
|
assert.equal(resolved.turnNumber, testCase.turnNumber);
|
||||||
|
assert.equal(resolved.fortAchieved, testCase.fortAchieved);
|
||||||
|
assert.equal(resolved.quickAchieved, testCase.quickAchieved);
|
||||||
|
assert.equal(resolved.lines.length, 3);
|
||||||
|
assert.match(resolved.summary, testCase.expectedSummary);
|
||||||
|
assert.match(resolved.lines.join('\n'), testCase.expectedLine);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
`${resolved.summary}\n${resolved.lines.join('\n')}`,
|
||||||
|
testCase.forbiddenText,
|
||||||
|
'A missed objective must never receive false praise.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const failedStatusOverridesAchieved =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
[dialogueModule.thirdBattleReturnSourceBattleId]: victoryReport(
|
||||||
|
dialogueModule,
|
||||||
|
{
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: false,
|
||||||
|
turnNumber: 35,
|
||||||
|
fortStatus: 'failed'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
failedStatusOverridesAchieved?.fortAchieved,
|
||||||
|
false,
|
||||||
|
'A failed objective status must prevent phantom achievement praise.'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
failedStatusOverridesAchieved?.variantId,
|
||||||
|
'fort-recovery'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyTargetBattleIsolation(dialogueModule) {
|
||||||
|
const targetVictory = victoryReport(dialogueModule, {
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 20
|
||||||
|
});
|
||||||
|
const otherVictory = {
|
||||||
|
...targetVictory,
|
||||||
|
battleId: 'fourth-battle-guangzong-camp'
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
'fourth-battle-guangzong-camp': otherVictory
|
||||||
|
},
|
||||||
|
firstBattleReport: otherVictory
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
'An unrelated victory must not create a third-camp priority dialogue.'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
[dialogueModule.thirdBattleReturnSourceBattleId]: otherVictory
|
||||||
|
},
|
||||||
|
firstBattleReport: targetVictory
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
'A mismatched canonical history entry must block stale report fallback.'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
[dialogueModule.thirdBattleReturnSourceBattleId]: {
|
||||||
|
...targetVictory,
|
||||||
|
outcome: 'defeat'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
firstBattleReport: targetVictory
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
'A canonical defeat must not be replaced by a stale victory report.'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
'legacy-wrong-key': targetVictory
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
'A target settlement stored under another battle key must be ignored.'
|
||||||
|
);
|
||||||
|
|
||||||
|
const historyWins =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
[dialogueModule.thirdBattleReturnSourceBattleId]: targetVictory
|
||||||
|
},
|
||||||
|
firstBattleReport: otherVictory
|
||||||
|
});
|
||||||
|
assert.equal(historyWins?.reportSource, 'battle-history');
|
||||||
|
assert.equal(historyWins?.variantId, 'decisive-advance');
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyLegacyReportFallback(dialogueModule) {
|
||||||
|
const legacyReport = victoryReport(dialogueModule, {
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 19,
|
||||||
|
omitStatus: true
|
||||||
|
});
|
||||||
|
const fromMissingHistory =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
firstBattleReport: legacyReport
|
||||||
|
});
|
||||||
|
assert.equal(fromMissingHistory?.reportSource, 'legacy-report');
|
||||||
|
assert.equal(fromMissingHistory?.variantId, 'decisive-advance');
|
||||||
|
assert.equal(fromMissingHistory?.fortAchieved, true);
|
||||||
|
assert.equal(fromMissingHistory?.quickAchieved, true);
|
||||||
|
assert.match(fromMissingHistory?.lines.join('\n') ?? '', /19턴 만에/);
|
||||||
|
|
||||||
|
const fromEmptyHistory =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {},
|
||||||
|
firstBattleReport: legacyReport
|
||||||
|
});
|
||||||
|
assert.equal(fromEmptyHistory?.reportSource, 'legacy-report');
|
||||||
|
assert.equal(fromEmptyHistory?.variantId, 'decisive-advance');
|
||||||
|
|
||||||
|
const missingTurn =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
firstBattleReport: {
|
||||||
|
...legacyReport,
|
||||||
|
turnNumber: 'unknown'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
missingTurn,
|
||||||
|
undefined,
|
||||||
|
'A legacy report without a trustworthy turn must preserve the static dialogue.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyInvalidLegacyDataFallsBackToStaticDialogue(
|
||||||
|
dialogueModule
|
||||||
|
) {
|
||||||
|
const complete = victoryReport(dialogueModule, {
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 21
|
||||||
|
});
|
||||||
|
const missingObjective =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
firstBattleReport: {
|
||||||
|
...complete,
|
||||||
|
objectives: complete.objectives.filter(
|
||||||
|
({ id }) => id !== dialogueModule.thirdBattleReturnObjectiveIds.quick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertNoPriorityOverride(
|
||||||
|
missingObjective,
|
||||||
|
'A legacy report with a missing objective'
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateObjective =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
battleHistory: {
|
||||||
|
[dialogueModule.thirdBattleReturnSourceBattleId]: {
|
||||||
|
...complete,
|
||||||
|
objectives: [
|
||||||
|
...complete.objectives,
|
||||||
|
{
|
||||||
|
id: dialogueModule.thirdBattleReturnObjectiveIds.fort,
|
||||||
|
achieved: false,
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertNoPriorityOverride(
|
||||||
|
duplicateObjective,
|
||||||
|
'A report with duplicate objective ids'
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidObjective =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
firstBattleReport: {
|
||||||
|
...complete,
|
||||||
|
objectives: complete.objectives.map((objective) =>
|
||||||
|
objective.id === dialogueModule.thirdBattleReturnObjectiveIds.fort
|
||||||
|
? { ...objective, achieved: 'yes' }
|
||||||
|
: objective
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertNoPriorityOverride(
|
||||||
|
invalidObjective,
|
||||||
|
'A legacy report with a non-boolean objective result'
|
||||||
|
);
|
||||||
|
|
||||||
|
const inconsistentTurn =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
|
||||||
|
firstBattleReport: victoryReport(dialogueModule, {
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 31
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assertNoPriorityOverride(
|
||||||
|
inconsistentTurn,
|
||||||
|
'A quick-victory result that contradicts its turn number'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyReturnedDataIsCloned(dialogueModule) {
|
||||||
|
const campaign = campaignWithHistory(dialogueModule, {
|
||||||
|
fortAchieved: true,
|
||||||
|
quickAchieved: true,
|
||||||
|
turnNumber: 18
|
||||||
|
});
|
||||||
|
const first =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue(campaign);
|
||||||
|
assert(first);
|
||||||
|
first.lines[0] = 'mutated';
|
||||||
|
|
||||||
|
const second =
|
||||||
|
dialogueModule.resolveThirdBattlePriorityReturnDialogue(campaign);
|
||||||
|
assert(second);
|
||||||
|
assert.notEqual(second.lines[0], 'mutated');
|
||||||
|
}
|
||||||
|
|
||||||
|
function campaignWithHistory(dialogueModule, options) {
|
||||||
|
return {
|
||||||
|
battleHistory: {
|
||||||
|
[dialogueModule.thirdBattleReturnSourceBattleId]:
|
||||||
|
victoryReport(dialogueModule, options)
|
||||||
|
},
|
||||||
|
firstBattleReport: {
|
||||||
|
...victoryReport(dialogueModule, options),
|
||||||
|
battleId: 'fourth-battle-guangzong-camp'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function victoryReport(
|
||||||
|
dialogueModule,
|
||||||
|
{
|
||||||
|
fortAchieved,
|
||||||
|
quickAchieved,
|
||||||
|
turnNumber,
|
||||||
|
fortStatus,
|
||||||
|
quickStatus,
|
||||||
|
omitStatus = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const objective = (id, achieved, status) => ({
|
||||||
|
id,
|
||||||
|
achieved,
|
||||||
|
...(omitStatus
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
status:
|
||||||
|
status ??
|
||||||
|
(achieved ? 'done' : 'failed')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
battleId: dialogueModule.thirdBattleReturnSourceBattleId,
|
||||||
|
outcome: 'victory',
|
||||||
|
turnNumber,
|
||||||
|
objectives: [
|
||||||
|
objective(
|
||||||
|
dialogueModule.thirdBattleReturnObjectiveIds.fort,
|
||||||
|
fortAchieved,
|
||||||
|
fortStatus
|
||||||
|
),
|
||||||
|
objective(
|
||||||
|
dialogueModule.thirdBattleReturnObjectiveIds.quick,
|
||||||
|
quickAchieved,
|
||||||
|
quickStatus
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoPriorityOverride(resolved, condition) {
|
||||||
|
assert.equal(
|
||||||
|
resolved,
|
||||||
|
undefined,
|
||||||
|
`${condition} must leave the existing static dialogue unchanged.`
|
||||||
|
);
|
||||||
|
}
|
||||||
304
src/game/data/thirdBattleReturnDialogue.ts
Normal file
304
src/game/data/thirdBattleReturnDialogue.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
export const thirdBattleReturnSourceBattleId =
|
||||||
|
'third-battle-guangzong-road';
|
||||||
|
export const thirdBattleReturnCampStep = 'third-camp';
|
||||||
|
export const thirdBattleReturnPriorityLabel = '새 이야기 · 우선';
|
||||||
|
export const thirdBattleReturnQuickVictoryTurnLimit = 30;
|
||||||
|
export const thirdBattleReturnObjectiveIds = {
|
||||||
|
fort: 'fort',
|
||||||
|
quick: 'quick'
|
||||||
|
} as const;
|
||||||
|
export const thirdBattleReturnTargetDialogueIds = {
|
||||||
|
fortRecovery: 'liu-guan-after-guangzong-road',
|
||||||
|
tempoRecovery: 'liu-zhang-after-guangzong-road',
|
||||||
|
decisiveAdvance: 'guan-zhang-after-guangzong-road'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ThirdBattleReturnDialogueVariantId =
|
||||||
|
| 'fort-recovery'
|
||||||
|
| 'tempo-recovery'
|
||||||
|
| 'decisive-advance';
|
||||||
|
|
||||||
|
export type ThirdBattleReturnTargetDialogueId =
|
||||||
|
(typeof thirdBattleReturnTargetDialogueIds)[
|
||||||
|
keyof typeof thirdBattleReturnTargetDialogueIds
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ThirdBattleReturnReportSource =
|
||||||
|
| 'battle-history'
|
||||||
|
| 'legacy-report';
|
||||||
|
|
||||||
|
export type ThirdBattleReturnReport = {
|
||||||
|
battleId?: unknown;
|
||||||
|
outcome?: unknown;
|
||||||
|
turnNumber?: unknown;
|
||||||
|
objectives?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThirdBattleReturnCampaignState = {
|
||||||
|
battleHistory?: Readonly<
|
||||||
|
Record<string, ThirdBattleReturnReport | null | undefined>
|
||||||
|
>;
|
||||||
|
firstBattleReport?: ThirdBattleReturnReport | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThirdBattlePriorityReturnDialogue = {
|
||||||
|
sourceBattleId: typeof thirdBattleReturnSourceBattleId;
|
||||||
|
campStep: typeof thirdBattleReturnCampStep;
|
||||||
|
targetDialogueId: ThirdBattleReturnTargetDialogueId;
|
||||||
|
priorityLabel: typeof thirdBattleReturnPriorityLabel;
|
||||||
|
reportSource: ThirdBattleReturnReportSource;
|
||||||
|
variantId: ThirdBattleReturnDialogueVariantId;
|
||||||
|
turnNumber: number;
|
||||||
|
fortAchieved: boolean;
|
||||||
|
quickAchieved: boolean;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
lines: [string, string, string];
|
||||||
|
};
|
||||||
|
|
||||||
|
type VictoryReportSelection = {
|
||||||
|
report: ThirdBattleReturnReport;
|
||||||
|
source: ThirdBattleReturnReportSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveThirdBattlePriorityReturnDialogue(
|
||||||
|
campaign?: ThirdBattleReturnCampaignState | null
|
||||||
|
): ThirdBattlePriorityReturnDialogue | undefined {
|
||||||
|
const selection = selectVictoryReport(campaign);
|
||||||
|
if (!selection) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnNumber = positiveInteger(selection.report.turnNumber);
|
||||||
|
const fortAchieved = readObjective(
|
||||||
|
selection.report.objectives,
|
||||||
|
thirdBattleReturnObjectiveIds.fort
|
||||||
|
);
|
||||||
|
const quickAchieved = readObjective(
|
||||||
|
selection.report.objectives,
|
||||||
|
thirdBattleReturnObjectiveIds.quick
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
turnNumber === undefined ||
|
||||||
|
fortAchieved === undefined ||
|
||||||
|
quickAchieved === undefined ||
|
||||||
|
quickAchieved !==
|
||||||
|
(turnNumber <= thirdBattleReturnQuickVictoryTurnLimit)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResultDialogue({
|
||||||
|
reportSource: selection.source,
|
||||||
|
turnNumber,
|
||||||
|
fortAchieved,
|
||||||
|
quickAchieved
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectVictoryReport(
|
||||||
|
campaign?: ThirdBattleReturnCampaignState | null
|
||||||
|
): VictoryReportSelection | undefined {
|
||||||
|
const history = campaign?.battleHistory;
|
||||||
|
if (
|
||||||
|
history &&
|
||||||
|
Object.prototype.hasOwnProperty.call(
|
||||||
|
history,
|
||||||
|
thirdBattleReturnSourceBattleId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const report = history[thirdBattleReturnSourceBattleId];
|
||||||
|
return validThirdBattleVictory(report)
|
||||||
|
? { report, source: 'battle-history' }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = campaign?.firstBattleReport;
|
||||||
|
return validThirdBattleVictory(report)
|
||||||
|
? { report, source: 'legacy-report' }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validThirdBattleVictory(
|
||||||
|
report?: ThirdBattleReturnReport | null
|
||||||
|
): report is ThirdBattleReturnReport {
|
||||||
|
return Boolean(
|
||||||
|
report &&
|
||||||
|
report.battleId === thirdBattleReturnSourceBattleId &&
|
||||||
|
report.outcome === 'victory'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readObjective(
|
||||||
|
rawObjectives: unknown,
|
||||||
|
objectiveId: string
|
||||||
|
): boolean | undefined {
|
||||||
|
if (!Array.isArray(rawObjectives)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = rawObjectives.filter(
|
||||||
|
(objective) =>
|
||||||
|
isRecord(objective) &&
|
||||||
|
objective.id === objectiveId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
matches.length !== 1 ||
|
||||||
|
typeof matches[0].achieved !== 'boolean'
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
matches[0].achieved === true &&
|
||||||
|
matches[0].status !== 'failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResultDialogue(options: {
|
||||||
|
reportSource: ThirdBattleReturnReportSource;
|
||||||
|
turnNumber: number;
|
||||||
|
fortAchieved: boolean;
|
||||||
|
quickAchieved: boolean;
|
||||||
|
}): ThirdBattlePriorityReturnDialogue {
|
||||||
|
if (!options.fortAchieved) {
|
||||||
|
return createFortRecoveryDialogue(options);
|
||||||
|
}
|
||||||
|
if (!options.quickAchieved) {
|
||||||
|
return createTempoRecoveryDialogue(options);
|
||||||
|
}
|
||||||
|
return createDecisiveAdvanceDialogue(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFortRecoveryDialogue(options: {
|
||||||
|
reportSource: ThirdBattleReturnReportSource;
|
||||||
|
turnNumber: number;
|
||||||
|
fortAchieved: boolean;
|
||||||
|
quickAchieved: boolean;
|
||||||
|
}): ThirdBattlePriorityReturnDialogue {
|
||||||
|
const pursuitLine = options.quickAchieved
|
||||||
|
? '유비: 전령을 쫓는 속도는 늦지 않았지만, 요새를 비워 둔 탓에 다음 길이 불안해졌소.'
|
||||||
|
: '유비: 전령을 쫓는 데도 시간이 걸렸고 요새까지 남겼소. 다음에는 길목부터 확실히 끊어야 하오.';
|
||||||
|
return baseDialogue({
|
||||||
|
...options,
|
||||||
|
targetDialogueId:
|
||||||
|
thirdBattleReturnTargetDialogueIds.fortRecovery,
|
||||||
|
variantId: 'fort-recovery',
|
||||||
|
title: '남겨 둔 강가 요새',
|
||||||
|
summary:
|
||||||
|
`강가 요새 미확보 · ${quickResultText(
|
||||||
|
options.quickAchieved,
|
||||||
|
options.turnNumber
|
||||||
|
)}`,
|
||||||
|
lines: [
|
||||||
|
`관우: ${options.turnNumber}턴 만에 전령 마원은 막았으나 강가 요새를 끝내 확보하지 못했습니다.`,
|
||||||
|
pursuitLine,
|
||||||
|
'관우: 다음 본영전에서는 먼저 물러설 자리를 지우고, 그 뒤에 적장을 몰아붙이겠습니다.'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTempoRecoveryDialogue(options: {
|
||||||
|
reportSource: ThirdBattleReturnReportSource;
|
||||||
|
turnNumber: number;
|
||||||
|
fortAchieved: boolean;
|
||||||
|
quickAchieved: boolean;
|
||||||
|
}): ThirdBattlePriorityReturnDialogue {
|
||||||
|
return baseDialogue({
|
||||||
|
...options,
|
||||||
|
targetDialogueId:
|
||||||
|
thirdBattleReturnTargetDialogueIds.tempoRecovery,
|
||||||
|
variantId: 'tempo-recovery',
|
||||||
|
title: '늦어진 전령 추격',
|
||||||
|
summary:
|
||||||
|
`강가 요새 확보 · ${quickResultText(
|
||||||
|
options.quickAchieved,
|
||||||
|
options.turnNumber
|
||||||
|
)}`,
|
||||||
|
lines: [
|
||||||
|
`장비: 강가 요새는 지켰지만 마원을 꺾는 데 ${options.turnNumber}턴이 걸려, 추격의 때를 놓쳤습니다.`,
|
||||||
|
'유비: 지킬 곳을 지킨 것은 옳소. 이제 전열을 세운 뒤 곧바로 익덕의 창이 이어지게 호흡을 줄이면 되오.',
|
||||||
|
'장비: 다음 본영전에서는 형님이 길을 잡는 순간 제가 바로 돌파하겠습니다.'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDecisiveAdvanceDialogue(options: {
|
||||||
|
reportSource: ThirdBattleReturnReportSource;
|
||||||
|
turnNumber: number;
|
||||||
|
fortAchieved: boolean;
|
||||||
|
quickAchieved: boolean;
|
||||||
|
}): ThirdBattlePriorityReturnDialogue {
|
||||||
|
return baseDialogue({
|
||||||
|
...options,
|
||||||
|
targetDialogueId:
|
||||||
|
thirdBattleReturnTargetDialogueIds.decisiveAdvance,
|
||||||
|
variantId: 'decisive-advance',
|
||||||
|
title: '본영으로 잇는 승리',
|
||||||
|
summary:
|
||||||
|
`강가 요새 확보 · ${quickResultText(
|
||||||
|
options.quickAchieved,
|
||||||
|
options.turnNumber
|
||||||
|
)}`,
|
||||||
|
lines: [
|
||||||
|
`관우: ${options.turnNumber}턴 만에 전령 마원을 꺾고 강가 요새까지 확보했습니다. 광종 본영으로 가는 길이 열렸습니다.`,
|
||||||
|
'장비: 지킬 곳도 지키고 쫓을 놈도 놓치지 않았으니, 이제 장각의 본영까지 이 기세를 잇읍시다.',
|
||||||
|
'관우: 좋다. 이번 성과를 자랑으로만 남기지 말고 본영 진입의 군율로 삼자.'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseDialogue(options: {
|
||||||
|
reportSource: ThirdBattleReturnReportSource;
|
||||||
|
targetDialogueId: ThirdBattleReturnTargetDialogueId;
|
||||||
|
variantId: ThirdBattleReturnDialogueVariantId;
|
||||||
|
turnNumber: number;
|
||||||
|
fortAchieved: boolean;
|
||||||
|
quickAchieved: boolean;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
lines: [string, string, string];
|
||||||
|
}): ThirdBattlePriorityReturnDialogue {
|
||||||
|
return {
|
||||||
|
sourceBattleId: thirdBattleReturnSourceBattleId,
|
||||||
|
campStep: thirdBattleReturnCampStep,
|
||||||
|
targetDialogueId: options.targetDialogueId,
|
||||||
|
priorityLabel: thirdBattleReturnPriorityLabel,
|
||||||
|
reportSource: options.reportSource,
|
||||||
|
variantId: options.variantId,
|
||||||
|
turnNumber: options.turnNumber,
|
||||||
|
fortAchieved: options.fortAchieved,
|
||||||
|
quickAchieved: options.quickAchieved,
|
||||||
|
title: options.title,
|
||||||
|
summary: options.summary,
|
||||||
|
lines: [...options.lines]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickResultText(
|
||||||
|
quickAchieved: boolean,
|
||||||
|
turnNumber: number
|
||||||
|
) {
|
||||||
|
return quickAchieved
|
||||||
|
? `${turnNumber}턴 신속 승리`
|
||||||
|
: `${turnNumber}턴 · ${thirdBattleReturnQuickVictoryTurnLimit}턴 제한 초과`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveInteger(value: unknown) {
|
||||||
|
return (
|
||||||
|
typeof value === 'number' &&
|
||||||
|
Number.isInteger(value) &&
|
||||||
|
value > 0 &&
|
||||||
|
value <= 9999
|
||||||
|
)
|
||||||
|
? value
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!Array.isArray(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -44,6 +44,11 @@ import {
|
|||||||
secondBattleReliefSourceBattleId,
|
secondBattleReliefSourceBattleId,
|
||||||
secondBattleReliefVisitId
|
secondBattleReliefVisitId
|
||||||
} from '../data/secondBattleReliefExploration';
|
} from '../data/secondBattleReliefExploration';
|
||||||
|
import {
|
||||||
|
resolveThirdBattlePriorityReturnDialogue,
|
||||||
|
thirdBattleReturnCampStep,
|
||||||
|
thirdBattleReturnSourceBattleId
|
||||||
|
} from '../data/thirdBattleReturnDialogue';
|
||||||
import {
|
import {
|
||||||
findCityStayAfterBattle,
|
findCityStayAfterBattle,
|
||||||
type CityEquipmentOffer,
|
type CityEquipmentOffer,
|
||||||
@@ -11643,7 +11648,18 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
||||||
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
|
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
|
||||||
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments);
|
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments);
|
||||||
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id;
|
const availableCampDialogues = this.availableCampDialogues();
|
||||||
|
const priorityReturnDialogue = this.thirdBattlePriorityReturnDialogue();
|
||||||
|
const pendingPriorityReturnDialogueId =
|
||||||
|
priorityReturnDialogue &&
|
||||||
|
!this.completedCampDialogues().includes(priorityReturnDialogue.targetDialogueId) &&
|
||||||
|
availableCampDialogues.some((dialogue) => dialogue.id === priorityReturnDialogue.targetDialogueId)
|
||||||
|
? priorityReturnDialogue.targetDialogueId
|
||||||
|
: undefined;
|
||||||
|
this.selectedDialogueId =
|
||||||
|
pendingPriorityReturnDialogueId ??
|
||||||
|
availableCampDialogues[0]?.id ??
|
||||||
|
campDialogues[0].id;
|
||||||
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
|
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
|
||||||
const cityStay = this.currentCityStay();
|
const cityStay = this.currentCityStay();
|
||||||
if (cityStay) {
|
if (cityStay) {
|
||||||
@@ -12883,6 +12899,18 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return resolveFirstBattleCamaraderieMemory(this.campaign);
|
return resolveFirstBattleCamaraderieMemory(this.campaign);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private thirdBattlePriorityReturnDialogue() {
|
||||||
|
if (
|
||||||
|
!this.campaign ||
|
||||||
|
this.retrySortieBattleId ||
|
||||||
|
this.campaign.step !== thirdBattleReturnCampStep ||
|
||||||
|
this.currentCampBattleId() !== thirdBattleReturnSourceBattleId
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return resolveThirdBattlePriorityReturnDialogue(this.campaign);
|
||||||
|
}
|
||||||
|
|
||||||
private firstPursuitScoutMemoryForNextSortie() {
|
private firstPursuitScoutMemoryForNextSortie() {
|
||||||
const campaign = this.campaign;
|
const campaign = this.campaign;
|
||||||
const scenario = this.nextSortieScenario();
|
const scenario = this.nextSortieScenario();
|
||||||
@@ -12917,6 +12945,18 @@ export class CampScene extends Phaser.Scene {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private campDialogueWithThirdBattlePriorityReturn(dialogue: CampDialogue) {
|
||||||
|
const priorityReturnDialogue = this.thirdBattlePriorityReturnDialogue();
|
||||||
|
if (!priorityReturnDialogue || dialogue.id !== priorityReturnDialogue.targetDialogueId) {
|
||||||
|
return dialogue;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...dialogue,
|
||||||
|
title: priorityReturnDialogue.title,
|
||||||
|
lines: [...priorityReturnDialogue.lines]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private hasPendingFirstBattleCampFollowup() {
|
private hasPendingFirstBattleCampFollowup() {
|
||||||
const followup = this.firstBattleCampFollowup();
|
const followup = this.firstBattleCampFollowup();
|
||||||
return Boolean(
|
return Boolean(
|
||||||
@@ -12933,6 +12973,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasPendingThirdBattlePriorityReturn() {
|
||||||
|
const priorityReturnDialogue = this.thirdBattlePriorityReturnDialogue();
|
||||||
|
return Boolean(
|
||||||
|
priorityReturnDialogue &&
|
||||||
|
!this.completedCampDialogues().includes(priorityReturnDialogue.targetDialogueId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private availableCampDialogues(): CampDialogue[] {
|
private availableCampDialogues(): CampDialogue[] {
|
||||||
const battleId = this.currentCampBattleId();
|
const battleId = this.currentCampBattleId();
|
||||||
const battleDialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId));
|
const battleDialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId));
|
||||||
@@ -12940,7 +12988,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const available = dialogues.length > 0
|
const available = dialogues.length > 0
|
||||||
? dialogues
|
? dialogues
|
||||||
: campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(defaultBattleScenario.id));
|
: campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(defaultBattleScenario.id));
|
||||||
const resolved = available.map((dialogue) => this.campDialogueWithFirstBattleFollowup(dialogue));
|
const resolved = available
|
||||||
|
.map((dialogue) => this.campDialogueWithFirstBattleFollowup(dialogue))
|
||||||
|
.map((dialogue) => this.campDialogueWithThirdBattlePriorityReturn(dialogue));
|
||||||
const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
||||||
return camaraderieDialogue && !resolved.some((dialogue) => dialogue.id === camaraderieDialogue.id)
|
return camaraderieDialogue && !resolved.some((dialogue) => dialogue.id === camaraderieDialogue.id)
|
||||||
? [...resolved, camaraderieDialogue]
|
? [...resolved, camaraderieDialogue]
|
||||||
@@ -13936,6 +13986,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const active = this.activeTab === tab;
|
const active = this.activeTab === tab;
|
||||||
const pendingFirstBattleFollowup = tab === 'dialogue' && this.hasPendingFirstBattleCampFollowup();
|
const pendingFirstBattleFollowup = tab === 'dialogue' && this.hasPendingFirstBattleCampFollowup();
|
||||||
const pendingCamaraderieMemory = tab === 'dialogue' && this.hasPendingFirstBattleCamaraderieMemory();
|
const pendingCamaraderieMemory = tab === 'dialogue' && this.hasPendingFirstBattleCamaraderieMemory();
|
||||||
|
const pendingThirdBattleReturn = tab === 'dialogue' && this.hasPendingThirdBattlePriorityReturn();
|
||||||
const pendingCampVisit = tab === 'visit' && this.availableCampVisits().some(
|
const pendingCampVisit = tab === 'visit' && this.availableCampVisits().some(
|
||||||
(visit) => !this.completedCampVisits().includes(visit.id)
|
(visit) => !this.completedCampVisits().includes(visit.id)
|
||||||
);
|
);
|
||||||
@@ -13946,11 +13997,19 @@ export class CampScene extends Phaser.Scene {
|
|||||||
indicator.setVisible(active || hovered);
|
indicator.setVisible(active || hovered);
|
||||||
indicator.setAlpha(active ? 1 : 0.66);
|
indicator.setAlpha(active ? 1 : 0.66);
|
||||||
if (tab === 'dialogue' && newBadge) {
|
if (tab === 'dialogue' && newBadge) {
|
||||||
newBadge.setText(pendingFirstBattleFollowup ? '회고' : pendingCamaraderieMemory ? '전우' : '회고');
|
newBadge.setText(
|
||||||
|
pendingThirdBattleReturn
|
||||||
|
? '우선'
|
||||||
|
: pendingFirstBattleFollowup
|
||||||
|
? '회고'
|
||||||
|
: pendingCamaraderieMemory
|
||||||
|
? '전우'
|
||||||
|
: '회고'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
newBadge?.setVisible(Boolean(
|
newBadge?.setVisible(Boolean(
|
||||||
(tab === 'city' && this.hasPendingCityStayActions()) ||
|
(tab === 'city' && this.hasPendingCityStayActions()) ||
|
||||||
(tab === 'dialogue' && (pendingFirstBattleFollowup || pendingCamaraderieMemory)) ||
|
(tab === 'dialogue' && (pendingThirdBattleReturn || pendingFirstBattleFollowup || pendingCamaraderieMemory)) ||
|
||||||
pendingCampVisit ||
|
pendingCampVisit ||
|
||||||
(tab === 'supplies' && pendingCategories.has('supplies')) ||
|
(tab === 'supplies' && pendingCategories.has('supplies')) ||
|
||||||
(tab === 'equipment' && pendingCategories.has('equipment'))
|
(tab === 'equipment' && pendingCategories.has('equipment'))
|
||||||
@@ -19838,6 +19897,11 @@ export class CampScene extends Phaser.Scene {
|
|||||||
bg.setStrokeStyle(1, palette.gold, 0.4);
|
bg.setStrokeStyle(1, palette.gold, 0.4);
|
||||||
const availableDialogues = this.availableCampDialogues();
|
const availableDialogues = this.availableCampDialogues();
|
||||||
const completedDialogues = this.completedAvailableDialogues().length;
|
const completedDialogues = this.completedAvailableDialogues().length;
|
||||||
|
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||||
|
const priorityDialogueCompleted = Boolean(
|
||||||
|
thirdBattlePriorityReturn &&
|
||||||
|
this.completedCampDialogues().includes(thirdBattlePriorityReturn.targetDialogueId)
|
||||||
|
);
|
||||||
const availableVisits = this.availableCampVisits();
|
const availableVisits = this.availableCampVisits();
|
||||||
const completedVisits = this.completedAvailableVisits().length;
|
const completedVisits = this.completedAvailableVisits().length;
|
||||||
const selectedUnits = this.selectedSortieUnits();
|
const selectedUnits = this.selectedSortieUnits();
|
||||||
@@ -19852,8 +19916,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const sortieNote = this.nextSortieRule().note;
|
const sortieNote = this.nextSortieRule().note;
|
||||||
const recoveryHint = this.sortieRecoveryHint();
|
const recoveryHint = this.sortieRecoveryHint();
|
||||||
const hasBattle = Boolean(this.nextSortieScenario());
|
const hasBattle = Boolean(this.nextSortieScenario());
|
||||||
|
const dialogueProgress = thirdBattlePriorityReturn
|
||||||
|
? `대화 우선 ${priorityDialogueCompleted ? 1 : 0}/1 · 선택 ${Math.max(
|
||||||
|
0,
|
||||||
|
completedDialogues - Number(priorityDialogueCompleted)
|
||||||
|
)}/${Math.max(0, availableDialogues.length - 1)}`
|
||||||
|
: `대화 ${completedDialogues}/${availableDialogues.length}`;
|
||||||
const readyLine = [
|
const readyLine = [
|
||||||
`대화 ${completedDialogues}/${availableDialogues.length}`,
|
dialogueProgress,
|
||||||
`방문 ${completedVisits}/${availableVisits.length}`,
|
`방문 ${completedVisits}/${availableVisits.length}`,
|
||||||
`${hasBattle ? '출전' : '동행'} ${selectedSummary}`,
|
`${hasBattle ? '출전' : '동행'} ${selectedSummary}`,
|
||||||
`보유 ${inventorySummary}`
|
`보유 ${inventorySummary}`
|
||||||
@@ -20111,6 +20181,20 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
|
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
|
||||||
const availableDialogues = this.availableCampDialogues();
|
const availableDialogues = this.availableCampDialogues();
|
||||||
const completedDialogues = this.completedAvailableDialogues().length;
|
const completedDialogues = this.completedAvailableDialogues().length;
|
||||||
|
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||||
|
const priorityDialogueCompleted = Boolean(
|
||||||
|
thirdBattlePriorityReturn &&
|
||||||
|
this.completedCampDialogues().includes(thirdBattlePriorityReturn.targetDialogueId)
|
||||||
|
);
|
||||||
|
const dialogueChecklistComplete = thirdBattlePriorityReturn
|
||||||
|
? priorityDialogueCompleted
|
||||||
|
: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length;
|
||||||
|
const dialogueChecklistDetail = thirdBattlePriorityReturn
|
||||||
|
? `우선 ${priorityDialogueCompleted ? 1 : 0}/1 · 선택 ${Math.max(
|
||||||
|
0,
|
||||||
|
completedDialogues - Number(priorityDialogueCompleted)
|
||||||
|
)}/${Math.max(0, availableDialogues.length - 1)}`
|
||||||
|
: `${completedDialogues}/${availableDialogues.length} 완료`;
|
||||||
const availableVisits = this.availableCampVisits();
|
const availableVisits = this.availableCampVisits();
|
||||||
const completedVisits = this.completedAvailableVisits().length;
|
const completedVisits = this.completedAvailableVisits().length;
|
||||||
const selected = this.selectedSortieUnits();
|
const selected = this.selectedSortieUnits();
|
||||||
@@ -20176,8 +20260,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '공명 대화',
|
label: '공명 대화',
|
||||||
complete: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length,
|
complete: dialogueChecklistComplete,
|
||||||
detail: `${completedDialogues}/${availableDialogues.length} 완료`,
|
detail: dialogueChecklistDetail,
|
||||||
priority: 'recommended'
|
priority: 'recommended'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -22740,6 +22824,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const dialogues = this.availableCampDialogues();
|
const dialogues = this.availableCampDialogues();
|
||||||
const firstBattleFollowup = this.firstBattleCampFollowup();
|
const firstBattleFollowup = this.firstBattleCampFollowup();
|
||||||
const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
||||||
|
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||||
const compactDialogueList = dialogues.length > 3;
|
const compactDialogueList = dialogues.length > 3;
|
||||||
const dialogueRowGap = compactDialogueList ? 35 : 64;
|
const dialogueRowGap = compactDialogueList ? 35 : 64;
|
||||||
const dialogueRowHeight = compactDialogueList ? 30 : 48;
|
const dialogueRowHeight = compactDialogueList ? 30 : 48;
|
||||||
@@ -22764,12 +22849,22 @@ export class CampScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
const isFirstBattleFollowup = firstBattleFollowup?.targetDialogueId === dialogue.id;
|
const isFirstBattleFollowup = firstBattleFollowup?.targetDialogueId === dialogue.id;
|
||||||
const isCamaraderieMemory = camaraderieDialogue?.id === dialogue.id;
|
const isCamaraderieMemory = camaraderieDialogue?.id === dialogue.id;
|
||||||
const title = isFirstBattleFollowup || isCamaraderieMemory
|
const isThirdBattlePriorityReturn =
|
||||||
|
thirdBattlePriorityReturn?.targetDialogueId === dialogue.id;
|
||||||
|
const pendingThirdBattlePriorityReturn =
|
||||||
|
isThirdBattlePriorityReturn && !completed;
|
||||||
|
const title = isFirstBattleFollowup || isCamaraderieMemory || isThirdBattlePriorityReturn
|
||||||
? this.compactText(dialogue.title, compactDialogueList ? 20 : 14)
|
? this.compactText(dialogue.title, compactDialogueList ? 20 : 14)
|
||||||
: dialogue.title;
|
: dialogue.title;
|
||||||
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 3 : 8), title, this.textStyle(compactDialogueList ? 11 : 15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 3 : 8), title, this.textStyle(compactDialogueList ? 11 : 15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
||||||
if (isFirstBattleFollowup || isCamaraderieMemory) {
|
if (isFirstBattleFollowup || isCamaraderieMemory || pendingThirdBattlePriorityReturn) {
|
||||||
const badgeLabel = completed ? '완료' : isCamaraderieMemory ? '전우 기억' : '회고';
|
const badgeLabel = pendingThirdBattlePriorityReturn
|
||||||
|
? thirdBattlePriorityReturn?.priorityLabel ?? '새 이야기 · 우선'
|
||||||
|
: completed
|
||||||
|
? '완료'
|
||||||
|
: isCamaraderieMemory
|
||||||
|
? '전우 기억'
|
||||||
|
: '회고';
|
||||||
this.track(
|
this.track(
|
||||||
this.add.text(
|
this.add.text(
|
||||||
x + 320,
|
x + 320,
|
||||||
@@ -22777,7 +22872,13 @@ export class CampScene extends Phaser.Scene {
|
|||||||
badgeLabel,
|
badgeLabel,
|
||||||
{
|
{
|
||||||
...this.textStyle(compactDialogueList ? 8 : 9, completed ? '#a8ffd0' : '#fff2b8', true),
|
...this.textStyle(compactDialogueList ? 8 : 9, completed ? '#a8ffd0' : '#fff2b8', true),
|
||||||
backgroundColor: completed ? '#21402d' : isCamaraderieMemory ? '#36507a' : '#8a3f25',
|
backgroundColor: completed
|
||||||
|
? '#21402d'
|
||||||
|
: pendingThirdBattlePriorityReturn
|
||||||
|
? '#8a3f25'
|
||||||
|
: isCamaraderieMemory
|
||||||
|
? '#36507a'
|
||||||
|
: '#8a3f25',
|
||||||
padding: { x: 4, y: 2 }
|
padding: { x: 4, y: 2 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -22803,12 +22904,25 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const completed = this.completedCampDialogues().includes(dialogue.id);
|
const completed = this.completedCampDialogues().includes(dialogue.id);
|
||||||
|
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||||
|
const isThirdBattlePriorityReturn =
|
||||||
|
thirdBattlePriorityReturn?.targetDialogueId === dialogue.id;
|
||||||
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.5);
|
bg.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.5);
|
||||||
this.track(this.add.text(x + 18, y + 16, dialogue.title, this.textStyle(20, '#f2e3bf', true)));
|
this.track(this.add.text(x + 18, y + 16, dialogue.title, this.textStyle(20, '#f2e3bf', true)));
|
||||||
|
if (isThirdBattlePriorityReturn) {
|
||||||
|
this.track(
|
||||||
|
this.add.text(
|
||||||
|
x + 18,
|
||||||
|
y + 45,
|
||||||
|
`전투 기록 · ${thirdBattlePriorityReturn.summary}`,
|
||||||
|
this.textStyle(11, completed ? '#a8ffd0' : '#ffdf7b', true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
const dialogueBody = this.track(
|
const dialogueBody = this.track(
|
||||||
this.add.text(x + 18, y + 52, dialogue.lines.join('\n'), {
|
this.add.text(x + 18, y + (isThirdBattlePriorityReturn ? 70 : 52), dialogue.lines.join('\n'), {
|
||||||
...this.textStyle(14, '#d4dce6'),
|
...this.textStyle(14, '#d4dce6'),
|
||||||
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
||||||
lineSpacing: 3
|
lineSpacing: 3
|
||||||
@@ -24152,11 +24266,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
||||||
const followup = this.firstBattleCampFollowup();
|
const followup = this.firstBattleCampFollowup();
|
||||||
const camaraderieMemory = this.firstBattleCamaraderieMemory();
|
const camaraderieMemory = this.firstBattleCamaraderieMemory();
|
||||||
|
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||||
const preparationLine = camaraderieMemory?.dialogue.id === dialogue.id
|
const preparationLine = camaraderieMemory?.dialogue.id === dialogue.id
|
||||||
? `\n다음 준비 · ${camaraderieMemory.unitNames.join('↔')}를 직접 핵심 공명조로 지정하면 추격 +${camaraderieMemory.bonusRate}%p · 자동 지정 없음`
|
? `\n다음 준비 · ${camaraderieMemory.unitNames.join('↔')}를 직접 핵심 공명조로 지정하면 추격 +${camaraderieMemory.bonusRate}%p · 자동 지정 없음`
|
||||||
: followup?.targetDialogueId === dialogue.id
|
: followup?.targetDialogueId === dialogue.id
|
||||||
? `\n다음 준비 · ${followup.recommendationTitle}`
|
? `\n다음 준비 · ${followup.recommendationTitle}`
|
||||||
: '';
|
: thirdBattlePriorityReturn?.targetDialogueId === dialogue.id
|
||||||
|
? '\n귀환 회고 완료 · 나머지 동료 대화는 자유 선택'
|
||||||
|
: '';
|
||||||
this.showCampNotice(`획득 내역 · ${choice.label} · 공명 +${rewardExp}\n응답 · ${choice.response}${preparationLine}`);
|
this.showCampNotice(`획득 내역 · ${choice.label} · 공명 +${rewardExp}\n응답 · ${choice.response}${preparationLine}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25155,6 +25272,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const firstBattleCampFollowup = this.firstBattleCampFollowup();
|
const firstBattleCampFollowup = this.firstBattleCampFollowup();
|
||||||
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
|
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
|
||||||
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
||||||
|
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||||
const firstPursuitScoutVisit = this.availableCampVisits().find(
|
const firstPursuitScoutVisit = this.availableCampVisits().find(
|
||||||
(visit) => visit.id === firstPursuitScoutVisitId
|
(visit) => visit.id === firstPursuitScoutVisitId
|
||||||
);
|
);
|
||||||
@@ -25326,6 +25444,32 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
thirdBattlePriorityReturn: thirdBattlePriorityReturn
|
||||||
|
? {
|
||||||
|
available: true,
|
||||||
|
sourceBattleId: thirdBattlePriorityReturn.sourceBattleId,
|
||||||
|
sourceTurn: thirdBattlePriorityReturn.turnNumber,
|
||||||
|
reportSource: thirdBattlePriorityReturn.reportSource,
|
||||||
|
fortAchieved: thirdBattlePriorityReturn.fortAchieved,
|
||||||
|
quickAchieved: thirdBattlePriorityReturn.quickAchieved,
|
||||||
|
variant: thirdBattlePriorityReturn.variantId,
|
||||||
|
targetDialogueId: thirdBattlePriorityReturn.targetDialogueId,
|
||||||
|
priorityLabel: thirdBattlePriorityReturn.priorityLabel,
|
||||||
|
summary: thirdBattlePriorityReturn.summary,
|
||||||
|
dialogueTitle: thirdBattlePriorityReturn.title,
|
||||||
|
dialogueLines: [...thirdBattlePriorityReturn.lines],
|
||||||
|
selected: this.selectedDialogueId === thirdBattlePriorityReturn.targetDialogueId,
|
||||||
|
completed: this.completedCampDialogues().includes(
|
||||||
|
thirdBattlePriorityReturn.targetDialogueId
|
||||||
|
),
|
||||||
|
pending: !this.completedCampDialogues().includes(
|
||||||
|
thirdBattlePriorityReturn.targetDialogueId
|
||||||
|
),
|
||||||
|
rowBounds: this.sortieObjectBoundsDebug(
|
||||||
|
this.campDialogueRowButtons[thirdBattlePriorityReturn.targetDialogueId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: null,
|
||||||
firstPursuitScoutMemory: {
|
firstPursuitScoutMemory: {
|
||||||
visitId: firstPursuitScoutVisitId,
|
visitId: firstPursuitScoutVisitId,
|
||||||
available: Boolean(firstPursuitScoutVisit),
|
available: Boolean(firstPursuitScoutVisit),
|
||||||
@@ -25396,6 +25540,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
completed: this.completedCampDialogues().includes(selectedCampDialogue.id),
|
completed: this.completedCampDialogues().includes(selectedCampDialogue.id),
|
||||||
firstVictoryFollowup: firstBattleCampFollowup?.targetDialogueId === selectedCampDialogue.id,
|
firstVictoryFollowup: firstBattleCampFollowup?.targetDialogueId === selectedCampDialogue.id,
|
||||||
camaraderieMemory: firstBattleCamaraderieMemory?.dialogue.id === selectedCampDialogue.id,
|
camaraderieMemory: firstBattleCamaraderieMemory?.dialogue.id === selectedCampDialogue.id,
|
||||||
|
thirdBattlePriorityReturn:
|
||||||
|
thirdBattlePriorityReturn?.targetDialogueId === selectedCampDialogue.id,
|
||||||
rowBounds: this.sortieObjectBoundsDebug(this.campDialogueRowButtons[selectedCampDialogue.id]),
|
rowBounds: this.sortieObjectBoundsDebug(this.campDialogueRowButtons[selectedCampDialogue.id]),
|
||||||
bodyBounds: this.sortieTextBoundsDebug(this.campDialogueBodyText),
|
bodyBounds: this.sortieTextBoundsDebug(this.campDialogueBodyText),
|
||||||
choices: selectedCampDialogue.choices.map((choice) => ({
|
choices: selectedCampDialogue.choices.map((choice) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user