Improve sortie formation readability

This commit is contained in:
2026-07-03 00:17:26 +09:00
parent c57d835799
commit 40769348ad

View File

@@ -1,6 +1,7 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png'; import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png';
import { soundDirector } from '../audio/SoundDirector'; import { soundDirector } from '../audio/SoundDirector';
import { battleUiIconFrames, battleUiIconTextureKey, loadBattleUiIcons, type BattleUiIconKey } from '../data/battleUiIcons';
import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type EquipmentSlot } from '../data/battleItems'; import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type EquipmentSlot } from '../data/battleItems';
import { getUnitClass, terrainRules, type TerrainType } from '../data/battleRules'; import { getUnitClass, terrainRules, type TerrainType } from '../data/battleRules';
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles'; import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles';
@@ -10021,7 +10022,7 @@ export class CampScene extends Phaser.Scene {
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id; this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id;
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id; this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
soundDirector.playMusic('militia-theme'); soundDirector.playMusic('militia-theme');
this.ensureCampAssets(() => this.drawCampScene()); loadBattleUiIcons(this, () => this.ensureCampAssets(() => this.drawCampScene()));
} }
private drawCampScene() { private drawCampScene() {
@@ -11229,6 +11230,7 @@ export class CampScene extends Phaser.Scene {
this.add.text(x + 34, y + 70, prepSubtitle, this.textStyle(15, '#d4dce6')) this.add.text(x + 34, y + 70, prepSubtitle, this.textStyle(15, '#d4dce6'))
); );
subtitle.setDepth(depth + 2); subtitle.setDepth(depth + 2);
this.renderSortieHeaderSummary(x + 690, y + 26, 464, 58, depth + 2);
this.renderSortieBriefing(x + 34, y + 104, 520, 168, depth + 2); this.renderSortieBriefing(x + 34, y + 104, 520, 168, depth + 2);
this.renderSortieChecklist(x + 576, y + 104, 578, 168, depth + 2); this.renderSortieChecklist(x + 576, y + 104, 578, 168, depth + 2);
@@ -11247,6 +11249,37 @@ export class CampScene extends Phaser.Scene {
}, depth + 3); }, depth + 3);
} }
private renderSortieHeaderSummary(x: number, y: number, width: number, height: number, depth: number) {
const selectedUnits = this.selectedSortieUnits();
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
const reserveUnits = this.sortieAllies().filter((unit) => !selectedIds.has(unit.id));
const selectedNames = selectedUnits.map((unit) => unit.name).join(', ');
const reserveNames = reserveUnits.map((unit) => unit.name).join(', ');
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.78));
bg.setOrigin(0);
bg.setDepth(depth);
bg.setStrokeStyle(1, palette.gold, 0.36);
this.renderSortieBattleUiIcon('leadership', x + 22, y + 20, 24, depth + 1, 0.95);
this.trackSortie(
this.add.text(
x + 44,
y + 10,
this.compactText(`출전 ${selectedUnits.length}명: ${selectedNames || '없음'}`, 35),
this.textStyle(13, '#f2e3bf', true)
)
).setDepth(depth + 1);
this.renderSortieBattleUiIcon('focus', x + 22, y + 44, 22, depth + 1, 0.74);
this.trackSortie(
this.add.text(
x + 44,
y + 35,
this.compactText(`대기 ${reserveUnits.length}명: ${reserveNames || '없음'}`, 35),
this.textStyle(12, reserveUnits.length > 0 ? '#c8d2dd' : '#9fb0bf', true)
)
).setDepth(depth + 1);
}
private renderSortieBriefing(x: number, y: number, width: number, height: number, depth: number) { private renderSortieBriefing(x: number, y: number, width: number, height: number, depth: number) {
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92)); const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
bg.setOrigin(0); bg.setOrigin(0);
@@ -11295,12 +11328,14 @@ export class CampScene extends Phaser.Scene {
bg.setDepth(depth); bg.setDepth(depth);
bg.setStrokeStyle(1, palette.blue, 0.48); bg.setStrokeStyle(1, palette.blue, 0.48);
const allies = this.sortieAllies(); const allies = this.sortieAllies();
const displayAllies = this.sortieRosterDisplayUnits(allies);
const selectedCount = this.selectedSortieUnitIds.length; const selectedCount = this.selectedSortieUnitIds.length;
const maxUnits = this.sortieMaxUnits(); const maxUnits = this.sortieMaxUnits();
const reserveCount = Math.max(0, allies.length - selectedCount);
const rosterView = this.sortieRosterViewState(allies.length, height); const rosterView = this.sortieRosterViewState(allies.length, height);
const { denseRoster, rowGap, rowHeight, listTopOffset, listFooterHeight, maxVisibleRows, maxScroll } = rosterView; const { denseRoster, rowGap, rowHeight, listTopOffset, listFooterHeight, maxVisibleRows, maxScroll } = rosterView;
this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll); this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll);
const visibleAllies = allies.slice(this.sortieRosterScroll, this.sortieRosterScroll + maxVisibleRows); const visibleAllies = displayAllies.slice(this.sortieRosterScroll, this.sortieRosterScroll + maxVisibleRows);
const handleRosterWheel = ( const handleRosterWheel = (
_pointer: Phaser.Input.Pointer, _pointer: Phaser.Input.Pointer,
_deltaX: number, _deltaX: number,
@@ -11320,9 +11355,9 @@ export class CampScene extends Phaser.Scene {
bg.on('wheel', handleRosterWheel); bg.on('wheel', handleRosterWheel);
} }
this.trackSortie( this.trackSortie(
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${maxUnits}`, this.textStyle(18, '#f2e3bf', true)) this.add.text(x + 18, y + 14, `출전 ${selectedCount}/${maxUnits} · 대기 ${reserveCount}`, this.textStyle(18, '#f2e3bf', true))
).setDepth(depth + 1); ).setDepth(depth + 1);
this.trackSortie(this.add.text(x + width - 18, y + 17, '클릭: 분석/출전 전환', this.textStyle(11, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1); this.trackSortie(this.add.text(x + width - 18, y + 17, '선택 무장 우선 표시 · 클릭 전환', this.textStyle(11, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1);
visibleAllies.forEach((unit, index) => { visibleAllies.forEach((unit, index) => {
const rowY = y + listTopOffset + index * rowGap; const rowY = y + listTopOffset + index * rowGap;
@@ -11331,10 +11366,11 @@ export class CampScene extends Phaser.Scene {
const recruited = !foundingSortieUnitIds.has(unit.id); const recruited = !foundingSortieUnitIds.has(unit.id);
const recommendation = this.sortieRecommendation(unit.id); const recommendation = this.sortieRecommendation(unit.id);
const summary = this.sortieUnitTacticalSummary(unit); const summary = this.sortieUnitTacticalSummary(unit);
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82)); const recommendedReserve = Boolean(recommendation && !selected);
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : recommendedReserve ? 0x241f17 : 0x151b24, selected ? 0.96 : 0.84));
row.setOrigin(0); row.setOrigin(0);
row.setDepth(depth + 1); row.setDepth(depth + 1);
row.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.62 : 0.38); row.setStrokeStyle(1, selected ? palette.green : recommendedReserve ? palette.gold : 0x53606c, selected ? 0.7 : recommendedReserve ? 0.54 : 0.36);
row.setInteractive({ useHandCursor: true }); row.setInteractive({ useHandCursor: true });
row.on('wheel', handleRosterWheel); row.on('wheel', handleRosterWheel);
row.on('pointerover', () => { row.on('pointerover', () => {
@@ -11342,22 +11378,25 @@ export class CampScene extends Phaser.Scene {
}); });
row.on('pointerdown', () => this.toggleSortieUnit(unit.id)); row.on('pointerdown', () => this.toggleSortieUnit(unit.id));
const marker = selected ? '출전' : '대기'; const markerLabel = required ? '필수 출전' : selected ? '출전' : recommendation ? '추천 대기' : recruited ? '합류 대기' : '대기';
const markerLabel = required ? '필수' : recommendation ? '추천' : recruited ? '합류' : marker; this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(denseRoster ? 9 : 11, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(denseRoster ? 10 : 12, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2); if (!denseRoster) {
this.trackSortie(this.add.text(x + (denseRoster ? 78 : 84), rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(denseRoster ? 12 : 14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2); this.renderSortieBattleUiIcon(this.unitClassIconKey(unit), x + 90, rowY + 8, 20, depth + 2, selected ? 1 : 0.58);
this.trackSortie(this.add.text(x + (denseRoster ? 194 : 214), rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, denseRoster ? 17 : 19), this.textStyle(denseRoster ? 10 : 12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2); }
this.trackSortie(this.add.text(x + (denseRoster ? 84 : 112), rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(denseRoster ? 12 : 14, selected ? '#f2e3bf' : recommendedReserve ? '#d8b15f' : '#87919c', true))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + (denseRoster ? 200 : 234), rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, denseRoster ? 17 : 17), this.textStyle(denseRoster ? 10 : 12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + 424, rowY, denseRoster ? this.compactText(summary.statLine, 23) : `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(denseRoster ? 10 : 12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2); this.trackSortie(this.add.text(x + 424, rowY, denseRoster ? this.compactText(summary.statLine, 23) : `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(denseRoster ? 10 : 12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
if (!denseRoster) { if (!denseRoster) {
this.drawSortieBar(x + 514, rowY + 6, 74, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2); this.drawSortieBar(x + 514, rowY + 6, 74, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2);
} }
this.trackSortie(this.add.text(x + 610, rowY, this.compactText(denseRoster ? summary.bondLine : summary.terrainLine, denseRoster ? 15 : 13), this.textStyle(11, selected ? (denseRoster ? '#a8ffd0' : '#d8b15f') : '#77818c', true))).setDepth(depth + 2); this.trackSortie(this.add.text(x + 610, rowY, this.compactText(denseRoster ? summary.bondLine : summary.terrainLine, denseRoster ? 15 : 13), this.textStyle(11, selected ? (denseRoster ? '#a8ffd0' : '#d8b15f') : recommendedReserve ? '#d8b15f' : '#77818c', true))).setDepth(depth + 2);
if (!denseRoster) { if (!denseRoster) {
this.renderSortieEquipmentIcons(unit, x + 84, rowY + 19, selected, depth + 2); this.renderSortieEquipmentIcons(unit, x + 112, rowY + 19, selected, depth + 2);
this.trackSortie(this.add.text(x + 162, rowY + 18, this.compactText(summary.equipmentLine, 34), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2); this.trackSortie(this.add.text(x + 190, rowY + 18, this.compactText(summary.equipmentLine, 29), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + 424, rowY + 18, this.compactText(summary.statLine, 23), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2); this.trackSortie(this.add.text(x + 424, rowY + 18, this.compactText(summary.statLine, 23), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + 610, rowY + 18, this.compactText(summary.bondLine, 15), this.textStyle(11, selected ? '#a8ffd0' : '#68727e', selected))).setDepth(depth + 2); const secondLine = recommendation && !selected ? `추천: ${recommendation.reason}` : summary.bondLine;
this.trackSortie(this.add.text(x + 610, rowY + 18, this.compactText(secondLine, 17), this.textStyle(11, selected ? '#a8ffd0' : recommendedReserve ? '#ffdf7b' : '#68727e', selected || recommendedReserve))).setDepth(depth + 2);
} }
}); });
@@ -11388,6 +11427,29 @@ export class CampScene extends Phaser.Scene {
} }
} }
private sortieRosterDisplayUnits(allies: UnitData[]) {
const selected = new Set(this.selectedSortieUnitIds);
const order = new Map(allies.map((unit, index) => [unit.id, index]));
return [...allies].sort((a, b) => {
const aSelected = selected.has(a.id);
const bSelected = selected.has(b.id);
if (aSelected !== bSelected) {
return Number(bSelected) - Number(aSelected);
}
const aRecommended = Boolean(this.sortieRecommendation(a.id));
const bRecommended = Boolean(this.sortieRecommendation(b.id));
if (aRecommended !== bRecommended) {
return Number(bRecommended) - Number(aRecommended);
}
const aRequired = this.isRequiredSortieUnit(a.id);
const bRequired = this.isRequiredSortieUnit(b.id);
if (aRequired !== bRequired) {
return Number(bRequired) - Number(aRequired);
}
return (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0);
});
}
private updateSortieRosterScroll(nextScroll: number, maxScroll: number) { private updateSortieRosterScroll(nextScroll: number, maxScroll: number) {
const clamped = Phaser.Math.Clamp(Math.round(nextScroll), 0, maxScroll); const clamped = Phaser.Math.Clamp(Math.round(nextScroll), 0, maxScroll);
if (clamped === this.sortieRosterScroll) { if (clamped === this.sortieRosterScroll) {
@@ -11431,19 +11493,38 @@ export class CampScene extends Phaser.Scene {
this.add.text(x + width - 18, y + 17, `${plan.selectedCount}/${plan.maxCount}명 · 지형 ${plan.terrainGrade}`, this.textStyle(12, '#d8b15f', true)) this.add.text(x + width - 18, y + 17, `${plan.selectedCount}/${plan.maxCount}명 · 지형 ${plan.terrainGrade}`, this.textStyle(12, '#d8b15f', true))
).setOrigin(1, 0).setDepth(depth + 1); ).setOrigin(1, 0).setDepth(depth + 1);
this.renderSortieDeploymentMap(x + 18, y + 46, width - 36, 92, depth + 1); this.renderSortieDeploymentMap(x + 18, y + 46, width - 36, 84, depth + 1);
this.renderSortieRoleSlots(x + 18, y + 136, width - 36, 48, plan, depth + 1);
this.trackSortie(
this.add.text(
x + 18,
y + 190,
this.compactText(`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개 · ${plan.objectiveLine}`, 44),
this.textStyle(11, '#d4dce6', true)
)
).setDepth(depth + 1);
const summaryLines = [ this.renderSortieFocusPanel(x + 18, y + 204, width - 36, 74, depth + 1);
plan.deploymentLine, }
`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개 · ${plan.reserveTrainingFocusLabel}`,
plan.recruitedLine, private renderSortieRoleSlots(x: number, y: number, width: number, height: number, plan: SortiePlanSummary, depth: number) {
plan.objectiveLine const gap = 8;
]; const slotWidth = Math.floor((width - gap) / 2);
summaryLines.forEach((line, index) => { const slotHeight = Math.floor((height - 6) / 2);
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 slotX = x + column * (slotWidth + gap);
const slotY = y + row * (slotHeight + 6);
const filled = slot.unitNames.length > 0;
const bg = this.trackSortie(this.add.rectangle(slotX, slotY, slotWidth, slotHeight, filled ? 0x172a22 : 0x111922, filled ? 0.94 : 0.78));
bg.setOrigin(0);
bg.setDepth(depth);
bg.setStrokeStyle(1, filled ? palette.green : 0x53606c, filled ? 0.58 : 0.34);
this.renderSortieBattleUiIcon(this.formationRoleIconKey(slot.role), slotX + 13, slotY + slotHeight / 2, 18, depth + 1, filled ? 0.95 : 0.5);
this.trackSortie(this.add.text(slotX + 28, slotY + 3, `${slot.label} ${slot.unitNames.length}`, this.textStyle(10, filled ? '#f2e3bf' : '#9fb0bf', true))).setDepth(depth + 1);
this.trackSortie(this.add.text(slotX + 76, slotY + 3, this.compactText(slot.unitNames.join(', ') || slot.description, 12), this.textStyle(10, filled ? '#c8d2dd' : '#68727e'))).setDepth(depth + 1);
}); });
this.renderSortieFocusPanel(x + 18, y + 190, width - 36, 86, depth + 1);
} }
private renderSortieFocusPanel(x: number, y: number, width: number, height: number, depth: number) { private renderSortieFocusPanel(x: number, y: number, width: number, height: number, depth: number) {
@@ -11459,7 +11540,6 @@ export class CampScene extends Phaser.Scene {
} }
const status = this.unitRosterStatus(unit); const status = this.unitRosterStatus(unit);
const summary = this.sortieUnitTacticalSummary(unit);
const recommendation = this.sortieRecommendation(unit.id); const recommendation = this.sortieRecommendation(unit.id);
const roleLabel = sortieFormationSlotDefinitions.find((slot) => slot.role === this.sortieFormationRole(unit))?.label ?? '예비'; const roleLabel = sortieFormationSlotDefinitions.find((slot) => slot.role === this.sortieFormationRole(unit))?.label ?? '예비';
const terrainScore = this.sortieTerrainScore(unit, this.nextSortieScenario()); const terrainScore = this.sortieTerrainScore(unit, this.nextSortieScenario());
@@ -11475,24 +11555,92 @@ export class CampScene extends Phaser.Scene {
.join(' / '); .join(' / ');
const reason = recommendation?.reason ?? (this.isSortieSelected(unit.id) ? '현재 출전 편성에 포함되어 있습니다.' : '대기 훈련으로 성장시키거나 전장 역할에 맞춰 교체할 수 있습니다.'); 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.renderSortieBattleUiIcon(this.unitClassIconKey(unit), x + 24, y + 25, 32, depth + 1, 0.98);
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 + 48, 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 + 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.renderSortieStatChip('might', '무', unit.stats.might, x + 48, y + 27, 50, 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.renderSortieStatChip('intelligence', '지', unit.stats.intelligence, x + 104, y + 27, 50, 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); this.renderSortieStatChip('leadership', '통', unit.stats.leadership, x + 160, y + 27, 50, depth + 1);
this.renderSortieStatChip('move', '이', unit.move, x + 216, y + 27, 50, depth + 1);
this.trackSortie(this.add.text(x + 274, y + 30, `지형 ${terrainGrade} ${terrainScore}`, this.textStyle(10, '#d8b15f', true))).setDepth(depth + 1);
this.renderSortieEquipmentIcons(unit, x + 26, y + 52, true, depth + 1, 20, 16, 25);
this.trackSortie(this.add.text(x + 94, y + 47, this.compactText(equipLine, 38), this.textStyle(10, '#c8d2dd', true))).setDepth(depth + 1);
this.trackSortie(
this.add.text(x + 94, y + 61, this.compactText(reason, 28), this.textStyle(9, recommendation ? '#ffdf7b' : '#9fb0bf', Boolean(recommendation)))
).setDepth(depth + 1);
} }
private renderSortieEquipmentIcons(unit: UnitData, x: number, y: number, selected: boolean, depth: number) { private renderSortieBattleUiIcon(iconKey: BattleUiIconKey, x: number, y: number, size: number, depth: number, alpha = 1) {
if (!this.textures.exists(battleUiIconTextureKey)) {
const fallback = this.trackSortie(this.add.circle(x, y, size / 2, palette.gold, 0.18));
fallback.setDepth(depth);
return;
}
const icon = this.trackSortie(this.add.image(x, y, battleUiIconTextureKey, battleUiIconFrames[iconKey]));
icon.setDisplaySize(size, size);
icon.setAlpha(alpha);
icon.setDepth(depth);
}
private unitClassIconKey(unit: UnitData): BattleUiIconKey {
switch (unit.classKey) {
case 'lord':
return 'leadership';
case 'infantry':
return 'armor';
case 'spearman':
return 'spear';
case 'cavalry':
return 'horse';
case 'archer':
return 'bow';
case 'strategist':
return 'strategy';
case 'quartermaster':
return 'heal';
case 'rebelLeader':
return 'axe';
case 'bandit':
case 'yellowTurban':
default:
return 'sword';
}
}
private formationRoleIconKey(role: SortieFormationRole): BattleUiIconKey {
if (role === 'front') {
return 'armor';
}
if (role === 'flank') {
return 'horse';
}
if (role === 'support') {
return 'strategy';
}
return 'focus';
}
private renderSortieStatChip(iconKey: BattleUiIconKey, label: string, value: number, x: number, y: number, width: number, depth: number) {
const bg = this.trackSortie(this.add.rectangle(x, y, width, 17, 0x0a1017, 0.84));
bg.setOrigin(0);
bg.setDepth(depth);
bg.setStrokeStyle(1, 0x53606c, 0.34);
this.renderSortieBattleUiIcon(iconKey, x + 9, y + 8, 14, depth + 1, 0.9);
this.trackSortie(this.add.text(x + 18, y + 3, `${label}${value}`, this.textStyle(9, '#f2e3bf', true))).setDepth(depth + 1);
}
private renderSortieEquipmentIcons(unit: UnitData, x: number, y: number, selected: boolean, depth: number, frameSize = 18, iconSize = 14, spacing = 23) {
equipmentSlots.forEach((slot, index) => { equipmentSlots.forEach((slot, index) => {
const state = unit.equipment[slot]; const state = unit.equipment[slot];
const item = getItem(state.itemId); const item = getItem(state.itemId);
const iconFrame = this.trackSortie(this.add.rectangle(x + index * 23, y, 18, 18, selected && item.rank === 'treasure' ? 0x2c2630 : 0x0a1017, 0.92)); const iconFrame = this.trackSortie(this.add.rectangle(x + index * spacing, y, frameSize, frameSize, selected && item.rank === 'treasure' ? 0x2c2630 : 0x0a1017, 0.92));
iconFrame.setStrokeStyle(1, item.rank === 'treasure' ? palette.gold : 0x53606c, selected ? 0.62 : 0.34); iconFrame.setStrokeStyle(1, item.rank === 'treasure' ? palette.gold : 0x53606c, selected ? 0.62 : 0.34);
iconFrame.setDepth(depth); iconFrame.setDepth(depth);
const icon = this.trackSortie(this.add.image(x + index * 23, y, `item-${item.id}`)); const icon = this.trackSortie(this.add.image(x + index * spacing, y, `item-${item.id}`));
icon.setDisplaySize(14, 14); icon.setDisplaySize(iconSize, iconSize);
icon.setAlpha(selected ? 1 : 0.52); icon.setAlpha(selected ? 1 : 0.52);
icon.setDepth(depth + 1); icon.setDepth(depth + 1);
}); });
@@ -11903,11 +12051,14 @@ export class CampScene extends Phaser.Scene {
const focus = this.reserveTrainingFocusDefinition(); const focus = this.reserveTrainingFocusDefinition();
const reserveCount = this.sortiePlanSummary().reserveCount; 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 + 12, y + 7, '대기 훈련', this.textStyle(12, '#f2e3bf', true))).setDepth(depth + 1);
const focusPreview = reserveCount > 0
? `경험 +${focus.expGained} · 장비 +${focus.equipmentExpGained}${focus.bondExpGained ? ` · 공명 +${focus.bondExpGained}` : ''}`
: '대기 없음';
this.trackSortie( this.trackSortie(
this.add.text( this.add.text(
x + width - 12, x + width - 12,
y + 7, y + 7,
reserveCount > 0 ? `${reserveCount}명 · ${focus.label}` : '대기 없음', reserveCount > 0 ? `${reserveCount}명 · ${focusPreview}` : focusPreview,
this.textStyle(11, reserveCount > 0 ? '#d8b15f' : '#9fb0bf', true) this.textStyle(11, reserveCount > 0 ? '#d8b15f' : '#9fb0bf', true)
) )
).setOrigin(1, 0).setDepth(depth + 1); ).setOrigin(1, 0).setDepth(depth + 1);
@@ -11916,14 +12067,15 @@ export class CampScene extends Phaser.Scene {
campaignReserveTrainingFocusDefinitions.forEach((option, index) => { campaignReserveTrainingFocusDefinitions.forEach((option, index) => {
const buttonX = x + 12 + index * (buttonWidth + 8); const buttonX = x + 12 + index * (buttonWidth + 8);
const selected = option.id === focus.id; 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)); const button = this.trackSortie(this.add.rectangle(buttonX, y + 28, buttonWidth, 20, selected ? 0x263a2d : 0x0e151d, selected ? 0.98 : 0.86));
button.setOrigin(0); button.setOrigin(0);
button.setDepth(depth + 1); button.setDepth(depth + 1);
button.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.78 : 0.42); button.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.78 : 0.42);
button.setInteractive({ useHandCursor: true }); button.setInteractive({ useHandCursor: true });
button.on('pointerdown', () => this.setReserveTrainingFocus(option.id)); 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))); const optionText = `${option.label} ${option.expGained}/${option.equipmentExpGained}${option.bondExpGained ? `/${option.bondExpGained}` : ''}`;
const label = this.trackSortie(this.add.text(buttonX + buttonWidth / 2, y + 33, this.compactText(optionText, 10), this.textStyle(9, selected ? '#a8ffd0' : '#c8d2dd', true)));
label.setOrigin(0.5, 0); label.setOrigin(0.5, 0);
label.setDepth(depth + 2); label.setDepth(depth + 2);
label.setInteractive({ useHandCursor: true }); label.setInteractive({ useHandCursor: true });