Add selectable reserve drill focus
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user