feat: add playable inter-battle city stays

This commit is contained in:
2026-07-24 02:20:10 +09:00
parent df1322b1f0
commit 412b8691b5
10 changed files with 3605 additions and 589 deletions

View File

@@ -1,6 +1,7 @@
import { equipmentExpToNext, equipmentSlots, itemCatalog, type EquipmentSlot } from '../data/battleItems';
import { unitClasses } from '../data/battleRules';
import { battleScenarios, type BattleScenarioId } from '../data/battles';
import { findCityStayAfterBattle, type CityStayId } from '../data/cityStays';
import {
campaignRecruitUnits,
jianYongRecruitBond,
@@ -508,6 +509,7 @@ export type CampaignState = {
battleHistory: Record<string, CampaignBattleSettlement>;
latestBattleId?: string;
pendingAftermathBattleId?: BattleScenarioId;
activeCityStayId?: CityStayId;
firstBattleReport?: FirstBattleReport;
};
@@ -808,6 +810,22 @@ export function listCampaignSaveSlots(): CampaignSaveSlotSummary[] {
export function markCampaignStep(step: CampaignStep) {
const state = ensureCampaignState();
state.step = step;
if (!normalizeActiveCityStayId(state.activeCityStayId, state.latestBattleId, step)) {
delete state.activeCityStayId;
}
state.updatedAt = new Date().toISOString();
persistCampaignState(state);
return cloneCampaignState(state);
}
export function setActiveCityStayId(cityStayId?: CityStayId) {
const state = ensureCampaignState();
const normalizedCityStayId = normalizeActiveCityStayId(cityStayId, state.latestBattleId, state.step);
if (normalizedCityStayId) {
state.activeCityStayId = normalizedCityStayId;
} else {
delete state.activeCityStayId;
}
state.updatedAt = new Date().toISOString();
persistCampaignState(state);
return cloneCampaignState(state);
@@ -1026,6 +1044,7 @@ export function setFirstBattleReport(report: FirstBattleReport) {
}
state.step = retryStep;
state.latestBattleId = battleId;
delete state.activeCityStayId;
delete state.pendingAftermathBattleId;
state.updatedAt = new Date().toISOString();
persistCampaignState(state);
@@ -1075,6 +1094,7 @@ export function setFirstBattleReport(report: FirstBattleReport) {
delete state.sortieRecommendationSelection;
}
state.latestBattleId = battleId;
delete state.activeCityStayId;
state.pendingAftermathBattleId = battleId as BattleScenarioId;
state.updatedAt = new Date().toISOString();
@@ -1728,6 +1748,16 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
);
}
normalized.latestBattleId = normalizeLatestBattleId(normalized.latestBattleId, normalized.battleHistory);
const activeCityStayId = normalizeActiveCityStayId(
normalized.activeCityStayId,
normalized.latestBattleId,
normalized.step
);
if (activeCityStayId) {
normalized.activeCityStayId = activeCityStayId;
} else {
delete normalized.activeCityStayId;
}
const pendingAftermathBattleId = typeof normalized.pendingAftermathBattleId === 'string' &&
normalized.pendingAftermathBattleId in battleScenarios
? normalized.pendingAftermathBattleId as BattleScenarioId
@@ -3228,6 +3258,27 @@ function cloneCampaignState(state: CampaignState): CampaignState {
return JSON.parse(JSON.stringify(state)) as CampaignState;
}
function normalizeActiveCityStayId(
cityStayId: unknown,
latestBattleId: string | undefined,
step: CampaignStep
): CityStayId | undefined {
if (typeof cityStayId !== 'string' || !latestBattleId || !isCampCampaignStep(step)) {
return undefined;
}
if (!(latestBattleId in battleScenarios)) {
return undefined;
}
const cityStay = findCityStayAfterBattle(latestBattleId as BattleScenarioId);
const expectedCampStep = cityStay
? campaignBattleSteps[cityStay.afterBattleId]?.victory
: undefined;
return cityStay?.id === cityStayId && expectedCampStep === step
? cityStay.id
: undefined;
}
function cloneReport(report: FirstBattleReport): FirstBattleReport {
const cloned = JSON.parse(JSON.stringify(report)) as FirstBattleReport;
cloned.itemRewards = normalizeRewardStrings(cloned.itemRewards);