diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index fb0d7bb..511bdb6 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -586,6 +586,12 @@ const sortieFormationSlotDefinitions: Omit[] = { role: 'support', label: '후원', description: '원거리, 책략, 보급 지원' }, { role: 'reserve', label: '예비', description: '빈틈 보강과 공명 연결' } ]; +const campCompanionRoleDefinitions: Omit[] = [ + { role: 'front', label: '대표', description: '대화의 중심과 군영 결정을 맡음' }, + { role: 'flank', label: '현장', description: '다음 장면의 현장 판단과 증언 담당' }, + { role: 'support', label: '보좌', description: '문서, 보급, 책략 기록을 보탬' }, + { role: 'reserve', label: '기록', description: '남은 기록과 공명 정리 담당' } +]; const defaultSortieRule: SortieRuleDefinition = { maxUnits: maxSortieUnits, @@ -12640,11 +12646,11 @@ export class CampScene extends Phaser.Scene { this.trackSortie(this.add.text(slotX + 8, slotY + 5, `${index + 1}`, this.textStyle(10, labelColor, true))).setDepth(depth + 1); if (unit && role) { this.renderSortieBattleUiIcon(this.formationRoleIconKey(role), slotX + 28, slotY + slotHeight / 2, 18, depth + 1, 0.96); - const roleLabel = this.sortieFormationRoleLabel(role); + const roleLabel = this.sortieFormationRoleLabel(role, hasBattle); this.trackSortie(this.add.text(slotX + 42, slotY + 4, this.compactText(unit.name, 8), this.textStyle(11, '#f2e3bf', true))).setDepth(depth + 1); this.trackSortie(this.add.text(slotX + 42, slotY + 17, roleLabel, this.textStyle(8, '#9fb0bf', true))).setDepth(depth + 1); } else { - this.trackSortie(this.add.text(slotX + 28, slotY + 8, '빈 슬롯', this.textStyle(10, '#7f8994', true))).setDepth(depth + 1); + this.trackSortie(this.add.text(slotX + 28, slotY + 8, hasBattle ? '빈 슬롯' : '빈 동행', this.textStyle(10, '#7f8994', true))).setDepth(depth + 1); } }); } @@ -12652,18 +12658,20 @@ export class CampScene extends Phaser.Scene { private renderSortieRoleQuickButtons(x: number, y: number, width: number, height: number, depth: number) { const focusedUnit = this.sortieFocusedUnit(); const enabled = Boolean(focusedUnit && this.isSortieSelected(focusedUnit.id)); + const hasBattle = Boolean(this.nextSortieScenario()); const gap = 6; const buttonWidth = Math.floor((width - gap * (sortieFormationSlotDefinitions.length - 1)) / sortieFormationSlotDefinitions.length); sortieFormationSlotDefinitions.forEach((slot, index) => { const buttonX = x + index * (buttonWidth + gap); const active = Boolean(focusedUnit && this.sortieFormationRole(focusedUnit) === slot.role); + const roleCopy = this.sortieFormationRoleCopy(slot.role, hasBattle); const bg = this.trackSortie(this.add.rectangle(buttonX, y, buttonWidth, height, active ? 0x263a2d : 0x111922, enabled ? 0.94 : 0.58)); bg.setOrigin(0); bg.setDepth(depth); bg.setStrokeStyle(1, active ? palette.green : 0x53606c, active ? 0.78 : 0.36); bg.setInteractive({ useHandCursor: true }); bg.on('pointerdown', () => this.assignFocusedSortieRole(slot.role)); - this.trackSortie(this.add.text(buttonX + buttonWidth / 2, y + 5, slot.label, this.textStyle(9, active ? '#a8ffd0' : enabled ? '#c8d2dd' : '#77818c', true))).setOrigin(0.5, 0).setDepth(depth + 1); + this.trackSortie(this.add.text(buttonX + buttonWidth / 2, y + 5, roleCopy.label, this.textStyle(9, active ? '#a8ffd0' : enabled ? '#c8d2dd' : '#77818c', true))).setOrigin(0.5, 0).setDepth(depth + 1); }); } @@ -12716,7 +12724,7 @@ export class CampScene extends Phaser.Scene { const availability = this.sortieUnitAvailability(unit); const recommendation = this.sortieRecommendation(unit.id); const hasBattle = Boolean(this.nextSortieScenario()); - const roleLabel = this.sortieFormationRoleLabel(this.sortieFormationRole(unit)); + const roleLabel = this.sortieFormationRoleLabel(this.sortieFormationRole(unit), hasBattle); const terrainScore = this.sortieTerrainScore(unit, this.nextSortieScenario()); const terrainGrade = this.sortieTerrainGrade(terrainScore); const statusColor = status.tone === 'disabled' ? '#77818c' : status.tone === 'selected' ? '#a8ffd0' : status.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf'; @@ -12778,7 +12786,7 @@ export class CampScene extends Phaser.Scene { ? '출전 등록' : '동행 등록'; this.renderSortiePanelButton(actionLabel, x + 18, y + height - 28, 108, 22, availability.available, selected, () => this.toggleSortieUnit(unit.id), depth + 1); - this.trackSortie(this.add.text(x + 140, y + height - 24, `${roleLabel} · 역할은 중앙 버튼으로 변경`, this.textStyle(10, selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 1); + this.trackSortie(this.add.text(x + 140, y + height - 24, hasBattle ? `${roleLabel} · 역할은 중앙 버튼으로 변경` : `${roleLabel} · 의정 역할은 중앙 버튼으로 변경`, this.textStyle(10, selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 1); } private renderSortieBattleUiIcon(iconKey: BattleUiIconKey, x: number, y: number, size: number, depth: number, alpha = 1) { @@ -12832,8 +12840,13 @@ export class CampScene extends Phaser.Scene { return 'focus'; } - private sortieFormationRoleLabel(role: SortieFormationRole) { - return sortieFormationSlotDefinitions.find((slot) => slot.role === role)?.label ?? '예비'; + private sortieFormationRoleCopy(role: SortieFormationRole, hasBattle = Boolean(this.nextSortieScenario())) { + const definitions = hasBattle ? sortieFormationSlotDefinitions : campCompanionRoleDefinitions; + return definitions.find((slot) => slot.role === role) ?? definitions[definitions.length - 1]; + } + + private sortieFormationRoleLabel(role: SortieFormationRole, hasBattle = Boolean(this.nextSortieScenario())) { + return this.sortieFormationRoleCopy(role, hasBattle).label; } private sortieRecommendationHint(recommendation: SortieRecommendation, unit?: UnitData, maxReasonLength = 20) { @@ -13190,8 +13203,8 @@ export class CampScene extends Phaser.Scene { frame.setStrokeStyle(1, palette.blue, 0.48); if (!scenario) { - this.trackSortie(this.add.text(x + 14, y + 22, '전투 배치 없음', this.textStyle(12, '#d8b15f', true))).setDepth(depth + 1); - this.trackSortie(this.add.text(x + 14, y + 40, '다음 장면에 함께할 동행 명단을 확인합니다.', this.textStyle(11, '#9fb0bf'))).setDepth(depth + 1); + this.trackSortie(this.add.text(x + 14, y + 20, '군영 동행 확인', this.textStyle(12, '#d8b15f', true))).setDepth(depth + 1); + this.trackSortie(this.add.text(x + 14, y + 38, '전투 배치 없이 의정에 함께할 무장을 정리합니다.', this.textStyle(11, '#9fb0bf'))).setDepth(depth + 1); return; } @@ -13303,8 +13316,9 @@ export class CampScene extends Phaser.Scene { }, { front: 0, flank: 0, support: 0, reserve: 0 } as Record ); + const hasBattle = Boolean(scenario); const formationSlots = sortieFormationSlotDefinitions.map((slot) => ({ - ...slot, + ...this.sortieFormationRoleCopy(slot.role, hasBattle), unitNames: selectedUnits.filter((unit) => this.sortieFormationRole(unit) === slot.role).map((unit) => unit.name) })); const warnings: string[] = []; @@ -13319,21 +13333,36 @@ export class CampScene extends Phaser.Scene { if (missingClassRecommendations.length > 0) { warnings.push(`추천 병종 ${missingClassRecommendations.map((entry) => entry.label).join(', ')} 부족.`); } - if (roleCounts.front <= 0) { - warnings.push('전열 담당이 없어 길목을 버티기 어렵습니다.'); - } - if (roleCounts.flank <= 0) { - warnings.push('돌파 담당이 없어 추격과 측면 압박이 느릴 수 있습니다.'); - } - if (roleCounts.support <= 0) { - warnings.push('후원 담당이 없어 회복, 보급, 원거리 견제가 약합니다.'); + if (hasBattle) { + if (roleCounts.front <= 0) { + warnings.push('전열 담당이 없어 길목을 버티기 어렵습니다.'); + } + if (roleCounts.flank <= 0) { + warnings.push('돌파 담당이 없어 추격과 측면 압박이 느릴 수 있습니다.'); + } + if (roleCounts.support <= 0) { + warnings.push('후원 담당이 없어 회복, 보급, 원거리 견제가 약합니다.'); + } + } else { + if (roleCounts.front <= 0) { + warnings.push('대표 동행이 없어 의정의 중심이 약합니다.'); + } + if (roleCounts.flank <= 0) { + warnings.push('현장 증언을 맡을 동행이 부족합니다.'); + } + if (roleCounts.support <= 0) { + warnings.push('보좌 기록을 맡을 동행이 부족합니다.'); + } } if (activeBondCount <= 0 && selectedUnits.length > 1) { warnings.push('함께하는 공명 조합이 없습니다.'); } - if (terrainScore > 0 && terrainScore < 98) { + if (hasBattle && terrainScore > 0 && terrainScore < 98) { warnings.push('선택 무장의 평균 지형 적성이 낮습니다.'); } + const roleLine = hasBattle + ? `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support} · 예비 ${roleCounts.reserve}` + : `대표 ${roleCounts.front} · 현장 ${roleCounts.flank} · 보좌 ${roleCounts.support} · 기록 ${roleCounts.reserve}`; return { selectedCount: selectedUnits.length, @@ -13346,7 +13375,7 @@ export class CampScene extends Phaser.Scene { reserveCount: reserveUnits.length, recommendedSelectedCount, recommendedTotal: recommended.length, - deploymentLine: `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support} · 예비 ${roleCounts.reserve}`, + deploymentLine: roleLine, recommendationLine: recommended.length > 0 ? `추천 무장 ${recommendedSelectedCount}/${recommended.length} · 병종 ${this.sortieRecommendedClassLine(scenario)}` : `추천 병종 ${this.sortieRecommendedClassLine(scenario)}`, @@ -13366,7 +13395,7 @@ export class CampScene extends Phaser.Scene { reserveTrainingBondPreview: reserveFocus.bondExpGained, objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : `${flow.title} · 군영 의정`, formationSlots, - warningLine: warnings[0] ?? '배치 균형 양호', + warningLine: warnings[0] ?? (hasBattle ? '배치 균형 양호' : '동행 균형 양호'), warnings }; } @@ -13477,7 +13506,10 @@ export class CampScene extends Phaser.Scene { ); const names = unitIds.slice(0, 3).map((id) => this.unitName(id)).join(', '); const extra = unitIds.length > 3 ? ` 외 ${unitIds.length - 3}명` : ''; - return `${hasBattle ? '추천 편성' : '추천 동행'} 적용 · ${names}${extra} · 전열 ${roleCounts.front} / 돌파 ${roleCounts.flank} / 후원 ${roleCounts.support}`; + const roleLine = hasBattle + ? `전열 ${roleCounts.front} / 돌파 ${roleCounts.flank} / 후원 ${roleCounts.support}` + : `대표 ${roleCounts.front} / 현장 ${roleCounts.flank} / 보좌 ${roleCounts.support}`; + return `${hasBattle ? '추천 편성' : '추천 동행'} 적용 · ${names}${extra} · ${roleLine}`; } private sortieTerrainScore(unit: UnitData, scenario: BattleScenarioDefinition | undefined) {