Add officer roster reserve growth

This commit is contained in:
2026-06-23 05:59:14 +09:00
parent 44009b3542
commit 8f083a6ca4
4 changed files with 195 additions and 19 deletions

View File

@@ -3177,13 +3177,16 @@ export class CampScene extends Phaser.Scene {
private renderUnitColumn() {
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
const availableHeight = 462;
const rowGap = allies.length > 1 ? Math.min(150, Math.floor(availableHeight / allies.length)) : 150;
const cardHeight = Math.min(126, Math.max(70, rowGap - 10));
const compact = cardHeight < 104;
const rowGap = allies.length > 1 ? Math.min(132, Math.floor(availableHeight / allies.length)) : 132;
const cardHeight = Math.min(118, Math.max(48, rowGap - 8));
const compact = cardHeight < 88;
this.renderRosterColumnSummary(42, 86, 318, allies);
allies.forEach((unit, index) => {
const x = 42;
const y = 120 + index * rowGap;
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));
bg.setOrigin(0);
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 portraitCenterY = y + cardHeight / 2;
const portraitSize = compact ? 62 : 92;
const portraitSize = compact ? 40 : 84;
if (portraitKey) {
const portrait = this.track(this.add.image(x + 58, portraitCenterY, portraitKeys[portraitKey]));
portrait.setDisplaySize(portraitSize, portraitSize);
} 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;
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 ? 38 : 51), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 12 : 14, '#d4dce6')));
this.track(this.add.text(textX, y + (compact ? 58 : 78), `경험 ${unit.exp}/100`, this.textStyle(compact ? 12 : 14, '#d8b15f', true)));
this.drawBar(textX, y + cardHeight - 18, compact ? 160 : 176, 8, unit.exp / 100, palette.gold);
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`;
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) {
const bg = this.track(this.add.circle(x, y, radius, 0x1f3140, 0.96));
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 ?? {}));
}
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() {
const unit = this.selectedUnit();
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.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) => {
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() {
@@ -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>) {
const bonuses = [
item.attackBonus ? `공격 +${item.attackBonus}` : '',
@@ -4278,6 +4371,8 @@ export class CampScene extends Phaser.Scene {
};
}),
sortiePlan: this.sortiePlanSummary(),
rosterCollection: this.rosterCollectionSummary(),
reserveTrainingAwards: this.latestReserveTrainingAwards(),
campaignProgress: this.campaignTimelineProgress(),
campBattleId: this.currentCampBattleId(),
campTitle: this.currentCampTitle(),