import { resolveThirdBattlePriorityReturnDialogue, thirdBattleReturnSourceBattleId } from './thirdBattleReturnDialogue'; import { completedThirdCampExplorationActivityIds, thirdCampExplorationVisitId, type ThirdCampExplorationActivityId } from './thirdCampExploration'; export const thirdCampPreparationSourceBattleId = thirdBattleReturnSourceBattleId; export const thirdCampPreparationTargetBattleId = 'fourth-battle-guangzong-camp'; export const thirdCampPreparationSelectionRecordId = 'third-camp-preparation-priority'; export const thirdCampPreparationActiveThroughTurn = 3; export const thirdCampPreparationInformationEnemyUnitId = 'guangzong-main-vanguard-a'; export const thirdCampPreparationPriorityIds = [ 'information', 'equipment', 'companion' ] as const; export type ThirdCampPreparationPriorityId = (typeof thirdCampPreparationPriorityIds)[number]; export type ThirdCampPreparationInformationEffect = { kind: 'marked-vanguard-intel'; activeThroughTurn: typeof thirdCampPreparationActiveThroughTurn; deploymentPreviewCount: 1; trackedEnemyUnitId: typeof thirdCampPreparationInformationEnemyUnitId; accuracyBonus: 10; maxBattleTriggers: 1; }; export type ThirdCampPreparationEquipmentEffect = { kind: 'prepared-equipment-guard'; activeThroughTurn: typeof thirdCampPreparationActiveThroughTurn; incomingDamageReductionPercent: 8; minimumDamage: 1; maxBattleTriggers: 1; targetRule: 'first-allied-hit'; }; export type ThirdCampPreparationCompanionEffect = { kind: 'return-bond-opening'; activeThroughTurn: typeof thirdCampPreparationActiveThroughTurn; cooperationRateBonus: 5; }; export type ThirdCampPreparationEffect = | ThirdCampPreparationInformationEffect | ThirdCampPreparationEquipmentEffect | ThirdCampPreparationCompanionEffect; export type ThirdCampPreparationPriorityDefinition = { id: ThirdCampPreparationPriorityId; label: string; activityLabel: string; summary: string; deploymentLine: string; activeLine: string; appliedLine: string; unusedLine: string; effect: ThirdCampPreparationEffect; }; export const thirdCampPreparationPriorities: readonly ThirdCampPreparationPriorityDefinition[] = [ { id: 'information', label: '본영 정보', activityLabel: '승전 보고 확인', summary: '광종 본영 선봉 한 명의 첫 행동을 배치 때 확인하고, 3턴 안 첫 공격 명중을 10%p 높입니다.', deploymentLine: '본영 정보 · 선봉 적 1명의 의도와 표식을 배치 단계에서 먼저 확인', activeLine: '본영 정보 · 첫 3턴 안 표식 대상 첫 공격 명중 +10%p', appliedLine: '본영 정보 · 표식 대상 첫 공격 명중 보정 적용', unusedLine: '본영 정보 · 표식 대상을 공격하기 전에 준비 시간이 끝남', effect: { kind: 'marked-vanguard-intel', activeThroughTurn: thirdCampPreparationActiveThroughTurn, deploymentPreviewCount: 1, trackedEnemyUnitId: thirdCampPreparationInformationEnemyUnitId, accuracyBonus: 10, maxBattleTriggers: 1 } }, { id: 'equipment', label: '선봉 방호', activityLabel: '승전 장비 확인', summary: '수선한 방호구로 첫 3턴 아군의 첫 피격 피해를 한 번 8% 줄입니다.', deploymentLine: '선봉 방호 · 아군 첫 피격 1회 방호 준비', activeLine: '선봉 방호 · 첫 3턴 동안 아군 첫 피격 1회 피해 -8%', appliedLine: '선봉 방호 · 아군 첫 피격 방호 적용', unusedLine: '선봉 방호 · 방호가 발동하기 전에 준비 시간이 끝남', effect: { kind: 'prepared-equipment-guard', activeThroughTurn: thirdCampPreparationActiveThroughTurn, incomingDamageReductionPercent: 8, minimumDamage: 1, maxBattleTriggers: 1, targetRule: 'first-allied-hit' } }, { id: 'companion', label: '동료 공명', activityLabel: '우선 귀환 대화', summary: '우선 귀환 대화를 나눈 공명조는 첫 3턴 협공 확률이 5%p 오릅니다.', deploymentLine: '동료 공명 · 귀환 대화 공명조의 초반 협공 신호 준비', activeLine: '동료 공명 · 첫 3턴 동안 해당 공명조 협공 확률 +5%p', appliedLine: '동료 공명 · 귀환 대화에서 맞춘 협공 신호 발동', unusedLine: '동료 공명 · 해당 공명조가 초반 협공을 잇지 못함', effect: { kind: 'return-bond-opening', activeThroughTurn: thirdCampPreparationActiveThroughTurn, cooperationRateBonus: 5 } } ] as const; export type ThirdCampPreparationEvidence = | { kind: 'exploration-activity'; battleId: typeof thirdCampPreparationSourceBattleId; visitId: typeof thirdCampExplorationVisitId; activityId: Exclude< ThirdCampExplorationActivityId, 'companion' >; } | { kind: 'exploration-activity'; battleId: typeof thirdCampPreparationSourceBattleId; visitId: typeof thirdCampExplorationVisitId; activityId: 'companion'; dialogueId: string; bondId: string; unitIds: [string, string]; } | { kind: 'victory-reward-category'; battleId: typeof thirdCampPreparationSourceBattleId; category: 'unlocks' | 'equipment'; } | { kind: 'priority-return-dialogue'; battleId: typeof thirdCampPreparationSourceBattleId; dialogueId: string; bondId: string; unitIds: [string, string]; }; export type ThirdCampPreparationUnavailableReason = | 'battle-record-required' | 'report-review-required' | 'equipment-change-required' | 'priority-dialogue-required'; export type ThirdCampPreparationAvailability = { priority: ThirdCampPreparationPriorityDefinition; selectable: boolean; evidence?: ThirdCampPreparationEvidence; unavailableReason?: ThirdCampPreparationUnavailableReason; }; export type ResolvedThirdCampPreparationEffect = | ThirdCampPreparationInformationEffect | ThirdCampPreparationEquipmentEffect | (ThirdCampPreparationCompanionEffect & { bondId: string; unitIds: [string, string]; }); export type ThirdCampPreparationMemory = { sourceBattleId: typeof thirdCampPreparationSourceBattleId; targetBattleId: typeof thirdCampPreparationTargetBattleId; selectionRecordId: typeof thirdCampPreparationSelectionRecordId; priorityId: ThirdCampPreparationPriorityId; label: string; summary: string; deploymentLine: string; activeLine: string; appliedLine: string; unusedLine: string; evidence: ThirdCampPreparationEvidence; effect: ResolvedThirdCampPreparationEffect; }; export type ThirdCampPreparationBattlePresentation = { selectionRecordId: typeof thirdCampPreparationSelectionRecordId; priorityId: ThirdCampPreparationPriorityId; label: string; status: | 'deployment' | 'active' | 'expired' | 'applied' | 'unused' | 'incompatible'; text: string; activeThroughTurn: typeof thirdCampPreparationActiveThroughTurn; turnNumber: number | null; usageCount: number; }; export type ThirdCampPreparationCompatibility = { compatible: boolean; requiredUnitIds: string[]; missingUnitIds: string[]; disabledReason: 'required-units-missing' | null; }; export type ThirdCampPreparationCampaignState = { step?: unknown; latestBattleId?: unknown; battleHistory?: Readonly< Record >; firstBattleReport?: ThirdCampPreparationBattleRecord | null; completedCampDialogues?: readonly unknown[]; completedCampVisits?: readonly unknown[]; thirdCampPreparationSelection?: { sourceBattleId?: unknown; targetBattleId?: unknown; priorityId?: unknown; confirmed?: unknown; } | null; acknowledgedVictoryRewardCategories?: Readonly< Record >; }; type ThirdCampPreparationBattleRecord = { battleId?: unknown; outcome?: unknown; turnNumber?: unknown; objectives?: unknown; units?: unknown; }; type ThirdBattleRecordSelection = { record: ThirdCampPreparationBattleRecord; source: 'battle-history' | 'legacy-report'; }; const companionBondByDialogueId: Readonly< Record > = { 'liu-guan-after-guangzong-road': { bondId: 'liu-bei__guan-yu', unitIds: ['liu-bei', 'guan-yu'] }, 'liu-zhang-after-guangzong-road': { bondId: 'liu-bei__zhang-fei', unitIds: ['liu-bei', 'zhang-fei'] }, 'guan-zhang-after-guangzong-road': { bondId: 'guan-yu__zhang-fei', unitIds: ['guan-yu', 'zhang-fei'] } }; export function isThirdCampPreparationPriorityId( value: unknown ): value is ThirdCampPreparationPriorityId { return ( typeof value === 'string' && (thirdCampPreparationPriorityIds as readonly string[]).includes( value ) ); } export function getThirdCampPreparationPriority( priorityId: unknown ): ThirdCampPreparationPriorityDefinition | undefined { const definition = isThirdCampPreparationPriorityId(priorityId) ? thirdCampPreparationPriorities.find( (candidate) => candidate.id === priorityId ) : undefined; return definition ? clonePriority(definition) : undefined; } export function hasThirdCampPreparationSourceVictory( campaign?: ThirdCampPreparationCampaignState | null ) { return Boolean(selectThirdBattleVictoryRecord(campaign)); } export function resolveThirdCampPreparationAvailability( campaign?: ThirdCampPreparationCampaignState | null ): ThirdCampPreparationAvailability[] { const source = selectThirdBattleVictoryRecord(campaign); if (!source) { return thirdCampPreparationPriorities.map((priority) => ({ priority: clonePriority(priority), selectable: false, unavailableReason: 'battle-record-required' })); } const informationEvidence = completedExplorationEvidence(campaign, 'information') ?? selectedLegacyRewardEvidence(campaign, 'information', 'unlocks'); const equipmentEvidence = completedExplorationEvidence(campaign, 'equipment') ?? selectedLegacyRewardEvidence( campaign, 'equipment', 'equipment' ); const companionEvidence = completedExplorationEvidence(campaign, 'companion') ?? completedCompanionEvidence(campaign); const evidenceByPriority: Partial< Record > = { ...(informationEvidence ? { information: informationEvidence } : {}), ...(equipmentEvidence ? { equipment: equipmentEvidence } : {}), ...(companionEvidence ? { companion: companionEvidence } : {}) }; const missingReasonByPriority: Record< ThirdCampPreparationPriorityId, ThirdCampPreparationUnavailableReason > = { information: 'report-review-required', equipment: 'equipment-change-required', companion: 'priority-dialogue-required' }; return thirdCampPreparationPriorities.map((priority) => { const evidence = evidenceByPriority[priority.id]; return { priority: clonePriority(priority), selectable: Boolean(evidence), ...(evidence ? { evidence: cloneEvidence(evidence) } : { unavailableReason: missingReasonByPriority[priority.id] }) }; }); } export function resolveThirdCampPreparationSelection( campaign?: ThirdCampPreparationCampaignState | null ) { const selection = campaign?.thirdCampPreparationSelection; if ( selection?.sourceBattleId !== thirdCampPreparationSourceBattleId || selection.targetBattleId !== thirdCampPreparationTargetBattleId ) { return undefined; } const priorityId = selection.priorityId; if (!isThirdCampPreparationPriorityId(priorityId)) { return undefined; } const availability = resolveThirdCampPreparationAvailability( campaign ).find((entry) => entry.priority.id === priorityId); return availability ? { priorityId, label: availability.priority.label, selectable: availability.selectable, confirmed: selection.confirmed === true, ...(availability.evidence ? { evidence: cloneEvidence(availability.evidence) } : {}), ...(availability.unavailableReason ? { unavailableReason: availability.unavailableReason } : {}) } : undefined; } export function shouldReviewThirdCampPreparationBeforeRetry( campaign?: ThirdCampPreparationCampaignState | null ) { const selection = resolveThirdCampPreparationSelection(campaign); if ( campaign?.step !== 'fourth-battle' || !selection?.selectable || selection.confirmed ) { return false; } const targetSettlement = campaign.battleHistory?.[ thirdCampPreparationTargetBattleId ]; return ( targetSettlement?.outcome === 'defeat' || (campaign.firstBattleReport?.battleId === thirdCampPreparationTargetBattleId && campaign.firstBattleReport.outcome === 'defeat') ); } export function resolveThirdCampPreparationMemory(options: { campaign?: ThirdCampPreparationCampaignState | null; battleId?: string; }): ThirdCampPreparationMemory | undefined { return resolveThirdCampPreparationMemoryCandidate(options); } export function resolveThirdCampPreparationBattleResumeMemory( options: { campaign?: ThirdCampPreparationCampaignState | null; battleId?: string; savedPriorityId?: unknown; } ): ThirdCampPreparationMemory | undefined { if (!isThirdCampPreparationPriorityId(options.savedPriorityId)) { return undefined; } return resolveThirdCampPreparationMemoryCandidate( options, options.savedPriorityId ); } function resolveThirdCampPreparationMemoryCandidate( options: { campaign?: ThirdCampPreparationCampaignState | null; battleId?: string; }, resumedPriorityId?: ThirdCampPreparationPriorityId ): ThirdCampPreparationMemory | undefined { if (options.battleId !== thirdCampPreparationTargetBattleId) { return undefined; } const selection = resolveThirdCampPreparationSelection( options.campaign ); if ( !selection?.selectable || !selection.evidence || (!selection.confirmed && resumedPriorityId !== selection.priorityId) ) { return undefined; } const priority = getThirdCampPreparationPriority( selection.priorityId ); if (!priority) { return undefined; } const effect = resolveEffect(priority.effect, selection.evidence); if (!effect) { return undefined; } return { sourceBattleId: thirdCampPreparationSourceBattleId, targetBattleId: thirdCampPreparationTargetBattleId, selectionRecordId: thirdCampPreparationSelectionRecordId, priorityId: priority.id, label: priority.label, summary: priority.summary, deploymentLine: priority.deploymentLine, activeLine: priority.activeLine, appliedLine: priority.appliedLine, unusedLine: priority.unusedLine, evidence: cloneEvidence(selection.evidence), effect }; } export function resolveThirdCampPreparationBattlePresentation(options: { memory: ThirdCampPreparationMemory; stage: 'deployment' | 'turn' | 'result'; turnNumber?: number; usageCount?: number; compatibility?: ThirdCampPreparationCompatibility; }): ThirdCampPreparationBattlePresentation | undefined { const priority = getThirdCampPreparationPriority( options.memory.priorityId ); if ( !priority || priority.label !== options.memory.label || options.memory.targetBattleId !== thirdCampPreparationTargetBattleId ) { return undefined; } const usageCount = nonNegativeInteger(options.usageCount); if (options.stage === 'deployment') { if (options.compatibility?.compatible === false) { return presentationBase( options.memory, 'incompatible', `${options.memory.label} · 필요한 공명조가 편성되지 않아 이번 전투에는 미적용`, null, usageCount ); } return presentationBase( options.memory, 'deployment', options.memory.deploymentLine, null, usageCount ); } if (options.stage === 'turn') { const turnNumber = positiveInteger(options.turnNumber); if (!turnNumber) { return undefined; } if (turnNumber > thirdCampPreparationActiveThroughTurn) { return presentationBase( options.memory, 'expired', `${options.memory.label} · 준비 효과 종료`, turnNumber, usageCount ); } if (options.compatibility?.compatible === false) { return presentationBase( options.memory, 'incompatible', `${options.memory.label} · 필요한 공명조가 편성되지 않아 이번 전투에는 미적용`, turnNumber, usageCount ); } return presentationBase( options.memory, 'active', `${options.memory.activeLine} · ${turnNumber}/${thirdCampPreparationActiveThroughTurn}턴`, turnNumber, usageCount ); } if (options.compatibility?.compatible === false) { return presentationBase( options.memory, 'incompatible', `${options.memory.label} · 필요한 공명조가 편성되지 않아 이번 전투에는 미적용`, null, usageCount ); } const applied = usageCount > 0; return presentationBase( options.memory, applied ? 'applied' : 'unused', applied ? `${options.memory.appliedLine} · ${usageCount}회` : options.memory.unusedLine, null, usageCount ); } export function evaluateThirdCampPreparationCompatibility( memory: ThirdCampPreparationMemory | undefined, selectedUnitIds: readonly string[] ): ThirdCampPreparationCompatibility { const requiredUnitIds = memory?.effect.kind === 'return-bond-opening' ? [...memory.effect.unitIds] : []; const selected = new Set(selectedUnitIds); const missingUnitIds = requiredUnitIds.filter( (unitId) => !selected.has(unitId) ); return { compatible: missingUnitIds.length === 0, requiredUnitIds, missingUnitIds, disabledReason: missingUnitIds.length > 0 ? 'required-units-missing' : null }; } function selectThirdBattleVictoryRecord( campaign?: ThirdCampPreparationCampaignState | null ): ThirdBattleRecordSelection | undefined { const history = campaign?.battleHistory; if ( history && Object.prototype.hasOwnProperty.call( history, thirdCampPreparationSourceBattleId ) ) { const record = history[thirdCampPreparationSourceBattleId]; return validThirdBattleVictory(record) ? { record, source: 'battle-history' } : undefined; } const record = campaign?.firstBattleReport; return validThirdBattleVictory(record) ? { record, source: 'legacy-report' } : undefined; } function validThirdBattleVictory( record?: ThirdCampPreparationBattleRecord | null ): record is ThirdCampPreparationBattleRecord { return Boolean( record && record.battleId === thirdCampPreparationSourceBattleId && record.outcome === 'victory' ); } function acknowledgedRewardEvidence( campaign: ThirdCampPreparationCampaignState | null | undefined, category: 'unlocks' | 'equipment' ): Extract< ThirdCampPreparationEvidence, { kind: 'victory-reward-category' } > | undefined { const acknowledged = campaign?.acknowledgedVictoryRewardCategories?.[ thirdCampPreparationSourceBattleId ]; return Array.isArray(acknowledged) && acknowledged.includes(category) ? { kind: 'victory-reward-category', battleId: thirdCampPreparationSourceBattleId, category } : undefined; } function selectedLegacyRewardEvidence( campaign: ThirdCampPreparationCampaignState | null | undefined, priorityId: Extract< ThirdCampPreparationPriorityId, 'information' | 'equipment' >, category: 'unlocks' | 'equipment' ) { const selection = campaign?.thirdCampPreparationSelection; if ( selection?.sourceBattleId !== thirdCampPreparationSourceBattleId || selection.targetBattleId !== thirdCampPreparationTargetBattleId || selection.priorityId !== priorityId ) { return undefined; } return acknowledgedRewardEvidence(campaign, category); } function completedExplorationEvidence( campaign: ThirdCampPreparationCampaignState | null | undefined, activityId: ThirdCampExplorationActivityId ): Extract< ThirdCampPreparationEvidence, { kind: 'exploration-activity' } > | undefined { const completedActivityIds = completedThirdCampExplorationActivityIds( arrayValue(campaign?.completedCampVisits).filter( (visitId): visitId is string => typeof visitId === 'string' ) ); if (!completedActivityIds.includes(activityId)) { return undefined; } if (activityId !== 'companion') { return { kind: 'exploration-activity', battleId: thirdCampPreparationSourceBattleId, visitId: thirdCampExplorationVisitId, activityId }; } const dialogue = resolveThirdBattlePriorityReturnDialogue(campaign); const bond = dialogue ? companionBondByDialogueId[dialogue.targetDialogueId] : undefined; return dialogue && bond ? { kind: 'exploration-activity', battleId: thirdCampPreparationSourceBattleId, visitId: thirdCampExplorationVisitId, activityId: 'companion', dialogueId: dialogue.targetDialogueId, bondId: bond.bondId, unitIds: [...bond.unitIds] } : undefined; } function completedCompanionEvidence( campaign?: ThirdCampPreparationCampaignState | null ): Extract< ThirdCampPreparationEvidence, { kind: 'priority-return-dialogue' } > | undefined { const dialogue = resolveThirdBattlePriorityReturnDialogue(campaign); if ( !dialogue || !arrayValue(campaign?.completedCampDialogues).includes( dialogue.targetDialogueId ) ) { return undefined; } const bond = companionBondByDialogueId[dialogue.targetDialogueId]; return bond ? { kind: 'priority-return-dialogue', battleId: thirdCampPreparationSourceBattleId, dialogueId: dialogue.targetDialogueId, bondId: bond.bondId, unitIds: [...bond.unitIds] } : undefined; } function resolveEffect( effect: ThirdCampPreparationEffect, evidence: ThirdCampPreparationEvidence ): ResolvedThirdCampPreparationEffect | undefined { if ( effect.kind === 'marked-vanguard-intel' && ( (evidence.kind === 'exploration-activity' && evidence.activityId === 'information') || (evidence.kind === 'victory-reward-category' && evidence.category === 'unlocks') ) ) { return { ...effect }; } if ( effect.kind === 'prepared-equipment-guard' && ( (evidence.kind === 'exploration-activity' && evidence.activityId === 'equipment') || (evidence.kind === 'victory-reward-category' && evidence.category === 'equipment') ) ) { return { ...effect }; } if ( effect.kind === 'return-bond-opening' && ( evidence.kind === 'priority-return-dialogue' || (evidence.kind === 'exploration-activity' && evidence.activityId === 'companion') ) ) { return { ...effect, bondId: evidence.bondId, unitIds: [...evidence.unitIds] }; } return undefined; } function presentationBase( memory: ThirdCampPreparationMemory, status: ThirdCampPreparationBattlePresentation['status'], text: string, turnNumber: number | null, usageCount: number ): ThirdCampPreparationBattlePresentation { return { selectionRecordId: thirdCampPreparationSelectionRecordId, priorityId: memory.priorityId, label: memory.label, status, text, activeThroughTurn: thirdCampPreparationActiveThroughTurn, turnNumber, usageCount }; } function clonePriority( priority: ThirdCampPreparationPriorityDefinition ): ThirdCampPreparationPriorityDefinition { return { ...priority, effect: { ...priority.effect } } as ThirdCampPreparationPriorityDefinition; } function cloneEvidence( evidence: ThirdCampPreparationEvidence ): ThirdCampPreparationEvidence { if ( evidence.kind === 'priority-return-dialogue' || (evidence.kind === 'exploration-activity' && evidence.activityId === 'companion') ) { return { ...evidence, unitIds: [...evidence.unitIds] }; } return { ...evidence }; } function positiveInteger(value: unknown) { return ( typeof value === 'number' && Number.isInteger(value) && value > 0 && value <= 9999 ) ? value : undefined; } function nonNegativeInteger(value: unknown) { return ( typeof value === 'number' && Number.isFinite(value) && value > 0 ) ? Math.min(999999, Math.floor(value)) : 0; } function arrayValue(value: unknown): unknown[] { return Array.isArray(value) ? value : []; }