feat: add selectable core resonance pairs
This commit is contained in:
@@ -3,6 +3,7 @@ import { isCampaignStep, type CampaignStep } from './campaignState';
|
||||
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';
|
||||
|
||||
export type BattleSaveFaction = 'ally' | 'enemy';
|
||||
export type BattleSaveRosterTab = 'ally' | 'enemy';
|
||||
@@ -69,11 +70,20 @@ export type BattleSaveTacticalCommandState = {
|
||||
effectLost?: boolean;
|
||||
};
|
||||
|
||||
export type BattleSaveCoreResonanceStats = {
|
||||
attempts: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
additionalDamage: number;
|
||||
};
|
||||
|
||||
export type BattleSaveState = {
|
||||
version: 1;
|
||||
battleId: string;
|
||||
campaignStep?: CampaignStep;
|
||||
sortieOrderId?: SortieOrderId;
|
||||
sortieResonanceBondId?: string;
|
||||
coreResonanceStats?: BattleSaveCoreResonanceStats;
|
||||
savedAt: string;
|
||||
turnNumber: number;
|
||||
activeFaction: BattleSaveFaction;
|
||||
@@ -148,12 +158,25 @@ export function parseBattleSaveState(raw: string | null | undefined, options: Ba
|
||||
|
||||
try {
|
||||
const state = JSON.parse(raw) as unknown;
|
||||
return isValidBattleSaveState(state, options) ? state : undefined;
|
||||
return normalizeBattleSaveState(state, options);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeBattleSaveState(
|
||||
state: unknown,
|
||||
options: BattleSaveValidationOptions = {}
|
||||
): BattleSaveState | undefined {
|
||||
const cloned = cloneJsonValue(state);
|
||||
if (!isRecord(cloned)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
normalizeCoreResonanceSaveFields(cloned, options);
|
||||
return isValidBattleSaveState(cloned, options) ? cloned : undefined;
|
||||
}
|
||||
|
||||
export function isValidBattleSaveState(state: unknown, options: BattleSaveValidationOptions = {}): state is BattleSaveState {
|
||||
if (!isRecord(state) || state.version !== 1 || !Array.isArray(state.units)) {
|
||||
return false;
|
||||
@@ -183,6 +206,10 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isOptionalCoreResonanceState(state, options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attackIntents = state.attackIntents;
|
||||
const units = state.units;
|
||||
|
||||
@@ -238,6 +265,118 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneJsonValue(value: unknown): unknown {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
return serialized === undefined ? undefined : JSON.parse(serialized) as unknown;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCoreResonanceSaveFields(
|
||||
state: Record<string, unknown>,
|
||||
options: BattleSaveValidationOptions
|
||||
) {
|
||||
const requestedBondId = typeof state.sortieResonanceBondId === 'string'
|
||||
? state.sortieResonanceBondId.trim()
|
||||
: '';
|
||||
const bonds = Array.isArray(state.bonds) ? state.bonds : [];
|
||||
const units = Array.isArray(state.units) ? state.units : [];
|
||||
const deployedUnitIds = new Set(
|
||||
units.filter(isRecord).map((unit) => unit.id).filter((unitId): unitId is string => typeof unitId === 'string')
|
||||
);
|
||||
const bond = bonds.find((candidate) => (
|
||||
isRecord(candidate) &&
|
||||
candidate.id === requestedBondId &&
|
||||
Number.isInteger(candidate.level) &&
|
||||
Number(candidate.level) >= coreSortieResonanceMinLevel &&
|
||||
Array.isArray(candidate.unitIds) &&
|
||||
candidate.unitIds.length === 2 &&
|
||||
candidate.unitIds.every((unitId) => (
|
||||
typeof unitId === 'string' &&
|
||||
deployedUnitIds.has(unitId) &&
|
||||
(!options.validAllyUnitIds || options.validAllyUnitIds.has(unitId))
|
||||
))
|
||||
));
|
||||
|
||||
if (!requestedBondId || !bond) {
|
||||
delete state.sortieResonanceBondId;
|
||||
delete state.coreResonanceStats;
|
||||
return;
|
||||
}
|
||||
|
||||
state.sortieResonanceBondId = requestedBondId;
|
||||
if (state.coreResonanceStats === undefined) {
|
||||
return;
|
||||
}
|
||||
const rawStats = isRecord(state.coreResonanceStats) ? state.coreResonanceStats : {};
|
||||
const rawSuccesses = normalizeCoreResonanceStatValue(rawStats.successes);
|
||||
const rawFailures = normalizeCoreResonanceStatValue(rawStats.failures);
|
||||
const attempts = Math.min(
|
||||
maxBattleUnitStatValue,
|
||||
Math.max(normalizeCoreResonanceStatValue(rawStats.attempts), rawSuccesses + rawFailures)
|
||||
);
|
||||
const successes = Math.min(rawSuccesses, attempts);
|
||||
state.coreResonanceStats = {
|
||||
attempts,
|
||||
successes,
|
||||
failures: attempts - successes,
|
||||
additionalDamage: normalizeCoreResonanceStatValue(rawStats.additionalDamage)
|
||||
} satisfies BattleSaveCoreResonanceStats;
|
||||
}
|
||||
|
||||
function normalizeCoreResonanceStatValue(value: unknown) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(maxBattleUnitStatValue, Math.max(0, Math.floor(value)));
|
||||
}
|
||||
|
||||
function isOptionalCoreResonanceState(
|
||||
state: Record<string, unknown>,
|
||||
options: BattleSaveValidationOptions
|
||||
) {
|
||||
const bondId = state.sortieResonanceBondId;
|
||||
const stats = state.coreResonanceStats;
|
||||
if (bondId === undefined) {
|
||||
return stats === undefined;
|
||||
}
|
||||
if (typeof bondId !== 'string' || bondId.length === 0 || !Array.isArray(state.bonds) || !Array.isArray(state.units)) {
|
||||
return false;
|
||||
}
|
||||
const deployedUnitIds = new Set(
|
||||
state.units.filter(isRecord).map((unit) => unit.id).filter((unitId): unitId is string => typeof unitId === 'string')
|
||||
);
|
||||
const bond = state.bonds.find((candidate) => (
|
||||
isRecord(candidate) &&
|
||||
candidate.id === bondId &&
|
||||
Number.isInteger(candidate.level) &&
|
||||
Number(candidate.level) >= coreSortieResonanceMinLevel &&
|
||||
Array.isArray(candidate.unitIds) &&
|
||||
candidate.unitIds.length === 2 &&
|
||||
candidate.unitIds.every((unitId) => (
|
||||
typeof unitId === 'string' &&
|
||||
deployedUnitIds.has(unitId) &&
|
||||
(!options.validAllyUnitIds || options.validAllyUnitIds.has(unitId))
|
||||
))
|
||||
));
|
||||
if (!bond) {
|
||||
return false;
|
||||
}
|
||||
if (stats === undefined) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
isRecord(stats) &&
|
||||
isBattleUnitStatValue(stats.attempts) &&
|
||||
isBattleUnitStatValue(stats.successes) &&
|
||||
isBattleUnitStatValue(stats.failures) &&
|
||||
isBattleUnitStatValue(stats.additionalDamage) &&
|
||||
Number(stats.attempts) === Number(stats.successes) + Number(stats.failures)
|
||||
);
|
||||
}
|
||||
|
||||
function isPositiveInteger(value: unknown) {
|
||||
return Number.isInteger(value) && Number(value) >= 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user