Cap campaign battle history snapshots

This commit is contained in:
2026-07-05 12:04:23 +09:00
parent 20def5e151
commit d48726c21c
2 changed files with 215 additions and 94 deletions

View File

@@ -269,6 +269,11 @@ const campaignInventoryCaps: Record<string, number> = {
const maxCampaignStringListEntries = 128;
const maxCampaignStringListLength = 96;
const maxCampaignRewardNoteLength = 180;
const maxCampaignBattleObjectiveEntries = 24;
const maxCampaignBattleUnitEntries = 96;
const maxCampaignBattleBondEntries = 96;
const maxCampaignReserveTrainingEntries = 96;
const maxCampaignDisplayTextLength = 180;
export const campaignReserveTrainingFocusDefinitions: CampaignReserveTrainingFocusDefinition[] = [
{
@@ -890,6 +895,37 @@ function uniqueStrings(value: unknown): string[] {
].slice(0, maxCampaignStringListEntries);
}
function normalizeKeyString(value: unknown): string {
if (typeof value !== 'string') {
return '';
}
const text = value.trim();
return text.length > 0 && text.length <= maxCampaignStringListLength ? text : '';
}
function normalizeDisplayString(value: unknown, maxLength = maxCampaignDisplayTextLength): string {
if (typeof value !== 'string') {
return '';
}
return value.trim().slice(0, maxLength);
}
function normalizeLimitedArray<T>(value: unknown, normalizer: (entry: unknown) => T | undefined, maxEntries: number): T[] {
const normalized: T[] = [];
for (const entry of arrayOrEmpty<unknown>(value)) {
const normalizedEntry = normalizer(entry);
if (normalizedEntry) {
normalized.push(normalizedEntry);
if (normalized.length >= maxEntries) {
break;
}
}
}
return normalized;
}
function normalizedRosterUnitIds(roster: UnitData[]) {
return new Set(
roster
@@ -986,44 +1022,44 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
return {
battleId,
battleTitle: settlement.battleTitle,
battleTitle: normalizeDisplayString(settlement.battleTitle),
outcome: settlement.outcome,
rewardGold: normalizeNonNegativeInteger(settlement.rewardGold),
itemRewards: uniqueStrings(settlement.itemRewards),
campaignRewards: cloneCampaignRewardSnapshot(settlement.campaignRewards),
objectives: arrayOrEmpty<unknown>(settlement.objectives)
.map(normalizeBattleObjectiveSnapshot)
.filter((objective): objective is BattleObjectiveSnapshot => Boolean(objective)),
units: arrayOrEmpty<unknown>(settlement.units)
.map(normalizeCampaignUnitProgressSnapshot)
.filter((unit): unit is CampaignUnitProgressSnapshot => Boolean(unit)),
bonds: arrayOrEmpty<unknown>(settlement.bonds)
.map(normalizeCampaignBondProgressSnapshot)
.filter((bond): bond is CampaignBondProgressSnapshot => Boolean(bond)),
reserveTraining: arrayOrEmpty<unknown>(settlement.reserveTraining)
.map(normalizeReserveTrainingSnapshot)
.filter((entry): entry is CampaignReserveTrainingSnapshot => Boolean(entry)),
objectives: normalizeLimitedArray(settlement.objectives, normalizeBattleObjectiveSnapshot, maxCampaignBattleObjectiveEntries),
units: normalizeLimitedArray(settlement.units, normalizeCampaignUnitProgressSnapshot, maxCampaignBattleUnitEntries),
bonds: normalizeLimitedArray(settlement.bonds, normalizeCampaignBondProgressSnapshot, maxCampaignBattleBondEntries),
reserveTraining: normalizeLimitedArray(settlement.reserveTraining, normalizeReserveTrainingSnapshot, maxCampaignReserveTrainingEntries),
completedAt
};
}
function normalizeBattleObjectiveSnapshot(value: unknown): BattleObjectiveSnapshot | undefined {
if (!isPlainObject(value) || typeof value.id !== 'string' || value.id.length === 0 || typeof value.label !== 'string' || value.label.length === 0) {
if (!isPlainObject(value)) {
return undefined;
}
const id = normalizeKeyString(value.id);
const label = normalizeDisplayString(value.label);
if (!id || !label) {
return undefined;
}
const status = value.status === 'active' || value.status === 'done' || value.status === 'failed' ? value.status : undefined;
const category = value.category === 'primary' || value.category === 'required' || value.category === 'bonus' ? value.category : undefined;
const targetTile = normalizeObjectiveTargetTile(value.targetTile);
const summary = normalizeDisplayString(value.summary);
const failureReason = normalizeDisplayString(value.failureReason);
return {
id: value.id,
label: value.label,
id,
label,
achieved: Boolean(value.achieved),
...(status ? { status } : {}),
detail: typeof value.detail === 'string' ? value.detail : '',
detail: normalizeDisplayString(value.detail),
...(category ? { category } : {}),
...(typeof value.summary === 'string' ? { summary: value.summary } : {}),
...(typeof value.failureReason === 'string' ? { failureReason: value.failureReason } : {}),
...(summary ? { summary } : {}),
...(failureReason ? { failureReason } : {}),
...(targetTile ? { targetTile } : {}),
rewardGold: normalizeNonNegativeInteger(value.rewardGold)
};
@@ -1045,15 +1081,21 @@ function normalizeObjectiveTargetTile(value: unknown): BattleObjectiveSnapshot['
}
function normalizeCampaignUnitProgressSnapshot(value: unknown): CampaignUnitProgressSnapshot | undefined {
if (!isPlainObject(value) || typeof value.unitId !== 'string' || value.unitId.length === 0 || typeof value.name !== 'string' || value.name.length === 0) {
if (!isPlainObject(value)) {
return undefined;
}
const unitId = normalizeKeyString(value.unitId);
const name = normalizeDisplayString(value.name);
if (!unitId || !name) {
return undefined;
}
const hp = normalizeNonNegativeInteger(value.hp);
const maxHp = Math.max(hp, normalizeNonNegativeInteger(value.maxHp));
return {
unitId: value.unitId,
name: value.name,
unitId,
name,
level: Math.max(1, normalizeNonNegativeInteger(value.level)),
exp: normalizeNonNegativeInteger(value.exp),
hp,
@@ -1063,13 +1105,19 @@ function normalizeCampaignUnitProgressSnapshot(value: unknown): CampaignUnitProg
}
function normalizeCampaignBondProgressSnapshot(value: unknown): CampaignBondProgressSnapshot | undefined {
if (!isPlainObject(value) || typeof value.id !== 'string' || value.id.length === 0 || typeof value.title !== 'string' || value.title.length === 0) {
if (!isPlainObject(value)) {
return undefined;
}
const id = normalizeKeyString(value.id);
const title = normalizeDisplayString(value.title);
if (!id || !title) {
return undefined;
}
return {
id: value.id,
title: value.title,
id,
title,
level: normalizeNonNegativeInteger(value.level),
exp: normalizeNonNegativeInteger(value.exp),
battleExp: normalizeNonNegativeInteger(value.battleExp)
@@ -1077,18 +1125,25 @@ function normalizeCampaignBondProgressSnapshot(value: unknown): CampaignBondProg
}
function normalizeReserveTrainingSnapshot(value: unknown): CampaignReserveTrainingSnapshot | undefined {
if (!isPlainObject(value) || typeof value.unitId !== 'string' || value.unitId.length === 0 || typeof value.name !== 'string' || value.name.length === 0) {
if (!isPlainObject(value)) {
return undefined;
}
const unitId = normalizeKeyString(value.unitId);
const name = normalizeDisplayString(value.name);
const focusLabel = normalizeDisplayString(value.focusLabel);
if (!unitId || !name) {
return undefined;
}
return {
unitId: value.unitId,
name: value.name,
unitId,
name,
expGained: normalizeNonNegativeInteger(value.expGained),
equipmentExpGained: normalizeNonNegativeInteger(value.equipmentExpGained),
bondExpGained: normalizeNonNegativeInteger(value.bondExpGained),
focusId: normalizeReserveTrainingFocusId(typeof value.focusId === 'string' ? value.focusId : undefined),
...(typeof value.focusLabel === 'string' ? { focusLabel: value.focusLabel } : {}),
...(focusLabel ? { focusLabel } : {}),
level: normalizeNonNegativeInteger(value.level),
exp: normalizeNonNegativeInteger(value.exp)
};
@@ -1153,21 +1208,15 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
return {
battleId,
battleTitle,
battleTitle: normalizeDisplayString(battleTitle),
outcome: report.outcome,
turnNumber: normalizeNonNegativeInteger(report.turnNumber),
rewardGold: normalizeNonNegativeInteger(report.rewardGold),
defeatedEnemies: normalizeNonNegativeInteger(report.defeatedEnemies),
totalEnemies: normalizeNonNegativeInteger(report.totalEnemies),
objectives: arrayOrEmpty<unknown>(report.objectives)
.map(normalizeBattleObjectiveSnapshot)
.filter((objective): objective is BattleObjectiveSnapshot => Boolean(objective)),
units: arrayOrEmpty<unknown>(report.units)
.map(normalizeUnitDataSnapshot)
.filter((unit): unit is UnitData => Boolean(unit)),
bonds: arrayOrEmpty<unknown>(report.bonds)
.map(normalizeCampBondSnapshot)
.filter((bond): bond is CampBondSnapshot => Boolean(bond)),
objectives: normalizeLimitedArray(report.objectives, normalizeBattleObjectiveSnapshot, maxCampaignBattleObjectiveEntries),
units: normalizeLimitedArray(report.units, normalizeUnitDataSnapshot, maxCampaignBattleUnitEntries),
bonds: normalizeLimitedArray(report.bonds, normalizeCampBondSnapshot, maxCampaignBattleBondEntries),
mvp: normalizeCampMvpSnapshot(report.mvp),
itemRewards: uniqueStrings(report.itemRewards),
campaignRewards: cloneCampaignRewardSnapshot(
@@ -1180,14 +1229,13 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
}
function normalizeUnitDataSnapshot(value: unknown): UnitData | undefined {
if (
!isPlainObject(value) ||
typeof value.id !== 'string' ||
value.id.length === 0 ||
typeof value.name !== 'string' ||
value.name.length === 0 ||
(value.faction !== 'ally' && value.faction !== 'enemy')
) {
if (!isPlainObject(value) || (value.faction !== 'ally' && value.faction !== 'enemy')) {
return undefined;
}
const id = normalizeKeyString(value.id);
const name = normalizeDisplayString(value.name);
if (!id || !name) {
return undefined;
}
@@ -1195,10 +1243,10 @@ function normalizeUnitDataSnapshot(value: unknown): UnitData | undefined {
const hp = normalizeNonNegativeInteger(value.hp);
const maxHp = Math.max(hp, normalizeNonNegativeInteger(value.maxHp));
return {
id: value.id,
name: value.name,
id,
name,
faction: value.faction,
className: typeof value.className === 'string' ? value.className : '',
className: normalizeDisplayString(value.className),
classKey: normalizeUnitClassKey(value.classKey),
level: Math.max(1, normalizeNonNegativeInteger(value.level)),
exp: normalizeNonNegativeInteger(value.exp),
@@ -1246,19 +1294,19 @@ function normalizeEquipmentLevel(value: unknown) {
}
function normalizeCampMvpSnapshot(value: unknown): CampMvpSnapshot | undefined {
if (
!isPlainObject(value) ||
typeof value.unitId !== 'string' ||
value.unitId.length === 0 ||
typeof value.name !== 'string' ||
value.name.length === 0
) {
if (!isPlainObject(value)) {
return undefined;
}
const unitId = normalizeKeyString(value.unitId);
const name = normalizeDisplayString(value.name);
if (!unitId || !name) {
return undefined;
}
return {
unitId: value.unitId,
name: value.name,
unitId,
name,
damageDealt: normalizeNonNegativeInteger(value.damageDealt),
defeats: normalizeNonNegativeInteger(value.defeats)
};
@@ -1270,20 +1318,16 @@ function normalizeCampBondSnapshot(value: unknown): CampBondSnapshot | undefined
}
const unitIds = arrayOrEmpty<unknown>(value.unitIds).filter((unitId): unitId is string => typeof unitId === 'string' && unitId.length > 0);
if (
typeof value.id !== 'string' ||
value.id.length === 0 ||
typeof value.title !== 'string' ||
value.title.length === 0 ||
unitIds.length < 2
) {
const id = normalizeKeyString(value.id);
const title = normalizeDisplayString(value.title);
if (!id || !title || unitIds.length < 2) {
return undefined;
}
return {
...(value as CampBondSnapshot),
id: value.id,
title: value.title,
id,
title,
unitIds: [unitIds[0], unitIds[1]] as [string, string],
level: normalizeNonNegativeInteger(value.level),
exp: normalizeNonNegativeInteger(value.exp),
@@ -1522,8 +1566,8 @@ function cloneCampaignState(state: CampaignState): CampaignState {
function cloneReport(report: FirstBattleReport): FirstBattleReport {
const cloned = JSON.parse(JSON.stringify(report)) as FirstBattleReport;
cloned.completedCampDialogues = [...new Set(cloned.completedCampDialogues ?? [])];
cloned.completedCampVisits = [...new Set(cloned.completedCampVisits ?? [])];
cloned.completedCampDialogues = uniqueStrings(cloned.completedCampDialogues);
cloned.completedCampVisits = uniqueStrings(cloned.completedCampVisits);
if (cloned.campaignRewards) {
cloned.campaignRewards = cloneCampaignRewardSnapshot(cloned.campaignRewards);
}
@@ -1544,17 +1588,19 @@ function cloneCampaignRewardSnapshot(rewards?: CampaignRewardSnapshot): Campaign
recruits: arrayOrEmpty<unknown>(rewardRecord.recruits)
.filter(isPlainObject)
.map((recruit) => ({
unitId: typeof recruit.unitId === 'string' ? recruit.unitId.trim() : '',
name: typeof recruit.name === 'string' ? recruit.name.trim() : ''
unitId: normalizeKeyString(recruit.unitId),
name: normalizeDisplayString(recruit.name)
}))
.filter((recruit) => recruit.unitId && recruit.name),
.filter((recruit) => recruit.unitId && recruit.name)
.slice(0, maxCampaignStringListEntries),
unlocks: arrayOrEmpty<unknown>(rewardRecord.unlocks)
.filter(isPlainObject)
.map((unlock) => ({
battleId: typeof unlock.battleId === 'string' ? unlock.battleId.trim() : '',
title: typeof unlock.title === 'string' ? unlock.title.trim() : ''
battleId: normalizeKeyString(unlock.battleId),
title: normalizeDisplayString(unlock.title)
}))
.filter((unlock) => unlock.battleId && unlock.title),
.filter((unlock) => unlock.battleId && unlock.title)
.slice(0, maxCampaignStringListEntries),
note: note.length > 0 && note.length <= maxCampaignRewardNoteLength ? note : undefined
};
}