867 lines
24 KiB
TypeScript
867 lines
24 KiB
TypeScript
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<string, ThirdCampPreparationBattleRecord | null | undefined>
|
|
>;
|
|
firstBattleReport?: ThirdCampPreparationBattleRecord | null;
|
|
completedCampDialogues?: readonly unknown[];
|
|
completedCampVisits?: readonly unknown[];
|
|
thirdCampPreparationSelection?: {
|
|
sourceBattleId?: unknown;
|
|
targetBattleId?: unknown;
|
|
priorityId?: unknown;
|
|
confirmed?: unknown;
|
|
} | null;
|
|
acknowledgedVictoryRewardCategories?: Readonly<
|
|
Record<string, readonly unknown[] | undefined>
|
|
>;
|
|
};
|
|
|
|
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<string, { bondId: string; unitIds: [string, string] }>
|
|
> = {
|
|
'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<ThirdCampPreparationPriorityId, ThirdCampPreparationEvidence>
|
|
> = {
|
|
...(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 : [];
|
|
}
|