feat: connect Xuzhou equipment to battle outcomes

This commit is contained in:
2026-07-27 15:39:11 +09:00
parent adf9e65668
commit 2737354e30
13 changed files with 5664 additions and 68 deletions

View File

@@ -17,6 +17,10 @@ import {
thirdCampPreparationTargetBattleId,
type ThirdCampPreparationPriorityId
} from '../data/thirdCampPreparation';
import {
normalizeCityEquipmentContributionSnapshot,
type CityEquipmentContributionSnapshot
} from '../data/cityEquipmentPreparation';
export type BattleSaveFaction = 'ally' | 'enemy';
export type BattleSaveRosterTab = 'ally' | 'enemy';
@@ -112,6 +116,9 @@ export type BattleSaveThirdCampPreparationState = {
preventedDamage: number;
};
export type BattleSaveCityEquipmentContributionState =
CityEquipmentContributionSnapshot;
export type BattleSaveEventPriority = 'critical' | 'high' | 'normal' | 'low';
export type BattleSavePendingEvent = {
@@ -133,6 +140,7 @@ export type BattleSaveState = {
coreResonanceStats?: BattleSaveCoreResonanceStats;
cooperationStatsByBond?: Record<string, BattleSaveCooperationStats>;
thirdCampPreparation?: BattleSaveThirdCampPreparationState;
cityEquipmentContribution?: BattleSaveCityEquipmentContributionState;
savedAt: string;
turnNumber: number;
activeFaction: BattleSaveFaction;
@@ -230,6 +238,7 @@ export function normalizeBattleSaveState(
normalizeCoreResonanceSaveFields(cloned, options);
normalizeCooperationSaveFields(cloned, options);
normalizeThirdCampPreparationSaveField(cloned);
normalizeCityEquipmentContributionSaveField(cloned, options);
const recommendation = normalizeCampaignSortieRecommendationSnapshot(cloned.sortieRecommendation, {
expectedBattleId: typeof cloned.battleId === 'string' ? cloned.battleId : options.expectedBattleId,
allowedUnitIds: options.validAllyUnitIds
@@ -283,6 +292,10 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
return false;
}
if (!isOptionalCityEquipmentContributionState(state.cityEquipmentContribution, state, options)) {
return false;
}
const attackIntents = state.attackIntents;
const units = state.units;
@@ -667,6 +680,129 @@ function isOptionalThirdCampPreparationState(
);
}
function normalizeCityEquipmentContributionSaveField(
state: Record<string, unknown>,
options: BattleSaveValidationOptions
) {
const normalized = canonicalCityEquipmentContributionSaveState(
state.cityEquipmentContribution,
state,
options
);
if (normalized) {
state.cityEquipmentContribution = normalized;
} else {
delete state.cityEquipmentContribution;
}
}
function isOptionalCityEquipmentContributionState(
value: unknown,
state: Record<string, unknown>,
options: BattleSaveValidationOptions
) {
if (value === undefined) {
return true;
}
const normalized = canonicalCityEquipmentContributionSaveState(
value,
state,
options
);
if (!normalized || !isRecord(value)) {
return false;
}
const fields: Array<keyof BattleSaveCityEquipmentContributionState> = [
'version',
'selectionRecordId',
'cityStayId',
'sourceBattleId',
'targetBattleId',
'offerId',
'itemId',
'slot',
'price',
'purchaseNumber',
'unitId',
'previousItemId',
'deployed',
'offensiveActions',
'defensiveHits',
'bonusDamage',
'preventedDamage'
];
return (
Object.keys(value).length === fields.length &&
fields.every((field) => value[field] === normalized[field])
);
}
function canonicalCityEquipmentContributionSaveState(
value: unknown,
state: Record<string, unknown>,
options: BattleSaveValidationOptions
): BattleSaveCityEquipmentContributionState | undefined {
const battleId =
typeof state.battleId === 'string' ? state.battleId : undefined;
const normalized =
normalizeCityEquipmentContributionSnapshot(value, battleId);
if (
!battleId ||
!normalized ||
!normalized.deployed ||
normalized.targetBattleId !== battleId ||
!Array.isArray(state.units)
) {
return undefined;
}
const unit = state.units.find(
(candidate) =>
isRecord(candidate) && candidate.id === normalized.unitId
);
const equipment =
unit && isRecord(unit.equipment) ? unit.equipment : undefined;
const equippedState =
equipment && isRecord(equipment[normalized.slot])
? equipment[normalized.slot]
: undefined;
if (
!unit ||
!equippedState ||
equippedState.itemId !== normalized.itemId ||
(options.validUnitIds &&
!options.validUnitIds.has(normalized.unitId)) ||
(options.validAllyUnitIds &&
!options.validAllyUnitIds.has(normalized.unitId)) ||
(options.validEquipmentItemIds &&
(!options.validEquipmentItemIds.has(normalized.itemId) ||
!options.validEquipmentItemIds.has(
normalized.previousItemId
))) ||
(options.validEquipmentSlotItems?.[normalized.slot] &&
(!options.validEquipmentSlotItems[normalized.slot]?.has(
normalized.itemId
) ||
!options.validEquipmentSlotItems[normalized.slot]?.has(
normalized.previousItemId
)))
) {
return undefined;
}
return {
...normalized,
bonusDamage:
normalized.offensiveActions > 0
? normalized.bonusDamage
: 0,
preventedDamage:
normalized.defensiveHits > 0
? normalized.preventedDamage
: 0
};
}
function isOptionalCoreResonanceState(
state: Record<string, unknown>,
options: BattleSaveValidationOptions