Add officer roster reserve growth
This commit is contained in:
@@ -42,14 +42,15 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
|||||||
- Eighteenth battle Bowang ambush as the first Red Cliffs preparation battle, with a new large battlefield, Xiahou Dun's vanguard, mixed enemy AI, Zhuge Liang sortie recommendation, and post-battle camp events
|
- Eighteenth battle Bowang ambush as the first Red Cliffs preparation battle, with a new large battlefield, Xiahou Dun's vanguard, mixed enemy AI, Zhuge Liang sortie recommendation, and post-battle camp events
|
||||||
- Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter
|
- Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter
|
||||||
- Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer
|
- Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer
|
||||||
|
- Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers
|
||||||
- Flow verification script from title through the eighteenth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang camp state, campaign timeline state, and camp save state
|
- Flow verification script from title through the eighteenth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang camp state, campaign timeline state, and camp save state
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Continue the Red Cliffs preparation chapter into Changban and the Jiangdong envoy route
|
1. Continue the Red Cliffs preparation chapter into Changban and the Jiangdong envoy route
|
||||||
2. Add a richer officer collection and reserve growth view so benched recruitable officers still feel valuable
|
2. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
||||||
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
3. Add more recruitable officers as the campaign moves toward Red Cliffs, Jing Province, Yi Province, and Shu Han
|
||||||
4. Add more recruitable officers as the campaign moves toward Red Cliffs, Jing Province, Yi Province, and Shu Han
|
4. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments
|
||||||
5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending
|
5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending
|
||||||
|
|
||||||
## Content Direction
|
## Content Direction
|
||||||
|
|||||||
@@ -1848,12 +1848,33 @@ try {
|
|||||||
!eighteenthCampState.availableDialogueIds.every((id) => id.endsWith('bowang-ambush')) ||
|
!eighteenthCampState.availableDialogueIds.every((id) => id.endsWith('bowang-ambush')) ||
|
||||||
eighteenthCampState.availableVisitIds?.length !== 2 ||
|
eighteenthCampState.availableVisitIds?.length !== 2 ||
|
||||||
!eighteenthCampState.report?.bonds?.some((bond) => bond.id === 'zhang-fei__zhuge-liang') ||
|
!eighteenthCampState.report?.bonds?.some((bond) => bond.id === 'zhang-fei__zhuge-liang') ||
|
||||||
!eighteenthCampState.report?.bonds?.some((bond) => bond.id === 'sun-qian__zhuge-liang')
|
!eighteenthCampState.report?.bonds?.some((bond) => bond.id === 'sun-qian__zhuge-liang') ||
|
||||||
|
eighteenthCampState.rosterCollection?.total < 8 ||
|
||||||
|
eighteenthCampState.rosterCollection?.reserveTrainingCount < 2 ||
|
||||||
|
eighteenthCampState.rosterCollection?.reserveTrainingExp < 12 ||
|
||||||
|
!eighteenthCampState.reserveTrainingAwards?.some((entry) => entry.unitId === 'jian-yong' && entry.expGained === 6) ||
|
||||||
|
!eighteenthCampState.reserveTrainingAwards?.every((entry) => entry.expGained === 6 && entry.equipmentExpGained === 2) ||
|
||||||
|
!eighteenthCampState.reserveTrainingAwards?.some((entry) => ['mi-zhu', 'sun-qian'].includes(entry.unitId))
|
||||||
) {
|
) {
|
||||||
throw new Error(`Expected eighteenth camp to expose Bowang dialogue/visit sets and Zhuge bond expansion: ${JSON.stringify(eighteenthCampState)}`);
|
throw new Error(`Expected eighteenth camp to expose Bowang dialogue/visit sets, Zhuge bonds, and reserve training growth: ${JSON.stringify(eighteenthCampState)}`);
|
||||||
}
|
}
|
||||||
await page.screenshot({ path: 'dist/verification-bowang-camp.png', fullPage: true });
|
await page.screenshot({ path: 'dist/verification-bowang-camp.png', fullPage: true });
|
||||||
|
|
||||||
|
await page.mouse.click(654, 38);
|
||||||
|
await page.waitForTimeout(160);
|
||||||
|
await page.mouse.click(180, 315);
|
||||||
|
await page.waitForTimeout(160);
|
||||||
|
const bowangRosterState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (
|
||||||
|
bowangRosterState?.activeTab !== 'status' ||
|
||||||
|
bowangRosterState.selectedUnitId !== 'jian-yong' ||
|
||||||
|
bowangRosterState.rosterCollection?.reserveCount < 2 ||
|
||||||
|
!bowangRosterState.reserveTrainingAwards?.some((entry) => entry.unitId === 'jian-yong' && entry.expGained === 6)
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected status tab to expose officer roster reserve growth details: ${JSON.stringify(bowangRosterState)}`);
|
||||||
|
}
|
||||||
|
await page.screenshot({ path: 'dist/verification-bowang-roster.png', fullPage: true });
|
||||||
|
|
||||||
await page.mouse.click(966, 38);
|
await page.mouse.click(966, 38);
|
||||||
await page.waitForTimeout(180);
|
await page.waitForTimeout(180);
|
||||||
const postBowangProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
const postBowangProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
|||||||
@@ -3177,13 +3177,16 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private renderUnitColumn() {
|
private renderUnitColumn() {
|
||||||
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||||
const availableHeight = 462;
|
const availableHeight = 462;
|
||||||
const rowGap = allies.length > 1 ? Math.min(150, Math.floor(availableHeight / allies.length)) : 150;
|
const rowGap = allies.length > 1 ? Math.min(132, Math.floor(availableHeight / allies.length)) : 132;
|
||||||
const cardHeight = Math.min(126, Math.max(70, rowGap - 10));
|
const cardHeight = Math.min(118, Math.max(48, rowGap - 8));
|
||||||
const compact = cardHeight < 104;
|
const compact = cardHeight < 88;
|
||||||
|
this.renderRosterColumnSummary(42, 86, 318, allies);
|
||||||
allies.forEach((unit, index) => {
|
allies.forEach((unit, index) => {
|
||||||
const x = 42;
|
const x = 42;
|
||||||
const y = 120 + index * rowGap;
|
const y = 120 + index * rowGap;
|
||||||
const active = this.selectedUnitId === unit.id;
|
const active = this.selectedUnitId === unit.id;
|
||||||
|
const rosterStatus = this.unitRosterStatus(unit);
|
||||||
|
const training = this.latestReserveTraining(unit.id);
|
||||||
const bg = this.track(this.add.rectangle(x, y, 318, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
const bg = this.track(this.add.rectangle(x, y, 318, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(2, active ? palette.gold : palette.blue, active ? 0.9 : 0.46);
|
bg.setStrokeStyle(2, active ? palette.gold : palette.blue, active ? 0.9 : 0.46);
|
||||||
@@ -3196,22 +3199,63 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
const portraitKey = portraitByUnitId[unit.id];
|
const portraitKey = portraitByUnitId[unit.id];
|
||||||
const portraitCenterY = y + cardHeight / 2;
|
const portraitCenterY = y + cardHeight / 2;
|
||||||
const portraitSize = compact ? 62 : 92;
|
const portraitSize = compact ? 40 : 84;
|
||||||
if (portraitKey) {
|
if (portraitKey) {
|
||||||
const portrait = this.track(this.add.image(x + 58, portraitCenterY, portraitKeys[portraitKey]));
|
const portrait = this.track(this.add.image(x + 58, portraitCenterY, portraitKeys[portraitKey]));
|
||||||
portrait.setDisplaySize(portraitSize, portraitSize);
|
portrait.setDisplaySize(portraitSize, portraitSize);
|
||||||
} else {
|
} else {
|
||||||
this.renderUnitFallbackPortrait(x + 58, portraitCenterY, compact ? 31 : 44, unit);
|
this.renderUnitFallbackPortrait(x + 58, portraitCenterY, compact ? 20 : 40, unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const textX = compact ? x + 102 : x + 116;
|
const textX = compact ? x + 102 : x + 116;
|
||||||
this.track(this.add.text(textX, y + (compact ? 13 : 20), `${unit.name} Lv ${unit.level}`, this.textStyle(compact ? 17 : 21, '#f2e3bf', true)));
|
this.track(this.add.text(textX, y + (compact ? 7 : 17), `${unit.name} Lv ${unit.level}`, this.textStyle(compact ? 14 : 20, '#f2e3bf', true)));
|
||||||
this.track(this.add.text(textX, y + (compact ? 38 : 51), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 12 : 14, '#d4dce6')));
|
this.track(this.add.text(textX, y + (compact ? 27 : 47), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 10 : 13, '#d4dce6')));
|
||||||
this.track(this.add.text(textX, y + (compact ? 58 : 78), `경험 ${unit.exp}/100`, this.textStyle(compact ? 12 : 14, '#d8b15f', true)));
|
const expLine = training ? `경험 ${unit.exp}/100 · 대기 +${training.expGained}` : `경험 ${unit.exp}/100`;
|
||||||
this.drawBar(textX, y + cardHeight - 18, compact ? 160 : 176, 8, unit.exp / 100, palette.gold);
|
this.track(this.add.text(textX, y + (compact ? 40 : 72), expLine, this.textStyle(compact ? 10 : 13, training ? '#a8ffd0' : '#d8b15f', true)));
|
||||||
|
this.drawBar(textX, y + cardHeight - (compact ? 10 : 17), compact ? 142 : 176, compact ? 5 : 8, unit.exp / 100, training ? palette.green : palette.gold);
|
||||||
|
const badgeColor = rosterStatus.tone === 'selected' ? '#a8ffd0' : rosterStatus.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
||||||
|
const badge = this.track(this.add.text(x + 302, y + (compact ? 7 : 17), rosterStatus.label, this.textStyle(compact ? 10 : 11, badgeColor, true)));
|
||||||
|
badge.setOrigin(1, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderRosterColumnSummary(x: number, y: number, width: number, allies: UnitData[]) {
|
||||||
|
const selected = this.selectedSortieUnits();
|
||||||
|
const reserveCount = Math.max(0, allies.length - selected.length);
|
||||||
|
const recentTrainingCount = this.latestReserveTrainingAwards().length;
|
||||||
|
const bg = this.track(this.add.rectangle(x, y, width, 26, 0x0d141c, 0.92));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, palette.gold, 0.38);
|
||||||
|
this.track(this.add.text(x + 12, y + 6, `전군 ${allies.length} · 출전 ${selected.length} · 대기 ${reserveCount}`, this.textStyle(11, '#f2e3bf', true)));
|
||||||
|
const trainingText = this.track(
|
||||||
|
this.add.text(
|
||||||
|
x + width - 12,
|
||||||
|
y + 6,
|
||||||
|
recentTrainingCount > 0 ? `최근 훈련 ${recentTrainingCount}` : '훈련 대기',
|
||||||
|
this.textStyle(11, recentTrainingCount > 0 ? '#a8ffd0' : '#9fb0bf', true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
trainingText.setOrigin(1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unitRosterStatus(unit: UnitData) {
|
||||||
|
const selected = this.isSortieSelected(unit.id);
|
||||||
|
const recommendation = this.sortieRecommendation(unit.id);
|
||||||
|
if (requiredSortieUnitIds.has(unit.id)) {
|
||||||
|
return { label: '필수', tone: 'recommended' as const };
|
||||||
|
}
|
||||||
|
if (selected) {
|
||||||
|
return { label: recommendation ? '추천 출전' : '출전', tone: 'selected' as const };
|
||||||
|
}
|
||||||
|
if (recommendation) {
|
||||||
|
return { label: '추천 대기', tone: 'recommended' as const };
|
||||||
|
}
|
||||||
|
if (this.latestReserveTraining(unit.id)) {
|
||||||
|
return { label: '훈련', tone: 'selected' as const };
|
||||||
|
}
|
||||||
|
return { label: '대기', tone: 'reserve' as const };
|
||||||
|
}
|
||||||
|
|
||||||
private renderUnitFallbackPortrait(x: number, y: number, radius: number, unit: UnitData) {
|
private renderUnitFallbackPortrait(x: number, y: number, radius: number, unit: UnitData) {
|
||||||
const bg = this.track(this.add.circle(x, y, radius, 0x1f3140, 0.96));
|
const bg = this.track(this.add.circle(x, y, radius, 0x1f3140, 0.96));
|
||||||
bg.setStrokeStyle(2, palette.gold, 0.58);
|
bg.setStrokeStyle(2, palette.gold, 0.58);
|
||||||
@@ -3423,6 +3467,36 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return new Set(Object.keys(this.campaign?.battleHistory ?? {}));
|
return new Set(Object.keys(this.campaign?.battleHistory ?? {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private latestReserveTrainingAwards() {
|
||||||
|
const latestBattleId = this.campaign?.latestBattleId;
|
||||||
|
if (!latestBattleId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.campaign?.battleHistory[latestBattleId]?.reserveTraining ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private latestReserveTraining(unitId: string) {
|
||||||
|
return this.latestReserveTrainingAwards().find((entry) => entry.unitId === unitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private rosterCollectionSummary() {
|
||||||
|
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||||
|
const selectedIds = new Set(this.selectedSortieUnitIds);
|
||||||
|
const selectedCount = allies.filter((unit) => selectedIds.has(unit.id)).length;
|
||||||
|
const recruitedCount = allies.filter((unit) => !foundingSortieUnitIds.has(unit.id)).length;
|
||||||
|
const reserveTrainingAwards = this.latestReserveTrainingAwards();
|
||||||
|
const averageLevel = allies.length > 0 ? Math.round(allies.reduce((sum, unit) => sum + unit.level, 0) / allies.length) : 0;
|
||||||
|
return {
|
||||||
|
total: allies.length,
|
||||||
|
selectedCount,
|
||||||
|
reserveCount: Math.max(0, allies.length - selectedCount),
|
||||||
|
recruitedCount,
|
||||||
|
averageLevel,
|
||||||
|
reserveTrainingCount: reserveTrainingAwards.length,
|
||||||
|
reserveTrainingExp: reserveTrainingAwards.reduce((sum, entry) => sum + entry.expGained, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private renderStatusPanel() {
|
private renderStatusPanel() {
|
||||||
const unit = this.selectedUnit();
|
const unit = this.selectedUnit();
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
@@ -3472,12 +3546,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.renderStatCard(label, `${value}`, statX + (index % 2) * 128, y + 22 + Math.floor(index / 2) * 42, 116);
|
this.renderStatCard(label, `${value}`, statX + (index % 2) * 128, y + 22 + Math.floor(index / 2) * 42, 116);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.track(this.add.text(x + 24, y + 210, '장비', this.textStyle(20, '#f2e3bf', true)));
|
this.renderSelectedUnitRosterPlan(unit, x + 24, y + 196, 780);
|
||||||
|
|
||||||
|
this.track(this.add.text(x + 24, y + 224, '장비', this.textStyle(20, '#f2e3bf', true)));
|
||||||
equipmentSlots.forEach((slot, index) => {
|
equipmentSlots.forEach((slot, index) => {
|
||||||
this.renderEquipmentCard(unit, slot, x + 24 + index * 260, y + 244, 244, 102);
|
this.renderEquipmentCard(unit, slot, x + 24 + index * 260, y + 256, 244, 98);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderSelectedUnitBondPanel(unit, x + 24, y + 362, 780);
|
this.renderSelectedUnitBondPanel(unit, x + 24, y + 366, 780);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDialoguePanel() {
|
private renderDialoguePanel() {
|
||||||
@@ -3974,6 +4050,23 @@ export class CampScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderSelectedUnitRosterPlan(unit: UnitData, x: number, y: number, width: number) {
|
||||||
|
const status = this.unitRosterStatus(unit);
|
||||||
|
const training = this.latestReserveTraining(unit.id);
|
||||||
|
const role = this.sortieFormationRole(unit);
|
||||||
|
const roleLabel = sortieFormationSlotDefinitions.find((slot) => slot.role === role)?.label ?? '예비';
|
||||||
|
const recommendation = this.sortieRecommendation(unit.id);
|
||||||
|
const bg = this.track(this.add.rectangle(x, y, width, 22, 0x151f2a, 0.82));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, training ? palette.green : palette.blue, training ? 0.5 : 0.34);
|
||||||
|
const planText = recommendation ? `추천: ${recommendation.reason}` : `${roleLabel} 역할로 편성 가능`;
|
||||||
|
const trainingText = training ? `최근 대기 훈련 경험 +${training.expGained}, 장비 +${training.equipmentExpGained}` : '대기 중에도 전투 후 소량의 훈련 성장이 반영됩니다.';
|
||||||
|
this.track(this.add.text(x + 12, y + 5, `${status.label} · ${roleLabel}`, this.textStyle(11, status.tone === 'reserve' ? '#9fb0bf' : '#f2e3bf', true)));
|
||||||
|
this.track(this.add.text(x + 112, y + 5, this.compactText(planText, 48), this.textStyle(11, '#d4dce6')));
|
||||||
|
const trainingLabel = this.track(this.add.text(x + width - 12, y + 5, this.compactText(trainingText, 28), this.textStyle(11, training ? '#a8ffd0' : '#9fb0bf', true)));
|
||||||
|
trainingLabel.setOrigin(1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private itemBonusText(item: ReturnType<typeof getItem>) {
|
private itemBonusText(item: ReturnType<typeof getItem>) {
|
||||||
const bonuses = [
|
const bonuses = [
|
||||||
item.attackBonus ? `공격 +${item.attackBonus}` : '',
|
item.attackBonus ? `공격 +${item.attackBonus}` : '',
|
||||||
@@ -4278,6 +4371,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
sortiePlan: this.sortiePlanSummary(),
|
sortiePlan: this.sortiePlanSummary(),
|
||||||
|
rosterCollection: this.rosterCollectionSummary(),
|
||||||
|
reserveTrainingAwards: this.latestReserveTrainingAwards(),
|
||||||
campaignProgress: this.campaignTimelineProgress(),
|
campaignProgress: this.campaignTimelineProgress(),
|
||||||
campBattleId: this.currentCampBattleId(),
|
campBattleId: this.currentCampBattleId(),
|
||||||
campTitle: this.currentCampTitle(),
|
campTitle: this.currentCampTitle(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { equipmentExpToNext, equipmentSlots } from '../data/battleItems';
|
||||||
import type { BattleBond, UnitData } from '../data/scenario';
|
import type { BattleBond, UnitData } from '../data/scenario';
|
||||||
|
|
||||||
export type BattleObjectiveSnapshot = {
|
export type BattleObjectiveSnapshot = {
|
||||||
@@ -96,6 +97,15 @@ export type CampaignBondProgressSnapshot = {
|
|||||||
battleExp: number;
|
battleExp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CampaignReserveTrainingSnapshot = {
|
||||||
|
unitId: string;
|
||||||
|
name: string;
|
||||||
|
expGained: number;
|
||||||
|
equipmentExpGained: number;
|
||||||
|
level: number;
|
||||||
|
exp: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type CampaignBattleSettlement = {
|
export type CampaignBattleSettlement = {
|
||||||
battleId: string;
|
battleId: string;
|
||||||
battleTitle: string;
|
battleTitle: string;
|
||||||
@@ -105,6 +115,7 @@ export type CampaignBattleSettlement = {
|
|||||||
objectives: BattleObjectiveSnapshot[];
|
objectives: BattleObjectiveSnapshot[];
|
||||||
units: CampaignUnitProgressSnapshot[];
|
units: CampaignUnitProgressSnapshot[];
|
||||||
bonds: CampaignBondProgressSnapshot[];
|
bonds: CampaignBondProgressSnapshot[];
|
||||||
|
reserveTraining?: CampaignReserveTrainingSnapshot[];
|
||||||
completedAt: string;
|
completedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,12 +276,15 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
|||||||
state.step = reportClone.outcome === 'victory' ? victoryStep : retryStep;
|
state.step = reportClone.outcome === 'victory' ? victoryStep : retryStep;
|
||||||
state.gold = Math.max(0, state.gold - (previousSettlement?.rewardGold ?? 0) + reportClone.rewardGold);
|
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.roster = mergeRosterProgress(state.roster, reportClone.units.filter((unit) => unit.faction === 'ally'));
|
||||||
|
const reserveTraining = previousSettlement || reportClone.outcome !== 'victory'
|
||||||
|
? previousSettlement?.reserveTraining ?? []
|
||||||
|
: applyReserveTrainingProgress(state.roster, reportClone.units.filter((unit) => unit.faction === 'ally'));
|
||||||
state.bonds = reportClone.bonds.map(cloneBondSnapshot);
|
state.bonds = reportClone.bonds.map(cloneBondSnapshot);
|
||||||
state.completedCampDialogues = completedCampDialogues;
|
state.completedCampDialogues = completedCampDialogues;
|
||||||
state.completedCampVisits = completedCampVisits;
|
state.completedCampVisits = completedCampVisits;
|
||||||
state.inventory = applyRewardDelta(state.inventory, previousSettlement?.itemRewards ?? [], -1);
|
state.inventory = applyRewardDelta(state.inventory, previousSettlement?.itemRewards ?? [], -1);
|
||||||
state.inventory = applyRewardDelta(state.inventory, reportClone.itemRewards, 1);
|
state.inventory = applyRewardDelta(state.inventory, reportClone.itemRewards, 1);
|
||||||
state.battleHistory[battleId] = createBattleSettlement(reportClone);
|
state.battleHistory[battleId] = createBattleSettlement(reportClone, reserveTraining);
|
||||||
state.latestBattleId = battleId;
|
state.latestBattleId = battleId;
|
||||||
state.updatedAt = new Date().toISOString();
|
state.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
@@ -492,7 +506,7 @@ function advanceBond(bond: CampBondSnapshot, amount: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBattleSettlement(report: FirstBattleReport): CampaignBattleSettlement {
|
function createBattleSettlement(report: FirstBattleReport, reserveTraining: CampaignReserveTrainingSnapshot[] = []): CampaignBattleSettlement {
|
||||||
return {
|
return {
|
||||||
battleId: report.battleId,
|
battleId: report.battleId,
|
||||||
battleTitle: report.battleTitle,
|
battleTitle: report.battleTitle,
|
||||||
@@ -518,6 +532,7 @@ function createBattleSettlement(report: FirstBattleReport): CampaignBattleSettle
|
|||||||
exp: bond.exp,
|
exp: bond.exp,
|
||||||
battleExp: bond.battleExp
|
battleExp: bond.battleExp
|
||||||
})),
|
})),
|
||||||
|
reserveTraining: reserveTraining.map((entry) => ({ ...entry })),
|
||||||
completedAt: report.createdAt
|
completedAt: report.createdAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -540,6 +555,50 @@ function mergeRosterProgress(currentRoster: UnitData[], deployedUnits: UnitData[
|
|||||||
return Array.from(merged.values());
|
return Array.from(merged.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyReserveTrainingProgress(roster: UnitData[], deployedUnits: UnitData[]): CampaignReserveTrainingSnapshot[] {
|
||||||
|
const deployedIds = new Set(deployedUnits.map((unit) => unit.id));
|
||||||
|
return roster
|
||||||
|
.filter((unit) => unit.faction === 'ally' && !deployedIds.has(unit.id))
|
||||||
|
.map((unit) => {
|
||||||
|
const expGained = 6;
|
||||||
|
const equipmentExpGained = 2;
|
||||||
|
advanceUnitExp(unit, expGained);
|
||||||
|
advanceReserveEquipment(unit, equipmentExpGained);
|
||||||
|
return {
|
||||||
|
unitId: unit.id,
|
||||||
|
name: unit.name,
|
||||||
|
expGained,
|
||||||
|
equipmentExpGained,
|
||||||
|
level: unit.level,
|
||||||
|
exp: unit.exp
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceUnitExp(unit: UnitData, amount: number) {
|
||||||
|
let exp = unit.exp + amount;
|
||||||
|
while (unit.level < 99 && exp >= 100) {
|
||||||
|
exp -= 100;
|
||||||
|
unit.level += 1;
|
||||||
|
unit.maxHp += 2;
|
||||||
|
unit.hp = Math.min(unit.maxHp, unit.hp + 2);
|
||||||
|
unit.attack += 1;
|
||||||
|
}
|
||||||
|
unit.exp = unit.level >= 99 ? Math.min(exp, 100) : exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceReserveEquipment(unit: UnitData, amount: number) {
|
||||||
|
equipmentSlots.forEach((slot) => {
|
||||||
|
const state = unit.equipment[slot];
|
||||||
|
let exp = state.exp + amount;
|
||||||
|
while (state.level < 9 && exp >= equipmentExpToNext(state.level)) {
|
||||||
|
exp -= equipmentExpToNext(state.level);
|
||||||
|
state.level += 1;
|
||||||
|
}
|
||||||
|
state.exp = state.level >= 9 ? Math.min(exp, equipmentExpToNext(state.level)) : exp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function parseRewardLabel(reward: string) {
|
function parseRewardLabel(reward: string) {
|
||||||
const normalized = reward.replace(/\s+/g, ' ').trim();
|
const normalized = reward.replace(/\s+/g, ' ').trim();
|
||||||
const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);
|
const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);
|
||||||
|
|||||||
Reference in New Issue
Block a user