Add officer roster reserve growth
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user