Add selectable reserve drill focus
This commit is contained in:
@@ -68,7 +68,7 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
||||
- Forty-second battle Jing rear-crisis route, using Ma Liang and Mi Zhu's rear-guard roles across an 84x66 Jiangling ferry battlefield to repel Lu Meng's disguised scouts and expose Mi Fang's wavering rear line
|
||||
- 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
|
||||
- Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers
|
||||
- Officer collection support in camp, including full roster status, selected/reserve/recommended markers, selectable reserve drill focus, and post-battle reserve training growth for benched officers
|
||||
- Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses
|
||||
- Flow verification script from title through the forty-second battle victory, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege/Jing rear camp states, campaign timeline state, and camp save state
|
||||
|
||||
@@ -76,7 +76,7 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
||||
|
||||
1. Build the Jing Province rear-collapse follow-up, using Lu Meng's infiltration, Mi Fang/Fu Shiren's wavering, and Guan Yu's overextended Fan Castle front as the next campaign tension
|
||||
2. Expand Jing rear-crisis aftermath events, including warehouse trust, river rumor choices, officer loyalty checks, and supply decisions that affect sortie recommendations
|
||||
3. Expand sortie selection into richer pre-battle officer planning, including explicit reserve drills, class practice, and bond-focused camp assignments
|
||||
3. Expand reserve planning into individual officer assignments, including targeted class practice, paired bond training, and camp task slots
|
||||
4. Add a dedicated treasure/equipment management view so players can compare special effects and growth
|
||||
5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ try {
|
||||
await page.waitForFunction(() => document.querySelector('canvas') !== null, undefined, { timeout: 90000 });
|
||||
await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined, undefined, { timeout: 90000 });
|
||||
await page.waitForFunction(() => window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
return activeScenes.includes('TitleScene');
|
||||
}, undefined, { timeout: 90000 });
|
||||
|
||||
const debugBeforeBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []);
|
||||
if (!debugBeforeBattle.includes('TitleScene')) {
|
||||
@@ -5480,6 +5483,7 @@ try {
|
||||
await clickSortieRosterUnit(page, unitId);
|
||||
}
|
||||
}
|
||||
await clickReserveTrainingFocus(page, 'bond-practice');
|
||||
|
||||
const jingRearSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
@@ -5489,9 +5493,12 @@ try {
|
||||
!jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
||||
!jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
||||
jingRearSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
||||
jingRearSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
||||
jingRearSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
||||
jingRearSortieReadyState.reserveTrainingFocus?.id !== 'bond-practice' ||
|
||||
!jingRearSortieReadyState.sortiePlan?.reserveTrainingLine?.includes('공명 합숙') ||
|
||||
jingRearSortieReadyState.sortiePlan?.reserveTrainingBondPreview !== 4
|
||||
) {
|
||||
throw new Error(`Expected Jing rear sortie to deploy Liu Bei, Guan Yu, Ma Liang, Mi Zhu, Zhao Yun, Huang Quan, and Wang Ping: ${JSON.stringify(jingRearSortieReadyState)}`);
|
||||
throw new Error(`Expected Jing rear sortie to deploy Liu Bei, Guan Yu, Ma Liang, Mi Zhu, Zhao Yun, Huang Quan, Wang Ping, and bond-focused reserve drill: ${JSON.stringify(jingRearSortieReadyState)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-jing-rear-sortie.png', fullPage: true });
|
||||
|
||||
@@ -5563,9 +5570,13 @@ try {
|
||||
!fortySecondCampState.availableDialogueIds.every((id) => id.endsWith('jing-rear')) ||
|
||||
fortySecondCampState.availableVisitIds?.length !== 2 ||
|
||||
!fortySecondCampState.availableVisitIds.every((id) => id.includes('jing-rear')) ||
|
||||
fortySecondCampState.rosterCollection?.total < 22
|
||||
fortySecondCampState.rosterCollection?.total < 22 ||
|
||||
fortySecondCampState.campaign?.reserveTrainingFocus !== 'bond-practice' ||
|
||||
!fortySecondCampState.reserveTrainingAwards?.some(
|
||||
(entry) => entry.focusId === 'bond-practice' && entry.focusLabel === '공명 합숙' && entry.bondExpGained > 0
|
||||
)
|
||||
) {
|
||||
throw new Error(`Expected forty-second camp to expose Jing rear dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortySecondCampState)}`);
|
||||
throw new Error(`Expected forty-second camp to expose Jing rear dialogue/visit sets, preserve the full officer roster, and record bond-focused reserve training: ${JSON.stringify(fortySecondCampState)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-jing-rear-camp.png', fullPage: true });
|
||||
|
||||
@@ -5706,15 +5717,28 @@ function assertSortieTacticalRoster(state, requiredUnitIds) {
|
||||
!state.sortiePlan ||
|
||||
!state.sortiePlan.deploymentLine?.includes('전열') ||
|
||||
!state.sortiePlan.objectiveLine ||
|
||||
!state.sortiePlan.reserveTrainingLine?.includes('대기 훈련') ||
|
||||
!state.sortiePlan.recommendationLine?.includes('추천') ||
|
||||
!Array.isArray(state.sortiePlan.formationSlots) ||
|
||||
state.sortiePlan.formationSlots.length < 3 ||
|
||||
typeof state.sortiePlan.activeBondCount !== 'number' ||
|
||||
typeof state.sortiePlan.terrainScore !== 'number' ||
|
||||
typeof state.sortiePlan.recommendedSelectedCount !== 'number' ||
|
||||
typeof state.sortiePlan.recommendedTotal !== 'number'
|
||||
typeof state.sortiePlan.recommendedTotal !== 'number' ||
|
||||
typeof state.sortiePlan.reserveTrainingExpPreview !== 'number' ||
|
||||
typeof state.sortiePlan.reserveTrainingEquipmentPreview !== 'number' ||
|
||||
typeof state.sortiePlan.reserveTrainingBondPreview !== 'number'
|
||||
) {
|
||||
throw new Error(`Expected sortie preparation to expose deployment plan, bond count, and terrain score: ${JSON.stringify(state.sortiePlan)}`);
|
||||
throw new Error(`Expected sortie preparation to expose deployment plan, reserve training, bond count, and terrain score: ${JSON.stringify(state.sortiePlan)}`);
|
||||
}
|
||||
|
||||
if (
|
||||
!state.reserveTrainingFocus ||
|
||||
!Array.isArray(state.reserveTrainingFocusOptions) ||
|
||||
state.reserveTrainingFocusOptions.length !== 3 ||
|
||||
!state.reserveTrainingFocusOptions.some((focus) => focus.id === 'bond-practice' && focus.bondExpGained > 0)
|
||||
) {
|
||||
throw new Error(`Expected sortie preparation to expose selectable reserve drill focuses: ${JSON.stringify(state.reserveTrainingFocusOptions)}`);
|
||||
}
|
||||
|
||||
const recommendedUnits = state.sortieRoster.filter((unit) => unit.recommended);
|
||||
@@ -5739,3 +5763,20 @@ async function clickSortieRosterUnit(page, unitId) {
|
||||
await page.mouse.click(254, 314 + (denseRoster ? 48 : 50) + index * rowGap);
|
||||
await page.waitForTimeout(120);
|
||||
}
|
||||
|
||||
async function clickReserveTrainingFocus(page, focusId) {
|
||||
const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (!state?.sortieVisible || !Array.isArray(state.reserveTrainingFocusOptions)) {
|
||||
throw new Error(`Cannot click reserve drill ${focusId}; sortie reserve drill selector is not visible: ${JSON.stringify(state)}`);
|
||||
}
|
||||
|
||||
const index = state.reserveTrainingFocusOptions.findIndex((focus) => focus.id === focusId);
|
||||
if (index < 0) {
|
||||
throw new Error(`Cannot click reserve drill ${focusId}; focus is missing: ${JSON.stringify(state.reserveTrainingFocusOptions)}`);
|
||||
}
|
||||
|
||||
const selectorX = 574;
|
||||
const buttonWidth = Math.floor((314 - 40) / state.reserveTrainingFocusOptions.length);
|
||||
await page.mouse.click(selectorX + 12 + index * (buttonWidth + 8) + buttonWidth / 2, 655);
|
||||
await page.waitForTimeout(120);
|
||||
}
|
||||
|
||||
@@ -53,12 +53,17 @@ import {
|
||||
import {
|
||||
applyCampVisitReward,
|
||||
applyCampBondExp,
|
||||
campaignReserveTrainingFocusDefinitions,
|
||||
defaultCampaignReserveTrainingFocusId,
|
||||
ensureCampaignRosterUnits,
|
||||
getCampaignState,
|
||||
getFirstBattleReport,
|
||||
markCampaignStep,
|
||||
saveCampaignState,
|
||||
setCampaignReserveTrainingFocus,
|
||||
type CampaignState,
|
||||
type CampaignReserveTrainingFocusDefinition,
|
||||
type CampaignReserveTrainingFocusId,
|
||||
type FirstBattleReport
|
||||
} from '../state/campaignState';
|
||||
import { palette } from '../ui/palette';
|
||||
@@ -175,6 +180,12 @@ type SortiePlanSummary = {
|
||||
recommendationLine: string;
|
||||
recruitedLine: string;
|
||||
reserveLine: string;
|
||||
reserveTrainingLine: string;
|
||||
reserveTrainingFocusId: CampaignReserveTrainingFocusId;
|
||||
reserveTrainingFocusLabel: string;
|
||||
reserveTrainingExpPreview: number;
|
||||
reserveTrainingEquipmentPreview: number;
|
||||
reserveTrainingBondPreview: number;
|
||||
objectiveLine: string;
|
||||
formationSlots: SortieFormationSlot[];
|
||||
warnings: string[];
|
||||
@@ -7085,7 +7096,8 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderSortieChecklist(x + 576, y + 104, 578, 168, depth + 2);
|
||||
this.renderSortieUnitSummary(x + 34, y + 292, 728, 286, depth + 2);
|
||||
this.renderSortieFormationPlan(x + 782, y + 292, 372, 286, depth + 2);
|
||||
this.renderSortieRewardHint(x + 34, y + 596, 808, 52, depth + 2);
|
||||
this.renderSortieRewardHint(x + 34, y + 596, 480, 52, depth + 2);
|
||||
this.renderSortieReserveTraining(x + 528, y + 596, 314, 52, depth + 2);
|
||||
|
||||
this.addSortieButton('군영으로', x + width - 298, y + height - 42, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
@@ -7205,7 +7217,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
const summaryLines = [
|
||||
plan.deploymentLine,
|
||||
`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개`,
|
||||
`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개 · ${plan.reserveTrainingFocusLabel}`,
|
||||
plan.recruitedLine,
|
||||
plan.objectiveLine
|
||||
];
|
||||
@@ -7416,6 +7428,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
|
||||
const scenario = this.nextSortieScenario();
|
||||
const rule = this.nextSortieRule(scenario);
|
||||
const reserveFocus = this.reserveTrainingFocusDefinition();
|
||||
const availableIds = new Set(allAllies.map((unit) => unit.id));
|
||||
const recommended = rule.recommended.filter((entry) => availableIds.has(entry.unitId));
|
||||
const missingRecommended = recommended.filter((entry) => !selectedIds.has(entry.unitId));
|
||||
@@ -7484,6 +7497,15 @@ export class CampScene extends Phaser.Scene {
|
||||
? `합류 무장 ${selectedRecruitedCount}/${recruitedUnits.length} · 대기 ${reserveUnits.length}`
|
||||
: '합류 무장 없음',
|
||||
reserveLine: reserveUnits.length > 0 ? `대기 무장: ${reserveNames}` : '전원 출전 중입니다.',
|
||||
reserveTrainingLine:
|
||||
reserveUnits.length > 0
|
||||
? `대기 훈련 ${reserveFocus.label}: 경험 +${reserveFocus.expGained}, 장비 +${reserveFocus.equipmentExpGained}${reserveFocus.bondExpGained ? `, 공명 +${reserveFocus.bondExpGained}` : ''}`
|
||||
: '대기 훈련 대상 없음',
|
||||
reserveTrainingFocusId: reserveFocus.id,
|
||||
reserveTrainingFocusLabel: reserveFocus.label,
|
||||
reserveTrainingExpPreview: reserveFocus.expGained,
|
||||
reserveTrainingEquipmentPreview: reserveFocus.equipmentExpGained,
|
||||
reserveTrainingBondPreview: reserveFocus.bondExpGained,
|
||||
objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : '다음 전투 정보 없음',
|
||||
formationSlots,
|
||||
warnings
|
||||
@@ -7557,6 +7579,43 @@ export class CampScene extends Phaser.Scene {
|
||||
).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderSortieReserveTraining(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.9));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.blue, 0.4);
|
||||
|
||||
const focus = this.reserveTrainingFocusDefinition();
|
||||
const reserveCount = this.sortiePlanSummary().reserveCount;
|
||||
this.trackSortie(this.add.text(x + 12, y + 7, '대기 훈련', this.textStyle(12, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + width - 12,
|
||||
y + 7,
|
||||
reserveCount > 0 ? `${reserveCount}명 · ${focus.label}` : '대기 없음',
|
||||
this.textStyle(11, reserveCount > 0 ? '#d8b15f' : '#9fb0bf', true)
|
||||
)
|
||||
).setOrigin(1, 0).setDepth(depth + 1);
|
||||
|
||||
const buttonWidth = Math.floor((width - 40) / campaignReserveTrainingFocusDefinitions.length);
|
||||
campaignReserveTrainingFocusDefinitions.forEach((option, index) => {
|
||||
const buttonX = x + 12 + index * (buttonWidth + 8);
|
||||
const selected = option.id === focus.id;
|
||||
const button = this.trackSortie(this.add.rectangle(buttonX, y + 30, buttonWidth, 16, selected ? 0x263a2d : 0x0e151d, selected ? 0.98 : 0.86));
|
||||
button.setOrigin(0);
|
||||
button.setDepth(depth + 1);
|
||||
button.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.78 : 0.42);
|
||||
button.setInteractive({ useHandCursor: true });
|
||||
button.on('pointerdown', () => this.setReserveTrainingFocus(option.id));
|
||||
|
||||
const label = this.trackSortie(this.add.text(buttonX + buttonWidth / 2, y + 33, option.label, this.textStyle(10, selected ? '#a8ffd0' : '#c8d2dd', true)));
|
||||
label.setOrigin(0.5, 0);
|
||||
label.setDepth(depth + 2);
|
||||
label.setInteractive({ useHandCursor: true });
|
||||
label.on('pointerdown', () => this.setReserveTrainingFocus(option.id));
|
||||
});
|
||||
}
|
||||
|
||||
private nextSortieBriefing() {
|
||||
const flow = this.currentSortieFlow();
|
||||
return {
|
||||
@@ -7570,6 +7629,17 @@ export class CampScene extends Phaser.Scene {
|
||||
return getSortieFlow(this.campaign?.latestBattleId, this.campaign?.step);
|
||||
}
|
||||
|
||||
private reserveTrainingFocusDefinition(): CampaignReserveTrainingFocusDefinition {
|
||||
const focusId = this.campaign?.reserveTrainingFocus ?? defaultCampaignReserveTrainingFocusId;
|
||||
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === focusId) ?? campaignReserveTrainingFocusDefinitions[0];
|
||||
}
|
||||
|
||||
private setReserveTrainingFocus(focusId: CampaignReserveTrainingFocusId) {
|
||||
this.campaign = setCampaignReserveTrainingFocus(focusId);
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private sortieChecklist(): SortieChecklistItem[] {
|
||||
const units = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
const liuBei = units.find((unit) => unit.id === 'liu-bei');
|
||||
@@ -7739,7 +7809,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const textX = compact ? x + 102 : x + 116;
|
||||
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 ? 27 : 47), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 10 : 13, '#d4dce6')));
|
||||
const expLine = training ? `경험 ${unit.exp}/100 · 대기 +${training.expGained}` : `경험 ${unit.exp}/100`;
|
||||
const expLine = training
|
||||
? `경험 ${unit.exp}/100 · ${training.focusLabel ?? '대기'} +${training.expGained}`
|
||||
: `경험 ${unit.exp}/100`;
|
||||
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';
|
||||
@@ -7760,7 +7832,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.add.text(
|
||||
x + width - 12,
|
||||
y + 6,
|
||||
recentTrainingCount > 0 ? `최근 훈련 ${recentTrainingCount}` : '훈련 대기',
|
||||
recentTrainingCount > 0 ? `최근 훈련 ${recentTrainingCount} · ${this.latestReserveTrainingFocusLabel()}` : '훈련 대기',
|
||||
this.textStyle(11, recentTrainingCount > 0 ? '#a8ffd0' : '#9fb0bf', true)
|
||||
)
|
||||
);
|
||||
@@ -8009,6 +8081,10 @@ export class CampScene extends Phaser.Scene {
|
||||
return this.campaign?.battleHistory[latestBattleId]?.reserveTraining ?? [];
|
||||
}
|
||||
|
||||
private latestReserveTrainingFocusLabel() {
|
||||
return this.latestReserveTrainingAwards()[0]?.focusLabel ?? '대기';
|
||||
}
|
||||
|
||||
private latestReserveTraining(unitId: string) {
|
||||
return this.latestReserveTrainingAwards().find((entry) => entry.unitId === unitId);
|
||||
}
|
||||
@@ -8026,6 +8102,7 @@ export class CampScene extends Phaser.Scene {
|
||||
reserveCount: Math.max(0, allies.length - selectedCount),
|
||||
recruitedCount,
|
||||
averageLevel,
|
||||
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
|
||||
reserveTrainingCount: reserveTrainingAwards.length,
|
||||
reserveTrainingExp: reserveTrainingAwards.reduce((sum, entry) => sum + entry.expGained, 0)
|
||||
};
|
||||
@@ -8594,7 +8671,9 @@ export class CampScene extends Phaser.Scene {
|
||||
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}` : '대기 중에도 전투 후 소량의 훈련 성장이 반영됩니다.';
|
||||
const trainingText = training
|
||||
? `최근 ${training.focusLabel ?? '대기 훈련'} 경험 +${training.expGained}, 장비 +${training.equipmentExpGained}${training.bondExpGained ? `, 공명 +${training.bondExpGained}` : ''}`
|
||||
: '대기 중에도 전투 후 선택한 훈련 방침의 성장이 반영됩니다.';
|
||||
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)));
|
||||
@@ -8888,6 +8967,8 @@ export class CampScene extends Phaser.Scene {
|
||||
selectedDialogueId: this.selectedDialogueId,
|
||||
selectedVisitId: this.selectedVisitId,
|
||||
selectedSortieUnitIds: [...this.selectedSortieUnitIds],
|
||||
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
|
||||
reserveTrainingFocusOptions: campaignReserveTrainingFocusDefinitions.map((focus) => ({ ...focus })),
|
||||
sortieRoster: this.sortieAllies().map((unit) => {
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
return {
|
||||
@@ -8923,6 +9004,7 @@ export class CampScene extends Phaser.Scene {
|
||||
gold: this.campaign.gold,
|
||||
inventory: this.campaign.inventory,
|
||||
selectedSortieUnitIds: this.campaign.selectedSortieUnitIds,
|
||||
reserveTrainingFocus: this.campaign.reserveTrainingFocus,
|
||||
roster: this.campaign.roster.map((unit) => ({
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
|
||||
@@ -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