feat: improve first-session combat and visual assets

This commit is contained in:
2026-07-18 22:11:50 +09:00
parent 4a2a0dfb19
commit f29956b90e
82 changed files with 3640 additions and 839 deletions

View File

@@ -644,7 +644,7 @@ try {
const crossBattleResonance = Object.entries(battleScenarios)
.filter(([battleId]) => battleId !== 'first-battle-zhuo-commandery')
.flatMap(([battleId, scenario]) => scenario.bonds.map((bond) => ({ battleId, bond })))
.find(({ bond }) => bond.level >= 30 && !firstBattleBondIds.has(bond.id));
.find(({ bond }) => bond.level >= 30 && bond.id !== 'liu-bei__jian-yong' && !firstBattleBondIds.has(bond.id));
assert(crossBattleResonance, 'Expected campaign scenarios to expose a later-battle-only eligible bond fixture.');
const crossBattleResonanceFixture = {
...coreResonanceFixture,
@@ -693,10 +693,10 @@ try {
);
const reloadedScenarioOnlyResonance = loadCampaignState();
assert(
normalizedScenarioOnlyResonance.bonds.length === 0 &&
!normalizedScenarioOnlyResonance.bonds.some((bond) => bond.id === crossBattleResonance.bond.id) &&
normalizedScenarioOnlyResonance.sortieResonanceSelection?.bondId === crossBattleResonance.bond.id &&
selectedScenarioOnlyResonance.sortieResonanceSelection?.battleId === crossBattleResonance.battleId &&
reloadedScenarioOnlyResonance.bonds.length === 0 &&
!reloadedScenarioOnlyResonance.bonds.some((bond) => bond.id === crossBattleResonance.bond.id) &&
reloadedScenarioOnlyResonance.sortieResonanceSelection?.bondId === crossBattleResonance.bond.id,
`Expected a scenario-owned eligible bond to remain selectable and reload safely before campaign progress contains it: ${JSON.stringify({ normalizedScenarioOnlyResonance, selectedScenarioOnlyResonance, reloadedScenarioOnlyResonance })}`
);
@@ -848,6 +848,87 @@ try {
completedCampVisits: [],
createdAt: '2026-07-03T09:00:00.000Z'
};
setFirstBattleReport(firstBattleReport);
const preJianYongSave = JSON.parse(storage.get(campaignStorageKey));
preJianYongSave.roster = preJianYongSave.roster.filter((unit) => unit.id !== 'jian-yong');
preJianYongSave.bonds = preJianYongSave.bonds.filter((bond) => bond.id !== 'liu-bei__jian-yong');
preJianYongSave.firstBattleReport.bonds = preJianYongSave.firstBattleReport.bonds.filter((bond) => bond.id !== 'liu-bei__jian-yong');
preJianYongSave.firstBattleReport.campaignRewards.recruits = preJianYongSave.firstBattleReport.campaignRewards.recruits.filter((recruit) => recruit.unitId !== 'jian-yong');
preJianYongSave.battleHistory[firstScenario.id].bonds = preJianYongSave.battleHistory[firstScenario.id].bonds.filter((bond) => bond.id !== 'liu-bei__jian-yong');
preJianYongSave.battleHistory[firstScenario.id].campaignRewards.recruits = preJianYongSave.battleHistory[firstScenario.id].campaignRewards.recruits.filter((recruit) => recruit.unitId !== 'jian-yong');
storage.set(campaignStorageKey, JSON.stringify(preJianYongSave));
const repairedEarlyChoiceSave = loadCampaignState();
const persistedEarlyChoiceSave = saveCampaignState(repairedEarlyChoiceSave);
assert(
persistedEarlyChoiceSave.roster.filter((unit) => unit.id === 'jian-yong').length === 1 &&
persistedEarlyChoiceSave.bonds.filter((bond) => bond.id === 'liu-bei__jian-yong').length === 1 &&
persistedEarlyChoiceSave.firstBattleReport?.bonds.filter((bond) => bond.id === 'liu-bei__jian-yong').length === 1 &&
persistedEarlyChoiceSave.firstBattleReport?.campaignRewards?.recruits.filter((recruit) => recruit.unitId === 'jian-yong').length === 1 &&
persistedEarlyChoiceSave.battleHistory[firstScenario.id]?.bonds.filter((bond) => bond.id === 'liu-bei__jian-yong').length === 1 &&
persistedEarlyChoiceSave.battleHistory[firstScenario.id]?.campaignRewards?.recruits.filter((recruit) => recruit.unitId === 'jian-yong').length === 1,
`Expected pre-Jian-Yong first-victory saves to backfill one roster member, reward, and resonance everywhere: ${JSON.stringify(persistedEarlyChoiceSave)}`
);
resetCampaignState();
const preservedGlobalBondCount = 157;
const preservedGlobalBonds = Array.from({ length: preservedGlobalBondCount }, (_, index) => ({
id: `legacy-global-bond-${index}`,
unitIds: ['liu-bei', 'guan-yu'],
title: `Legacy global bond ${index}`,
level: 1,
exp: index,
description: `Late-campaign global bond fixture ${index}`,
battleExp: 0
}));
const rewardRecruitLimit = 128;
const preservedRewardRecruits = Array.from({ length: rewardRecruitLimit }, (_, index) => ({
unitId: `legacy-reward-recruit-${index}`,
name: `Legacy reward recruit ${index}`
}));
const largePreJianYongSave = JSON.parse(JSON.stringify(preJianYongSave));
largePreJianYongSave.bonds = preservedGlobalBonds;
largePreJianYongSave.firstBattleReport.bonds = preservedGlobalBonds.slice(0, 96);
largePreJianYongSave.battleHistory[firstScenario.id].bonds = preservedGlobalBonds.slice(0, 96);
largePreJianYongSave.firstBattleReport.campaignRewards.recruits = preservedRewardRecruits;
largePreJianYongSave.battleHistory[firstScenario.id].campaignRewards.recruits = preservedRewardRecruits;
const originalFirstScenarioUnitCount = firstScenario.units.length;
firstScenario.units.push(...preservedRewardRecruits.map((recruit) => ({ id: recruit.unitId, faction: 'ally' })));
let repairedLargeEarlyChoiceSave;
try {
storage.set(campaignStorageKey, JSON.stringify(largePreJianYongSave));
repairedLargeEarlyChoiceSave = saveCampaignState(loadCampaignState());
} finally {
firstScenario.units.splice(originalFirstScenarioUnitCount);
}
const repairedLargeGlobalBondIds = new Set(repairedLargeEarlyChoiceSave.bonds.map((bond) => bond.id));
assert(
repairedLargeEarlyChoiceSave.bonds.length === preservedGlobalBondCount + 1 &&
preservedGlobalBonds.every((bond) => repairedLargeGlobalBondIds.has(bond.id)) &&
repairedLargeEarlyChoiceSave.bonds.filter((bond) => bond.id === 'liu-bei__jian-yong').length === 1,
`Expected all ${preservedGlobalBondCount} late-campaign global bonds to survive Jian Yong backfill: ${JSON.stringify(repairedLargeEarlyChoiceSave.bonds)}`
);
assert(
repairedLargeEarlyChoiceSave.firstBattleReport?.bonds.length === 96 &&
repairedLargeEarlyChoiceSave.firstBattleReport.bonds.some((bond) => bond.id === 'liu-bei__jian-yong') &&
repairedLargeEarlyChoiceSave.battleHistory[firstScenario.id]?.bonds.length === 96 &&
repairedLargeEarlyChoiceSave.battleHistory[firstScenario.id].bonds.some((bond) => bond.id === 'liu-bei__jian-yong'),
`Expected report and settlement bond snapshots to retain their 96-entry cap while adding Jian Yong: ${JSON.stringify({ report: repairedLargeEarlyChoiceSave.firstBattleReport?.bonds, settlement: repairedLargeEarlyChoiceSave.battleHistory[firstScenario.id]?.bonds })}`
);
const repairedReportRecruits = repairedLargeEarlyChoiceSave.firstBattleReport?.campaignRewards?.recruits ?? [];
const repairedSettlementRecruits = repairedLargeEarlyChoiceSave.battleHistory[firstScenario.id]?.campaignRewards?.recruits ?? [];
const preservedRewardRecruitIds = new Set(preservedRewardRecruits.slice(0, rewardRecruitLimit - 1).map((recruit) => recruit.unitId));
assert(
repairedReportRecruits.length === rewardRecruitLimit &&
repairedSettlementRecruits.length === rewardRecruitLimit &&
repairedReportRecruits.filter((recruit) => recruit.unitId === 'jian-yong').length === 1 &&
repairedSettlementRecruits.filter((recruit) => recruit.unitId === 'jian-yong').length === 1 &&
[...preservedRewardRecruitIds].every((unitId) => repairedReportRecruits.some((recruit) => recruit.unitId === unitId)) &&
[...preservedRewardRecruitIds].every((unitId) => repairedSettlementRecruits.some((recruit) => recruit.unitId === unitId)) &&
!repairedReportRecruits.some((recruit) => recruit.unitId === preservedRewardRecruits[rewardRecruitLimit - 1].unitId) &&
!repairedSettlementRecruits.some((recruit) => recruit.unitId === preservedRewardRecruits[rewardRecruitLimit - 1].unitId),
`Expected 128-entry reward recruit snapshots to preserve 127 valid entries and append required Jian Yong without exceeding the schema cap: ${JSON.stringify({ report: repairedReportRecruits, settlement: repairedSettlementRecruits })}`
);
resetCampaignState();
const performanceUnit = alliedFirstBattleUnits.find((unit) => unit.id === 'liu-bei') ?? alliedFirstBattleUnits[0];
const recommendedButNotDeployedUnit = alliedFirstBattleUnits.find((unit) => unit.id !== performanceUnit?.id);
assert(performanceUnit, 'Expected an allied first-battle unit for sortie performance clone checks');
@@ -971,10 +1052,10 @@ try {
);
const individualizedReserveBond = individualizedReserveTraining.bonds.find((bond) => bond.id === 'reserve-drill-bond');
assert(
individualizedReserveAward?.focusId === 'bond-practice' &&
individualizedReserveAward?.focusId === 'bond-practice' &&
individualizedReserveAward.expGained === 5 &&
individualizedReserveAward.equipmentExpGained === 1 &&
individualizedReserveAward.bondExpGained === 4 &&
individualizedReserveAward.bondExpGained === 8 &&
individualizedReserveBond?.exp === 4,
`Expected per-officer reserve drill assignments to override the global focus and apply bond growth during battle settlement: ${JSON.stringify(individualizedReserveTraining)}`
);
@@ -1754,8 +1835,10 @@ try {
);
const staleReportRefs = loadCampaignState();
assert(
staleReportRefs.firstBattleReport?.bonds.length === 1 &&
staleReportRefs.firstBattleReport.bonds[0].id === 'liu-bei__guan-yu' &&
staleReportRefs.firstBattleReport?.bonds.length === 2 &&
staleReportRefs.firstBattleReport.bonds.some((bond) => bond.id === 'liu-bei__guan-yu') &&
staleReportRefs.firstBattleReport.bonds.some((bond) => bond.id === 'liu-bei__jian-yong') &&
!staleReportRefs.firstBattleReport.bonds.some((bond) => bond.id === 'liu-bei__ghost') &&
staleReportRefs.firstBattleReport.mvp === undefined &&
JSON.stringify(staleReportRefs.firstBattleReport.sortiePerformance?.selectedUnitIds) === JSON.stringify(['liu-bei']) &&
JSON.stringify(staleReportRefs.firstBattleReport.sortiePerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) &&
@@ -2015,10 +2098,11 @@ try {
malformedReport.firstBattleReport.campaignRewards?.supplies[0] === 'Bean' &&
malformedReport.firstBattleReport.campaignRewards?.equipment.length === 1 &&
malformedReport.firstBattleReport.campaignRewards?.reputation.length === 1 &&
malformedReport.firstBattleReport.campaignRewards?.recruits.length === 1 &&
malformedReport.firstBattleReport.campaignRewards?.recruits.length === 2 &&
malformedReport.firstBattleReport.campaignRewards?.recruits[0].unitId === 'guan-yu' &&
malformedReport.firstBattleReport.campaignRewards?.recruits[0].name === 'Guan Yu' &&
malformedReport.firstBattleReport.campaignRewards?.recruits.filter((recruit) => recruit.unitId === 'guan-yu').length === 1 &&
malformedReport.firstBattleReport.campaignRewards?.recruits.filter((recruit) => recruit.unitId === 'jian-yong').length === 1 &&
malformedReport.firstBattleReport.campaignRewards?.unlocks.length === 1 &&
malformedReport.firstBattleReport.campaignRewards?.unlocks[0].battleId === 'second-battle-yellow-turban-pursuit' &&
malformedReport.firstBattleReport.campaignRewards?.unlocks[0].title === 'Next' &&
@@ -2259,8 +2343,9 @@ try {
assert(
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.itemRewards.length === 1 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.equipment[0] === 'Iron Sword' &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits.length === 1 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits.length === 2 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits[0].unitId === 'guan-yu' &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits.some((recruit) => recruit.unitId === 'jian-yong') &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.unlocks.length === 1 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.unlocks[0].battleId === 'second-battle-yellow-turban-pursuit' &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.note === undefined,
@@ -2283,10 +2368,11 @@ try {
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].equipment.weapon.itemId === 'training-sword' &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].equipment.armor.itemId === 'cloth-armor' &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].equipment.accessory.itemId === 'grain-pouch' &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds.length === 1 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds.length === 2 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds[0].level === 1 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds[0].exp === 99 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds[0].battleExp === 4 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds.some((bond) => bond.id === 'liu-bei__jian-yong') &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.reserveTraining?.length === 1 &&
malformedHistory.battleHistory['first-battle-zhuo-commandery']?.reserveTraining?.[0].focusId === 'balanced',
`Expected nested battle history progress arrays to filter invalid entries and normalize numbers: ${JSON.stringify(malformedHistory)}`