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

@@ -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,