feat: add selectable core resonance pairs

This commit is contained in:
2026-07-11 22:05:33 +09:00
parent 7d3c49ac0b
commit 51529e51c2
9 changed files with 1693 additions and 100 deletions

View File

@@ -3,6 +3,7 @@ import { unitClasses } from '../data/battleRules';
import { battleScenarios, type BattleScenarioId } from '../data/battles';
import { campaignRecruitUnits, type BattleBond, type UnitData } from '../data/scenario';
import { normalizeSortieFormationAssignments, type SortieFormationAssignments } from '../data/sortieDeployment';
import { coreSortieResonanceMinLevel } from '../data/sortieSynergy';
import {
isSortieOrderId,
sortieOrderCheckIdsByOrder,
@@ -395,6 +396,11 @@ export type CampaignSortieOrderSelection = {
orderId: CampaignSortiePresetId;
};
export type CampaignSortieResonanceSelection = {
battleId: BattleScenarioId;
bondId: string;
};
export type CampaignSortieOrderHistory = Partial<
Record<BattleScenarioId, Partial<Record<CampaignSortiePresetId, CampaignSortieOrderResultSnapshot>>>
>;
@@ -413,6 +419,7 @@ export type CampaignState = {
sortieItemAssignments: CampaignSortieItemAssignments;
sortieFormationPresets: CampaignSortieFormationPresets;
sortieOrderSelection?: CampaignSortieOrderSelection;
sortieResonanceSelection?: CampaignSortieResonanceSelection;
sortieOrderHistory: CampaignSortieOrderHistory;
claimedSortieOrderRewardIds: string[];
reserveTrainingFocus: CampaignReserveTrainingFocusId;
@@ -722,6 +729,32 @@ export function setCampaignSortieOrderSelection(battleId: BattleScenarioId, orde
return cloneCampaignState(state);
}
export function setCampaignSortieResonanceSelection(battleId: BattleScenarioId, bondId?: string) {
const state = ensureCampaignState();
if (!hasCampaignBattleScenario(battleId)) {
return cloneCampaignState(state);
}
if (bondId) {
const normalizedBondId = normalizeKeyString(bondId);
const bond = resolveCampaignSortieResonanceBond(battleId, normalizedBondId, state.bonds);
const selectedUnitIds = new Set(state.selectedSortieUnitIds);
if (
!bond ||
bond.level < coreSortieResonanceMinLevel ||
!bond.unitIds.every((unitId) => selectedUnitIds.has(unitId))
) {
return cloneCampaignState(state);
}
state.sortieResonanceSelection = { battleId, bondId: bond.id };
} else if (!state.sortieResonanceSelection || state.sortieResonanceSelection.battleId === battleId) {
delete state.sortieResonanceSelection;
}
state.updatedAt = new Date().toISOString();
persistCampaignState(state);
return cloneCampaignState(state);
}
export function setCampaignReserveTrainingFocus(focusId: CampaignReserveTrainingFocusId) {
const state = ensureCampaignState();
const normalizedFocusId = normalizeReserveTrainingFocusId(focusId);
@@ -890,6 +923,9 @@ export function setFirstBattleReport(report: FirstBattleReport) {
if (state.sortieOrderSelection?.battleId === battleId) {
delete state.sortieOrderSelection;
}
if (state.sortieResonanceSelection?.battleId === battleId) {
delete state.sortieResonanceSelection;
}
state.latestBattleId = battleId;
state.updatedAt = new Date().toISOString();
@@ -1089,6 +1125,14 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
if (!normalized.sortieOrderSelection) {
delete normalized.sortieOrderSelection;
}
normalized.sortieResonanceSelection = normalizeCampaignSortieResonanceSelection(
normalized.sortieResonanceSelection,
normalized.bonds,
normalized.selectedSortieUnitIds
);
if (!normalized.sortieResonanceSelection) {
delete normalized.sortieResonanceSelection;
}
normalized.sortieOrderHistory = normalizeCampaignSortieOrderHistory(normalized.sortieOrderHistory);
normalized.claimedSortieOrderRewardIds = normalizeCampaignSortieOrderRewardClaims(
normalized.claimedSortieOrderRewardIds,
@@ -1266,6 +1310,53 @@ function normalizeCampaignSortieOrderSelection(value: unknown): CampaignSortieOr
return { battleId: battleId as BattleScenarioId, orderId: value.orderId };
}
function normalizeCampaignSortieResonanceSelection(
value: unknown,
bonds: readonly CampBondSnapshot[],
selectedSortieUnitIds: readonly string[]
): CampaignSortieResonanceSelection | undefined {
if (!isPlainObject(value)) {
return undefined;
}
const battleId = typeof value.battleId === 'string' ? value.battleId : '';
const bondId = normalizeKeyString(value.bondId);
if (!hasCampaignBattleScenario(battleId) || !bondId) {
return undefined;
}
const bond = resolveCampaignSortieResonanceBond(battleId, bondId, bonds);
const selectedUnitIds = new Set(selectedSortieUnitIds);
if (
!bond ||
bond.level < coreSortieResonanceMinLevel ||
!bond.unitIds.every((unitId) => selectedUnitIds.has(unitId))
) {
return undefined;
}
return { battleId: battleId as BattleScenarioId, bondId };
}
function resolveCampaignSortieResonanceBond(
battleId: string,
bondId: string,
campaignBonds: readonly CampBondSnapshot[]
): BattleBond | undefined {
if (!hasCampaignBattleScenario(battleId)) {
return undefined;
}
const scenarioBond = battleScenarios[battleId].bonds.find((bond) => bond.id === bondId);
if (!scenarioBond) {
return undefined;
}
const campaignBond = campaignBonds.find((bond) => bond.id === bondId);
return campaignBond
? { ...scenarioBond, level: campaignBond.level, exp: campaignBond.exp }
: scenarioBond;
}
function hasCampaignBattleScenario(battleId: string): battleId is BattleScenarioId {
return Object.prototype.hasOwnProperty.call(battleScenarios, battleId);
}
export function normalizeCampaignSortieOrderResultSnapshot(
value: unknown,
outcome?: FirstBattleReport['outcome'],