Add focused sortie officer analysis
This commit is contained in:
@@ -143,6 +143,30 @@ type SortieUnitTacticalSummary = {
|
||||
terrainLine: string;
|
||||
};
|
||||
|
||||
type SortieFocusedUnitSummary = SortieUnitTacticalSummary & {
|
||||
id: string;
|
||||
name: string;
|
||||
selected: boolean;
|
||||
required: boolean;
|
||||
recommended: boolean;
|
||||
recommendationReason: string | null;
|
||||
className: string;
|
||||
classRole: string;
|
||||
formationRole: SortieFormationRole;
|
||||
terrainScore: number;
|
||||
terrainGrade: string;
|
||||
equipment: {
|
||||
slot: EquipmentSlot;
|
||||
label: string;
|
||||
itemName: string;
|
||||
level: number;
|
||||
exp: number;
|
||||
next: number;
|
||||
rank: string;
|
||||
bonusText: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type SortieTerrainCounts = Partial<Record<TerrainType, number>>;
|
||||
|
||||
type SortieRecommendation = {
|
||||
@@ -6218,6 +6242,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private tabButtons: CampTabButtonView[] = [];
|
||||
private visitedTabs = new Set<CampTab>();
|
||||
private selectedSortieUnitIds: string[] = [];
|
||||
private sortieFocusedUnitId = 'liu-bei';
|
||||
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
|
||||
|
||||
constructor() {
|
||||
@@ -7235,6 +7260,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.campaign = getCampaignState();
|
||||
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
||||
this.ensureSortieFocus();
|
||||
|
||||
const depth = 40;
|
||||
const width = 1188;
|
||||
@@ -7332,7 +7358,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${maxUnits}`, this.textStyle(18, '#f2e3bf', true))
|
||||
).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 18, y + 17, '클릭해서 출전/대기 전환', this.textStyle(12, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 18, y + 17, '클릭해서 분석 및 출전/대기 전환', this.textStyle(12, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
|
||||
allies.forEach((unit, index) => {
|
||||
const rowY = y + (denseRoster ? 48 : 50) + index * rowGap;
|
||||
@@ -7345,7 +7371,10 @@ export class CampScene extends Phaser.Scene {
|
||||
row.setOrigin(0);
|
||||
row.setDepth(depth + 1);
|
||||
row.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.62 : 0.38);
|
||||
row.setInteractive({ useHandCursor: !required });
|
||||
row.setInteractive({ useHandCursor: true });
|
||||
row.on('pointerover', () => {
|
||||
this.sortieFocusedUnitId = unit.id;
|
||||
});
|
||||
row.on('pointerdown', () => this.toggleSortieUnit(unit.id));
|
||||
|
||||
const marker = selected ? '출전' : '대기';
|
||||
@@ -7389,23 +7418,48 @@ export class CampScene extends Phaser.Scene {
|
||||
plan.objectiveLine
|
||||
];
|
||||
summaryLines.forEach((line, index) => {
|
||||
this.trackSortie(this.add.text(x + 18, y + 146 + index * 16, this.compactText(line, 32), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 142 + index * 15, this.compactText(line, 32), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
|
||||
});
|
||||
|
||||
plan.formationSlots.forEach((slot, index) => {
|
||||
const column = index % 2;
|
||||
const row = Math.floor(index / 2);
|
||||
const rowX = x + 18 + column * 176;
|
||||
const rowY = y + 208 + row * 24;
|
||||
const names = slot.unitNames.length > 0 ? slot.unitNames.join(', ') : '미배치';
|
||||
const color = slot.unitNames.length > 0 ? '#a8ffd0' : '#9fb0bf';
|
||||
this.trackSortie(this.add.text(rowX, rowY, slot.label, this.textStyle(12, color, true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(rowX + 42, rowY, this.compactText(names, 13), this.textStyle(11, '#c8d2dd'))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(rowX + 42, rowY + 13, this.compactText(slot.description, 14), this.textStyle(10, '#87919c'))).setDepth(depth + 1);
|
||||
});
|
||||
this.renderSortieFocusPanel(x + 18, y + 190, width - 36, 86, depth + 1);
|
||||
}
|
||||
|
||||
const warning = plan.warnings[0] ?? plan.reserveLine;
|
||||
this.trackSortie(this.add.text(x + 18, y + height - 24, this.compactText(`조언: ${warning}`, 42), this.textStyle(12, plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true))).setDepth(depth + 1);
|
||||
private renderSortieFocusPanel(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const unit = this.sortieFocusedUnit();
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.92));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.42);
|
||||
|
||||
if (!unit) {
|
||||
this.trackSortie(this.add.text(x + 12, y + 18, '선택 가능한 무장이 없습니다.', this.textStyle(12, '#9fb0bf'))).setDepth(depth + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.unitRosterStatus(unit);
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const roleLabel = sortieFormationSlotDefinitions.find((slot) => slot.role === this.sortieFormationRole(unit))?.label ?? '예비';
|
||||
const terrainScore = this.sortieTerrainScore(unit, this.nextSortieScenario());
|
||||
const terrainGrade = this.sortieTerrainGrade(terrainScore);
|
||||
const statusColor = status.tone === 'selected' ? '#a8ffd0' : status.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
||||
const headline = `${unit.name} Lv${unit.level} · ${unit.className} · ${roleLabel}`;
|
||||
const equipLine = equipmentSlots
|
||||
.map((slot) => {
|
||||
const state = unit.equipment[slot];
|
||||
const item = getItem(state.itemId);
|
||||
return `${equipmentSlotLabels[slot]} ${item.name} Lv${state.level}`;
|
||||
})
|
||||
.join(' / ');
|
||||
const reason = recommendation?.reason ?? (this.isSortieSelected(unit.id) ? '현재 출전 편성에 포함되어 있습니다.' : '대기 훈련으로 성장시키거나 전장 역할에 맞춰 교체할 수 있습니다.');
|
||||
|
||||
this.trackSortie(this.add.text(x + 12, y + 7, '선택 무장 분석', this.textStyle(11, '#d8b15f', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 104, y + 6, this.compactText(headline, 24), this.textStyle(13, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 12, y + 7, status.label, this.textStyle(11, statusColor, true))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 12, y + 24, this.compactText(`${summary.statLine} · 지형 ${terrainGrade}(${terrainScore})`, 44), this.textStyle(10, '#c8d2dd'))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 12, y + 39, this.compactText(equipLine, 44), this.textStyle(10, '#c8d2dd'))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 12, y + 54, this.compactText(summary.bondLine, 44), this.textStyle(10, '#a8ffd0', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 12, y + height - 13, this.compactText(reason, 44), this.textStyle(10, recommendation ? '#ffdf7b' : '#9fb0bf', Boolean(recommendation)))).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderSortieEquipmentIcons(unit: UnitData, x: number, y: number, selected: boolean, depth: number) {
|
||||
@@ -7542,6 +7596,69 @@ export class CampScene extends Phaser.Scene {
|
||||
return this.nextSortieRule(scenario).recommended.find((entry) => entry.unitId === unitId);
|
||||
}
|
||||
|
||||
private ensureSortieFocus() {
|
||||
const allies = this.sortieAllies();
|
||||
if (allies.some((unit) => unit.id === this.sortieFocusedUnitId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = new Set(this.selectedSortieUnitIds);
|
||||
const recommended = this.nextSortieRule()
|
||||
.recommended
|
||||
.find((entry) => allies.some((unit) => unit.id === entry.unitId));
|
||||
this.sortieFocusedUnitId =
|
||||
recommended?.unitId ??
|
||||
allies.find((unit) => selectedIds.has(unit.id))?.id ??
|
||||
allies[0]?.id ??
|
||||
'liu-bei';
|
||||
}
|
||||
|
||||
private sortieFocusedUnit() {
|
||||
return this.sortieAllies().find((unit) => unit.id === this.sortieFocusedUnitId) ?? this.selectedSortieUnits()[0] ?? this.sortieAllies()[0];
|
||||
}
|
||||
|
||||
private sortieFocusedUnitSummary(): SortieFocusedUnitSummary | null {
|
||||
const unit = this.sortieFocusedUnit();
|
||||
if (!unit) {
|
||||
return null;
|
||||
}
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const scenario = this.nextSortieScenario();
|
||||
const terrainScore = this.sortieTerrainScore(unit, scenario);
|
||||
return {
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
selected: this.isSortieSelected(unit.id),
|
||||
required: this.isRequiredSortieUnit(unit.id),
|
||||
recommended: Boolean(recommendation),
|
||||
recommendationReason: recommendation?.reason ?? null,
|
||||
className: unit.className,
|
||||
classRole: getUnitClass(unit.classKey).role,
|
||||
formationRole: this.sortieFormationRole(unit),
|
||||
terrainScore,
|
||||
terrainGrade: this.sortieTerrainGrade(terrainScore),
|
||||
statLine: summary.statLine,
|
||||
equipmentLine: summary.equipmentLine,
|
||||
bondLine: summary.bondLine,
|
||||
terrainLine: summary.terrainLine,
|
||||
equipment: equipmentSlots.map((slot) => {
|
||||
const state = unit.equipment[slot];
|
||||
const item = getItem(state.itemId);
|
||||
return {
|
||||
slot,
|
||||
label: equipmentSlotLabels[slot],
|
||||
itemName: item.name,
|
||||
level: state.level,
|
||||
exp: state.exp,
|
||||
next: equipmentExpToNext(state.level),
|
||||
rank: item.rank,
|
||||
bonusText: this.itemBonusText(item)
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private renderSortieDeploymentMap(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const scenario = this.nextSortieScenario();
|
||||
const frame = this.trackSortie(this.add.rectangle(x, y, width, height, 0x070b10, 0.9));
|
||||
@@ -9010,6 +9127,7 @@ export class CampScene extends Phaser.Scene {
|
||||
if (!unit) {
|
||||
return;
|
||||
}
|
||||
this.sortieFocusedUnitId = unitId;
|
||||
if (this.isRequiredSortieUnit(unitId)) {
|
||||
this.showCampNotice(`${unit.name}는 반드시 출전해야 합니다.`);
|
||||
return;
|
||||
@@ -9134,6 +9252,8 @@ export class CampScene extends Phaser.Scene {
|
||||
selectedDialogueId: this.selectedDialogueId,
|
||||
selectedVisitId: this.selectedVisitId,
|
||||
selectedSortieUnitIds: [...this.selectedSortieUnitIds],
|
||||
sortieFocusedUnitId: this.sortieFocusedUnitId,
|
||||
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
|
||||
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
|
||||
reserveTrainingFocusOptions: campaignReserveTrainingFocusDefinitions.map((focus) => ({ ...focus })),
|
||||
sortieRoster: this.sortieAllies().map((unit) => {
|
||||
|
||||
Reference in New Issue
Block a user