feat: carry third-camp preparation into battle

This commit is contained in:
2026-07-27 13:09:59 +09:00
parent b2e7c8b403
commit adf9e65668
12 changed files with 5307 additions and 43 deletions

View File

@@ -9,6 +9,14 @@ import type { UnitDirection } from '../data/unitAssets';
import { equipmentExpToNext, equipmentSlots, type EquipmentSlot } from '../data/battleItems';
import type { SortieOrderId } from '../data/sortieOrders';
import { coreSortieResonanceMinLevel } from '../data/sortieSynergy';
import {
getThirdCampPreparationPriority,
isThirdCampPreparationPriorityId,
thirdCampPreparationInformationEnemyUnitId,
thirdCampPreparationSelectionRecordId,
thirdCampPreparationTargetBattleId,
type ThirdCampPreparationPriorityId
} from '../data/thirdCampPreparation';
export type BattleSaveFaction = 'ally' | 'enemy';
export type BattleSaveRosterTab = 'ally' | 'enemy';
@@ -91,6 +99,19 @@ export type BattleSaveCooperationStats = {
additionalDamage: number;
};
export type BattleSaveThirdCampPreparationState = {
selectionRecordId: typeof thirdCampPreparationSelectionRecordId;
priorityId: ThirdCampPreparationPriorityId;
label: string;
markedEnemyUnitId?: typeof thirdCampPreparationInformationEnemyUnitId;
informationConsumed: boolean;
equipmentConsumed: boolean;
companionAttempts: number;
companionSuccesses: number;
usageCount: number;
preventedDamage: number;
};
export type BattleSaveEventPriority = 'critical' | 'high' | 'normal' | 'low';
export type BattleSavePendingEvent = {
@@ -111,6 +132,7 @@ export type BattleSaveState = {
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
coreResonanceStats?: BattleSaveCoreResonanceStats;
cooperationStatsByBond?: Record<string, BattleSaveCooperationStats>;
thirdCampPreparation?: BattleSaveThirdCampPreparationState;
savedAt: string;
turnNumber: number;
activeFaction: BattleSaveFaction;
@@ -207,6 +229,7 @@ export function normalizeBattleSaveState(
normalizeCoreResonanceSaveFields(cloned, options);
normalizeCooperationSaveFields(cloned, options);
normalizeThirdCampPreparationSaveField(cloned);
const recommendation = normalizeCampaignSortieRecommendationSnapshot(cloned.sortieRecommendation, {
expectedBattleId: typeof cloned.battleId === 'string' ? cloned.battleId : options.expectedBattleId,
allowedUnitIds: options.validAllyUnitIds
@@ -256,6 +279,10 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
return false;
}
if (!isOptionalThirdCampPreparationState(state.thirdCampPreparation, state)) {
return false;
}
const attackIntents = state.attackIntents;
const units = state.units;
@@ -505,6 +532,141 @@ function isOptionalCooperationStatsByBond(
));
}
function normalizeThirdCampPreparationSaveField(
state: Record<string, unknown>
) {
if (
state.battleId !== thirdCampPreparationTargetBattleId ||
!isRecord(state.thirdCampPreparation)
) {
delete state.thirdCampPreparation;
return;
}
const raw = state.thirdCampPreparation;
const priorityId = isThirdCampPreparationPriorityId(raw.priorityId)
? raw.priorityId
: undefined;
const priority = priorityId
? getThirdCampPreparationPriority(priorityId)
: undefined;
if (
!priorityId ||
!priority ||
raw.selectionRecordId !== thirdCampPreparationSelectionRecordId
) {
delete state.thirdCampPreparation;
return;
}
const informationConsumed =
priorityId === 'information' && raw.informationConsumed === true;
const equipmentConsumed =
priorityId === 'equipment' && raw.equipmentConsumed === true;
const companionAttempts = priorityId === 'companion'
? normalizeCoreResonanceStatValue(raw.companionAttempts)
: 0;
const companionSuccesses = priorityId === 'companion'
? Math.min(
companionAttempts,
normalizeCoreResonanceStatValue(raw.companionSuccesses)
)
: 0;
const usageCount = priorityId === 'information'
? Number(informationConsumed)
: priorityId === 'equipment'
? Number(equipmentConsumed)
: companionAttempts;
const preventedDamage = priorityId === 'equipment' && equipmentConsumed
? normalizeCoreResonanceStatValue(raw.preventedDamage)
: 0;
state.thirdCampPreparation = {
selectionRecordId: thirdCampPreparationSelectionRecordId,
priorityId,
label: priority.label,
...(priorityId === 'information'
? {
markedEnemyUnitId:
thirdCampPreparationInformationEnemyUnitId
}
: {}),
informationConsumed,
equipmentConsumed,
companionAttempts,
companionSuccesses,
usageCount,
preventedDamage
} satisfies BattleSaveThirdCampPreparationState;
}
function isOptionalThirdCampPreparationState(
value: unknown,
state: Record<string, unknown>
) {
if (value === undefined) {
return true;
}
if (
state.battleId !== thirdCampPreparationTargetBattleId ||
!isRecord(value) ||
value.selectionRecordId !==
thirdCampPreparationSelectionRecordId ||
!isThirdCampPreparationPriorityId(value.priorityId)
) {
return false;
}
const priority = getThirdCampPreparationPriority(value.priorityId);
if (
!priority ||
value.label !== priority.label ||
typeof value.informationConsumed !== 'boolean' ||
typeof value.equipmentConsumed !== 'boolean' ||
!isBattleUnitStatValue(value.companionAttempts) ||
!isBattleUnitStatValue(value.companionSuccesses) ||
!isBattleUnitStatValue(value.usageCount) ||
!isBattleUnitStatValue(value.preventedDamage) ||
Number(value.companionSuccesses) >
Number(value.companionAttempts)
) {
return false;
}
if (value.priorityId === 'information') {
return (
value.markedEnemyUnitId ===
thirdCampPreparationInformationEnemyUnitId &&
value.equipmentConsumed === false &&
Number(value.companionAttempts) === 0 &&
Number(value.companionSuccesses) === 0 &&
Number(value.usageCount) ===
Number(value.informationConsumed) &&
Number(value.preventedDamage) === 0
);
}
if (value.priorityId === 'equipment') {
return (
value.markedEnemyUnitId === undefined &&
value.informationConsumed === false &&
Number(value.companionAttempts) === 0 &&
Number(value.companionSuccesses) === 0 &&
Number(value.usageCount) ===
Number(value.equipmentConsumed) &&
(
value.equipmentConsumed === true ||
Number(value.preventedDamage) === 0
)
);
}
return (
value.markedEnemyUnitId === undefined &&
value.informationConsumed === false &&
value.equipmentConsumed === false &&
Number(value.usageCount) ===
Number(value.companionAttempts) &&
Number(value.preventedDamage) === 0
);
}
function isOptionalCoreResonanceState(
state: Record<string, unknown>,
options: BattleSaveValidationOptions