Add selectable reserve drill focus

This commit is contained in:
2026-06-23 17:34:45 +09:00
parent 0736f07d1d
commit 8591fd7fa3
4 changed files with 242 additions and 21 deletions

View File

@@ -152,10 +152,53 @@ export type CampaignReserveTrainingSnapshot = {
name: string;
expGained: number;
equipmentExpGained: number;
bondExpGained?: number;
focusId?: CampaignReserveTrainingFocusId;
focusLabel?: string;
level: number;
exp: number;
};
export type CampaignReserveTrainingFocusId = 'balanced' | 'class-practice' | 'bond-practice';
export type CampaignReserveTrainingFocusDefinition = {
id: CampaignReserveTrainingFocusId;
label: string;
summary: string;
expGained: number;
equipmentExpGained: number;
bondExpGained: number;
};
export const defaultCampaignReserveTrainingFocusId: CampaignReserveTrainingFocusId = 'balanced';
export const campaignReserveTrainingFocusDefinitions: CampaignReserveTrainingFocusDefinition[] = [
{
id: 'balanced',
label: '균형 훈련',
summary: '대기 무장의 경험치와 장비 숙련을 고르게 올립니다.',
expGained: 6,
equipmentExpGained: 2,
bondExpGained: 0
},
{
id: 'class-practice',
label: '병과 숙련',
summary: '대기 무장의 장비 경험치를 더 올려 병과 운용을 다듬습니다.',
expGained: 4,
equipmentExpGained: 5,
bondExpGained: 0
},
{
id: 'bond-practice',
label: '공명 합숙',
summary: '대기 무장의 개인 성장은 낮지만 관련 공명 경험치를 함께 올립니다.',
expGained: 5,
equipmentExpGained: 1,
bondExpGained: 4
}
];
export type CampaignBattleSettlement = {
battleId: string;
battleTitle: string;
@@ -179,6 +222,7 @@ export type CampaignState = {
bonds: CampBondSnapshot[];
inventory: Record<string, number>;
selectedSortieUnitIds: string[];
reserveTrainingFocus: CampaignReserveTrainingFocusId;
completedCampDialogues: string[];
completedCampVisits: string[];
battleHistory: Record<string, CampaignBattleSettlement>;
@@ -256,6 +300,7 @@ export function createInitialCampaignState(): CampaignState {
bonds: [],
inventory: {},
selectedSortieUnitIds: [],
reserveTrainingFocus: defaultCampaignReserveTrainingFocusId,
completedCampDialogues: [],
completedCampVisits: [],
battleHistory: {}
@@ -331,6 +376,14 @@ export function markCampaignStep(step: CampaignStep) {
return cloneCampaignState(state);
}
export function setCampaignReserveTrainingFocus(focusId: CampaignReserveTrainingFocusId) {
const state = ensureCampaignState();
state.reserveTrainingFocus = normalizeReserveTrainingFocusId(focusId);
state.updatedAt = new Date().toISOString();
persistCampaignState(state);
return cloneCampaignState(state);
}
export function setFirstBattleReport(report: FirstBattleReport) {
const state = ensureCampaignState();
const reportClone = cloneReport(report);
@@ -350,10 +403,18 @@ export function setFirstBattleReport(report: FirstBattleReport) {
state.step = reportClone.outcome === 'victory' ? victoryStep : retryStep;
state.gold = Math.max(0, state.gold - (previousSettlement?.rewardGold ?? 0) + reportClone.rewardGold);
state.roster = mergeRosterProgress(state.roster, reportClone.units.filter((unit) => unit.faction === 'ally'));
state.bonds = reportClone.bonds.map(cloneBondSnapshot);
const reserveTraining = previousSettlement || reportClone.outcome !== 'victory'
? previousSettlement?.reserveTraining ?? []
: applyReserveTrainingProgress(state.roster, reportClone.units.filter((unit) => unit.faction === 'ally'));
state.bonds = reportClone.bonds.map(cloneBondSnapshot);
: applyReserveTrainingProgress(
state.roster,
state.bonds,
reportClone.units.filter((unit) => unit.faction === 'ally'),
state.reserveTrainingFocus
);
if (!previousSettlement && reportClone.outcome === 'victory') {
reportClone.bonds = state.bonds.map(cloneBondSnapshot);
}
state.completedCampDialogues = completedCampDialogues;
state.completedCampVisits = completedCampVisits;
state.inventory = applyRewardDelta(state.inventory, previousSettlement?.itemRewards ?? [], -1);
@@ -516,6 +577,7 @@ function normalizeCampaignState(state: CampaignState): CampaignState {
normalized.completedCampVisits = [...new Set(normalized.completedCampVisits ?? [])];
normalized.inventory = { ...(normalized.inventory ?? {}) };
normalized.selectedSortieUnitIds = [...new Set(normalized.selectedSortieUnitIds ?? [])];
normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus);
normalized.battleHistory = normalized.battleHistory ?? {};
if (normalized.firstBattleReport) {
normalized.firstBattleReport = cloneReport(normalized.firstBattleReport);
@@ -523,6 +585,17 @@ function normalizeCampaignState(state: CampaignState): CampaignState {
return normalized;
}
function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrainingFocusId {
return campaignReserveTrainingFocusDefinitions.some((focus) => focus.id === focusId)
? focusId as CampaignReserveTrainingFocusId
: defaultCampaignReserveTrainingFocusId;
}
function reserveTrainingFocusDefinition(focusId?: string) {
const normalizedId = normalizeReserveTrainingFocusId(focusId);
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0];
}
function readStoredCampaignState(slot?: number) {
const raw = tryStorage()?.getItem(slot ? campaignSaveSlotKey(slot) : campaignStorageKey);
if (!raw) {
@@ -629,13 +702,35 @@ function mergeRosterProgress(currentRoster: UnitData[], deployedUnits: UnitData[
return Array.from(merged.values());
}
function applyReserveTrainingProgress(roster: UnitData[], deployedUnits: UnitData[]): CampaignReserveTrainingSnapshot[] {
function applyReserveTrainingProgress(
roster: UnitData[],
bonds: CampBondSnapshot[],
deployedUnits: UnitData[],
focusId: CampaignReserveTrainingFocusId
): CampaignReserveTrainingSnapshot[] {
const focus = reserveTrainingFocusDefinition(focusId);
const deployedIds = new Set(deployedUnits.map((unit) => unit.id));
return roster
.filter((unit) => unit.faction === 'ally' && !deployedIds.has(unit.id))
const reserveUnits = roster.filter((unit) => unit.faction === 'ally' && !deployedIds.has(unit.id));
const reserveIds = new Set(reserveUnits.map((unit) => unit.id));
const bondExpByUnit = new Map<string, number>();
if (focus.bondExpGained > 0 && reserveIds.size > 0) {
bonds.forEach((bond) => {
const reservePartners = bond.unitIds.filter((unitId) => reserveIds.has(unitId));
if (reservePartners.length === 0) {
return;
}
advanceBond(bond, focus.bondExpGained);
reservePartners.forEach((unitId) => {
bondExpByUnit.set(unitId, (bondExpByUnit.get(unitId) ?? 0) + focus.bondExpGained);
});
});
}
return reserveUnits
.map((unit) => {
const expGained = 6;
const equipmentExpGained = 2;
const expGained = focus.expGained;
const equipmentExpGained = focus.equipmentExpGained;
advanceUnitExp(unit, expGained);
advanceReserveEquipment(unit, equipmentExpGained);
return {
@@ -643,6 +738,9 @@ function applyReserveTrainingProgress(roster: UnitData[], deployedUnits: UnitDat
name: unit.name,
expGained,
equipmentExpGained,
bondExpGained: bondExpByUnit.get(unit.id) ?? 0,
focusId: focus.id,
focusLabel: focus.label,
level: unit.level,
exp: unit.exp
};