feat: make Xuzhou choices shape battle eight

This commit is contained in:
2026-07-27 17:39:13 +09:00
parent 2737354e30
commit eec4896002
13 changed files with 4231 additions and 84 deletions

View File

@@ -21,6 +21,10 @@ import {
normalizeCityEquipmentContributionSnapshot,
type CityEquipmentContributionSnapshot
} from '../data/cityEquipmentPreparation';
import {
normalizeXuzhouStayBattlePayoffRuntime,
type XuzhouStayBattlePayoffRuntime
} from '../data/xuzhouStayBattlePayoff';
export type BattleSaveFaction = 'ally' | 'enemy';
export type BattleSaveRosterTab = 'ally' | 'enemy';
@@ -118,6 +122,8 @@ export type BattleSaveThirdCampPreparationState = {
export type BattleSaveCityEquipmentContributionState =
CityEquipmentContributionSnapshot;
export type BattleSaveXuzhouStayBattlePayoffState =
XuzhouStayBattlePayoffRuntime;
export type BattleSaveEventPriority = 'critical' | 'high' | 'normal' | 'low';
@@ -141,6 +147,7 @@ export type BattleSaveState = {
cooperationStatsByBond?: Record<string, BattleSaveCooperationStats>;
thirdCampPreparation?: BattleSaveThirdCampPreparationState;
cityEquipmentContribution?: BattleSaveCityEquipmentContributionState;
xuzhouStayBattlePayoff?: BattleSaveXuzhouStayBattlePayoffState;
savedAt: string;
turnNumber: number;
activeFaction: BattleSaveFaction;
@@ -239,6 +246,7 @@ export function normalizeBattleSaveState(
normalizeCooperationSaveFields(cloned, options);
normalizeThirdCampPreparationSaveField(cloned);
normalizeCityEquipmentContributionSaveField(cloned, options);
normalizeXuzhouStayBattlePayoffSaveField(cloned, options);
const recommendation = normalizeCampaignSortieRecommendationSnapshot(cloned.sortieRecommendation, {
expectedBattleId: typeof cloned.battleId === 'string' ? cloned.battleId : options.expectedBattleId,
allowedUnitIds: options.validAllyUnitIds
@@ -296,6 +304,10 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
return false;
}
if (!isOptionalXuzhouStayBattlePayoffState(state.xuzhouStayBattlePayoff, state, options)) {
return false;
}
const attackIntents = state.attackIntents;
const units = state.units;
@@ -803,6 +815,100 @@ function canonicalCityEquipmentContributionSaveState(
};
}
function normalizeXuzhouStayBattlePayoffSaveField(
state: Record<string, unknown>,
options: BattleSaveValidationOptions
) {
const normalized = canonicalXuzhouStayBattlePayoffSaveState(
state.xuzhouStayBattlePayoff,
state,
options
);
if (normalized) {
state.xuzhouStayBattlePayoff = normalized;
} else {
delete state.xuzhouStayBattlePayoff;
}
}
function isOptionalXuzhouStayBattlePayoffState(
value: unknown,
state: Record<string, unknown>,
options: BattleSaveValidationOptions
) {
if (value === undefined) {
return true;
}
const normalized = canonicalXuzhouStayBattlePayoffSaveState(
value,
state,
options
);
if (!normalized || !isRecord(value)) {
return false;
}
const fields: Array<
keyof BattleSaveXuzhouStayBattlePayoffState
> = [
'version',
'recordId',
'cityStayId',
'sourceBattleId',
'targetBattleId',
'informationVisitId',
'dialogueId',
'informationCompleted',
'informationCounterplays',
'miZhuDeployed',
'supportAttempts',
'supportUses',
'bonusHealing',
'bonusBuffTurns'
];
if (normalized.choiceId) {
fields.push('choiceId');
}
return (
Object.keys(value).length === fields.length &&
fields.every((field) => value[field] === normalized[field])
);
}
function canonicalXuzhouStayBattlePayoffSaveState(
value: unknown,
state: Record<string, unknown>,
options: BattleSaveValidationOptions
): BattleSaveXuzhouStayBattlePayoffState | undefined {
const battleId =
typeof state.battleId === 'string' ? state.battleId : undefined;
if (!battleId || !Array.isArray(state.units)) {
return undefined;
}
const deployedUnitIds = state.units
.filter(isRecord)
.map((unit) => unit.id)
.filter(
(unitId): unitId is string =>
typeof unitId === 'string' &&
(!options.validAllyUnitIds ||
options.validAllyUnitIds.has(unitId))
);
const normalized = normalizeXuzhouStayBattlePayoffRuntime(
value,
{
expectedBattleId: battleId,
expectedRosterUnitIds: deployedUnitIds
}
);
if (
!normalized ||
(!normalized.informationCompleted && !normalized.choiceId)
) {
return undefined;
}
return normalized;
}
function isOptionalCoreResonanceState(
state: Record<string, unknown>,
options: BattleSaveValidationOptions