feat: streamline victory debrief flow

This commit is contained in:
2026-07-28 06:48:05 +09:00
parent 5f6490b2ae
commit 7e06ffb9f5
14 changed files with 1224 additions and 439 deletions

View File

@@ -160,7 +160,7 @@ const sortieFlows: Record<string, SortieFlow> = {
eyebrow: '다음 전장',
title: secondBattleScenario.title,
description: '탁현에서 선봉을 잃고 퇴각한 한석이 잔당을 이끌고 북쪽 나루와 마을을 붙잡았습니다. 첫 승리 보상으로 정비를 마친 뒤, 세 형제가 한석의 퇴로를 따라 추격합니다.',
rewardHint: '지난 보상: 콩, 탁주, 연습검 · 다음 보상: 소모품, 황건 부적, 의용군 명성',
rewardHint: '다음 보상: 소모품, 황건 부적, 의용군 명성',
nextBattleId: secondBattleScenario.id,
campaignStep: 'second-battle',
pages: secondBattleIntroPages
@@ -170,7 +170,7 @@ const sortieFlows: Record<string, SortieFlow> = {
eyebrow: '다음 전장',
title: thirdBattleScenario.title,
description: '북쪽 마을을 구했지만 황건 전령이 광종 본영으로 향하고 있습니다. 나루 추격전에서 얻은 보급과 부적을 점검하고, 강가 요새로 이어지는 길을 끊어야 합니다.',
rewardHint: '지난 보상: 콩, 상처약, 탁주, 황건 부적 · 다음 보상: 단궁, 의용군 명성',
rewardHint: '다음 보상: 단궁, 의용군 명성',
nextBattleId: thirdBattleScenario.id,
campaignStep: 'third-battle',
pages: thirdBattleIntroPages

View File

@@ -15365,6 +15365,7 @@ export class BattleScene extends Phaser.Scene {
const totalEnemies = battleUnits.filter((unit) => unit.faction === 'enemy').length;
const aliveAllies = allies.filter((unit) => unit.hp > 0).length;
const objectives = this.resultObjectives(outcome);
const achievedObjectiveCount = objectives.filter((objective) => objective.achieved).length;
const rewardSnapshot = this.resultCampaignRewards(outcome);
const shade = this.trackResultObject(this.add.rectangle(0, 0, battleReferenceWidth, battleReferenceHeight, 0x020406, 0.68));
@@ -15388,7 +15389,7 @@ export class BattleScene extends Phaser.Scene {
title.setOrigin(0.5, 0);
title.setDepth(depth + 2);
const subtitleText = this.resultSubtitle(outcome, rewardSnapshot);
const subtitleText = this.resultSubtitle(outcome);
const subtitle = this.trackResultObject(this.add.text(left + panelWidth / 2, top + 72, subtitleText, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '17px',
@@ -15409,7 +15410,7 @@ export class BattleScene extends Phaser.Scene {
this.renderResultMetric('소요 턴', `${this.turnNumber}`, left + 44, metricTop, metricWidth, depth + 2);
this.renderResultMetric('생존 아군', `${aliveAllies} / ${allies.length}`, left + 44 + (metricWidth + metricGap), metricTop, metricWidth, depth + 2);
this.renderResultMetric('격파 적군', `${defeatedEnemies} / ${totalEnemies}`, left + 44 + (metricWidth + metricGap) * 2, metricTop, metricWidth, depth + 2);
this.renderResultMetric('전투 보상', outcome === 'victory' ? `군자금 ${this.resultDisplayedGold(outcome)}` : '없음', left + 44 + (metricWidth + metricGap) * 3, metricTop, metricWidth, depth + 2);
this.renderResultMetric('달성 목표', `${achievedObjectiveCount} / ${objectives.length}`, left + 44 + (metricWidth + metricGap) * 3, metricTop, metricWidth, depth + 2);
this.renderResultObjectivePanel(objectives, left + 44, top + 178, 456, depth + 2);
this.renderResultRewardPanel(outcome, objectives, rewardSnapshot, left + 526, top + 178, 470, depth + 2);
@@ -16051,13 +16052,11 @@ export class BattleScene extends Phaser.Scene {
this.resultSortieOrder = persistedReport.sortieOrder ?? sortieOrder;
}
private resultSubtitle(outcome: BattleOutcome, rewards: CampaignRewardSnapshot) {
private resultSubtitle(outcome: BattleOutcome) {
const base =
outcome !== 'victory'
? `${battleScenario.title}에서 패배했습니다. 진형과 보급을 정비한 뒤 다시 도전해야 합니다.`
: rewards.unlocks[0]?.title
? `${battleScenario.title} 승리. 보상 정산 후 ${rewards.unlocks[0].title}로 이어집니다.`
: `${battleScenario.title} 승리. 획득 보상과 장수 성장을 정산합니다.`;
: `${battleScenario.title} 승리. 보상 정산 후 승리 후일담으로 이어집니다.`;
const preparation =
this.thirdCampPreparationPresentation('result');
const xuzhouResult =
@@ -17061,10 +17060,10 @@ export class BattleScene extends Phaser.Scene {
...rewards.equipment,
...rewards.reputation
].join(' · ');
const progressLine = [
rewards.recruits.length > 0 ? `${rewards.recruits.map((recruit) => recruit.name).join('·')} 합류` : '',
rewards.unlocks.length > 0 ? `${rewards.unlocks.map((unlock) => unlock.title).join('·')} 해금` : ''
].filter(Boolean).join(' · ');
const progressCount = rewards.recruits.length + rewards.unlocks.length;
const progressLine = progressCount > 0
? `신규 합류 ${rewards.recruits.length}명 · 새 전장 ${rewards.unlocks.length}`
: '새로운 합류·해금 없음';
const rewardCards: Array<{
label: string;
value: string;
@@ -17098,8 +17097,8 @@ export class BattleScene extends Phaser.Scene {
},
{
label: '새 전력 · 해금',
value: progressLine || rewards.note || '새 해금 없음',
badge: rewards.recruits.length + rewards.unlocks.length > 0 ? 'NEW' : '변화 없음',
value: progressLine,
badge: progressCount > 0 ? 'NEW' : '변화 없음',
icon: 'success',
accent: palette.green
}

View File

@@ -358,7 +358,16 @@ type CampTabButtonView = {
type VictoryRewardCardId = CampaignVictoryRewardCategoryId;
type VictoryRewardActionId = 'equipment' | 'supplies' | 'guided' | 'sortie' | 'close';
type VictoryRewardActionId = 'equipment' | 'supplies' | 'sortie' | 'close';
type VictorySettlementArrival = {
battleId: string;
battleTitle: string;
message: string;
settledBattleIds: string[];
pendingCategories: CampaignVictoryRewardCategoryId[];
passiveCategoriesAcknowledged: CampaignVictoryRewardCategoryId[];
};
type VictoryRewardIcon =
| { kind: 'mark'; mark: string }
@@ -11471,9 +11480,12 @@ export class CampScene extends Phaser.Scene {
private navigationBlockerObjects: Phaser.GameObjects.GameObject[] = [];
private pendingSaveSlot?: number;
private pendingVictoryRewardReport?: CampaignVictoryRewardReport;
private victoryRewardLedgerReport?: CampaignVictoryRewardReport;
private victorySettlementArrival?: VictorySettlementArrival;
private victoryRewardObjects: Phaser.GameObjects.GameObject[] = [];
private victoryRewardCards: VictoryRewardCardView[] = [];
private victoryRewardActions: VictoryRewardActionView[] = [];
private reportVictoryRewardLedgerButton?: Phaser.GameObjects.Rectangle;
private reportFormationReviewObjects: Phaser.GameObjects.GameObject[] = [];
private reportFormationReviewVisible = false;
private reportFormationReviewFeedback = '';
@@ -11635,9 +11647,12 @@ export class CampScene extends Phaser.Scene {
this.navigationBlockerObjects = [];
this.pendingSaveSlot = undefined;
this.pendingVictoryRewardReport = undefined;
this.victoryRewardLedgerReport = undefined;
this.victorySettlementArrival = undefined;
this.victoryRewardObjects = [];
this.victoryRewardCards = [];
this.victoryRewardActions = [];
this.reportVictoryRewardLedgerButton = undefined;
this.equipmentSwapConfirmObjects = [];
this.pendingEquipmentSwap = undefined;
this.equipmentSwapConfirmPanel = undefined;
@@ -11676,6 +11691,9 @@ export class CampScene extends Phaser.Scene {
this.campaign = getCampaignState();
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
this.pendingVictoryRewardReport = getPendingCampaignVictoryRewardNoticeReport();
if (this.pendingVictoryRewardReport) {
this.settleVictoryRewardArrival(this.pendingVictoryRewardReport);
}
if (!this.retrySortieBattleId) {
this.ensureCurrentCampRecruitment();
}
@@ -11875,8 +11893,12 @@ export class CampScene extends Phaser.Scene {
this.renderStaticButtons();
this.render();
if (this.pendingVictoryRewardReport) {
this.time.delayedCall(0, () => this.showVictoryRewardAcknowledgement());
if (this.victorySettlementArrival) {
this.time.delayedCall(0, () => {
if (this.victorySettlementArrival) {
this.showCampNotice(this.victorySettlementArrival.message);
}
});
} else if (this.openSortiePrepOnCreate) {
this.time.delayedCall(0, () => {
this.showRequestedSortiePrep();
@@ -12714,8 +12736,8 @@ export class CampScene extends Phaser.Scene {
}
const achieved = this.report.objectives.filter((objective) => objective.achieved).length;
const gold = this.campaign?.gold ?? this.report.rewardGold;
return `${this.report.turnNumber}승리 · 군자금 ${gold} · 목표 ${achieved}/${this.report.objectives.length}`;
const outcome = this.report.outcome === 'victory' ? '승리' : '패배';
return `${this.report.turnNumber}${outcome} · 목표 ${achieved}/${this.report.objectives.length} · 군영 준비 중`;
}
private currentCampTitle() {
@@ -13576,13 +13598,14 @@ export class CampScene extends Phaser.Scene {
this.pendingSaveSlot = undefined;
}
private showVictoryRewardAcknowledgement() {
let report = this.pendingVictoryRewardReport ?? getPendingCampaignVictoryRewardNoticeReport();
if (!report || report.outcome !== 'victory' || this.sortieObjects.length > 0) {
private settleVictoryRewardArrival(report: CampaignVictoryRewardReport) {
if (report.outcome !== 'victory') {
this.pendingVictoryRewardReport = undefined;
return;
}
this.hideVictoryRewardAcknowledgement();
const passiveCategoriesAcknowledged = getPendingCampaignVictoryRewardCategories(report.battleId)
.filter((category) => category === 'gold' || category === 'reputation');
getPendingCampaignVictoryRewardBattleIds().forEach((battleId) => {
const passiveCategories = getPendingCampaignVictoryRewardCategories(battleId)
.filter((category) => category === 'gold' || category === 'reputation');
@@ -13590,12 +13613,42 @@ export class CampScene extends Phaser.Scene {
this.campaign = acknowledgeCampaignVictoryReward(battleId, passiveCategories);
}
});
report = getPendingCampaignVictoryRewardNoticeReport();
if (!report) {
this.pendingVictoryRewardReport = undefined;
const settledBattleIds: string[] = [];
const seenBattleIds = new Set<string>();
seenBattleIds.add(report.battleId);
settledBattleIds.push(report.battleId);
this.campaign = dismissCampaignVictoryRewardNotice(report.battleId);
let pendingNotice = getPendingCampaignVictoryRewardNoticeReport();
while (pendingNotice && !seenBattleIds.has(pendingNotice.battleId)) {
seenBattleIds.add(pendingNotice.battleId);
settledBattleIds.push(pendingNotice.battleId);
this.campaign = dismissCampaignVictoryRewardNotice(pendingNotice.battleId);
pendingNotice = getPendingCampaignVictoryRewardNoticeReport();
}
const message =
`${report.battleTitle} 정산 완료 · 세부 획득 내역은 하단 전투 보고의 보상 장부에 기록했습니다.\n` +
'NEW 표식이 있는 준비 화면과 상단 다음 할 일을 따라 군영을 둘러보십시오.';
this.victorySettlementArrival = {
battleId: report.battleId,
battleTitle: report.battleTitle,
message,
settledBattleIds,
pendingCategories: getPendingCampaignVictoryRewardCategories(report.battleId),
passiveCategoriesAcknowledged,
};
this.pendingVictoryRewardReport = undefined;
}
private showVictoryRewardAcknowledgement(reportOverride?: CampaignVictoryRewardReport) {
const report = reportOverride ?? this.report ?? getPendingCampaignVictoryRewardNoticeReport();
if (!report || report.outcome !== 'victory' || this.sortieObjects.length > 0) {
return;
}
this.pendingVictoryRewardReport = report;
this.hideVictoryRewardAcknowledgement();
this.victoryRewardLedgerReport = report;
const rewards = report.campaignRewards;
const presentation = campaignVictoryRewardPresentation(report);
const pendingCategorySet = new Set(getPendingCampaignVictoryRewardCategories(report.battleId));
@@ -13617,9 +13670,6 @@ export class CampScene extends Phaser.Scene {
const panelY = 90;
const panelWidth = 884;
const panelHeight = 546;
const firstCampGuidedAction = report.battleId === campBattleIds.first
? this.firstCampGuidedAction()
: undefined;
const shade = this.trackVictoryReward(
this.add.rectangle(0, 0, campLegacyCanvasWidth, campLegacyCanvasHeight, 0x020406, 0.78)
@@ -13637,12 +13687,12 @@ export class CampScene extends Phaser.Scene {
panel.setInteractive();
const eyebrow = this.trackVictoryReward(
this.add.text(panelX + 34, panelY + 24, 'VICTORY REWARD', this.textStyle(11, '#d8b15f', true))
this.add.text(panelX + 34, panelY + 24, 'BATTLE LEDGER', this.textStyle(11, '#d8b15f', true))
);
eyebrow.setDepth(depth + 2);
const title = this.trackVictoryReward(
this.add.text(panelX + 34, panelY + 45, '이번 승전 반영', this.textStyle(30, '#f2e3bf', true))
this.add.text(panelX + 34, panelY + 45, '보상 장부', this.textStyle(30, '#f2e3bf', true))
);
title.setDepth(depth + 2);
@@ -13656,7 +13706,7 @@ export class CampScene extends Phaser.Scene {
this.add.text(
panelX + 34,
panelY + 88,
`${report.battleTitle} 승리 보상이 군영 자산에 반영되었습니다.`,
`${report.battleTitle}에서 획득한 세부 내역입니다. 군영에서 언제든 다시 확인할 수 있습니다.`,
this.textStyle(14, '#d4dce6', true)
)
);
@@ -13822,7 +13872,7 @@ export class CampScene extends Phaser.Scene {
this.add.text(
panelX + 34,
panelY + 378,
note ? `전황 기록 · ${note}` : '보상 확인 후 필요한 정비 화면으로 바로 이동할 수 있습니다.',
note ? `전황 기록 · ${note}` : 'NEW 표시는 각 준비 화면에서 해당 자산을 확인하면 해제됩니다.',
{
...this.textStyle(12, note ? '#ffdf7b' : '#9fb0bf', Boolean(note)),
wordWrap: { width: panelWidth - 68, useAdvancedWrap: true }
@@ -13831,24 +13881,15 @@ export class CampScene extends Phaser.Scene {
);
noteText.setDepth(depth + 2);
const actionDefinitions: Array<{ id: VictoryRewardActionId; label: string; primary?: boolean }> =
firstCampGuidedAction
? [
{ id: 'equipment', label: '장비 확인' },
{ id: 'supplies', label: '보급 확인' },
{ id: 'guided', label: firstCampGuidedAction.label, primary: true },
{ id: 'sortie', label: '바로 출진' },
{ id: 'close', label: '닫기' }
]
: [
{ id: 'equipment', label: '장비 확인' },
{ id: 'supplies', label: '보급 확인' },
{ id: 'sortie', label: '출진 준비', primary: true },
{ id: 'close', label: '닫기' }
];
const buttonWidth = firstCampGuidedAction ? 156 : 158;
const actionDefinitions: Array<{ id: VictoryRewardActionId; label: string; primary?: boolean }> = [
{ id: 'equipment', label: '장비 확인' },
{ id: 'supplies', label: '보급 확인' },
{ id: 'sortie', label: '출진 준비' },
{ id: 'close', label: '장부 닫기', primary: true }
];
const buttonWidth = 158;
const buttonHeight = 48;
const actionGap = firstCampGuidedAction ? 8 : 14;
const actionGap = 14;
const totalActionWidth = actionDefinitions.length * buttonWidth + (actionDefinitions.length - 1) * actionGap;
const actionStartX = panelX + Math.floor((panelWidth - totalActionWidth) / 2);
actionDefinitions.forEach((definition, index) => {
@@ -13869,11 +13910,7 @@ export class CampScene extends Phaser.Scene {
buttonX + buttonWidth / 2,
buttonY + buttonHeight / 2,
definition.label,
this.textStyle(
definition.id === 'guided' && Array.from(definition.label).length > 8 ? 12 : 14,
definition.primary ? '#fff2b8' : '#f2e3bf',
true
)
this.textStyle(14, definition.primary ? '#fff2b8' : '#f2e3bf', true)
)
);
label.setOrigin(0.5);
@@ -13896,14 +13933,13 @@ export class CampScene extends Phaser.Scene {
primary: Boolean(definition.primary)
});
});
soundDirector.playRewardRevealCue();
}
private completeVictoryRewardAcknowledgement(
action: VictoryRewardActionId,
categories = this.victoryRewardCategoriesForAction(action)
) {
const report = this.pendingVictoryRewardReport;
const report = this.victoryRewardLedgerReport;
const rewardedRecruitId = report?.campaignRewards?.recruits[0]?.unitId;
if (action === 'sortie' && rewardedRecruitId && this.sortieAllies().some((unit) => unit.id === rewardedRecruitId)) {
this.focusVictoryRewardRecruit(rewardedRecruitId);
@@ -13930,11 +13966,7 @@ export class CampScene extends Phaser.Scene {
this.render();
return;
}
if (action === 'guided') {
this.runFirstCampGuidedAction();
return;
}
if (action === 'sortie' || (action === 'close' && this.openSortiePrepOnCreate)) {
if (action === 'sortie') {
this.showRequestedSortiePrep();
}
}
@@ -13981,6 +14013,7 @@ export class CampScene extends Phaser.Scene {
this.victoryRewardObjects = [];
this.victoryRewardCards = [];
this.victoryRewardActions = [];
this.victoryRewardLedgerReport = undefined;
}
private rewardInventoryLabel(label: string) {
@@ -21950,6 +21983,7 @@ export class CampScene extends Phaser.Scene {
return;
}
this.reportVictoryRewardLedgerButton = undefined;
this.reportFormationReviewToggleButton = undefined;
this.reportFormationHistoryToggleButton = undefined;
this.reportSortieOrderRewardBadge = undefined;
@@ -21964,15 +21998,22 @@ export class CampScene extends Phaser.Scene {
this.add.text(
x + 18,
y + 39,
this.compactText(`MVP ${report.mvp?.name ?? '-'} · 전투 보상 ${report.rewardGold} · 전리품 ${report.itemRewards.join(', ') || '없음'}`, 56),
this.compactText(
`${report.outcome === 'victory' ? '승리' : '패배'} · ${report.battleTitle} · ${report.turnNumber}턴 · MVP ${report.mvp?.name ?? '-'}`,
56
),
this.textStyle(14, '#d4dce6')
)
);
const achievedObjectiveCount = report.objectives.filter((objective) => objective.achieved).length;
this.track(
this.add.text(
x + 18,
y + 62,
this.compactText(this.reportCampaignRewardLine(report), 64),
this.compactText(
`목표 ${achievedObjectiveCount}/${report.objectives.length} · 격파 ${report.defeatedEnemies}/${report.totalEnemies} · 세부 보상과 전과는 보상 장부에서 확인`,
64
),
this.textStyle(12, '#d8b15f', true)
)
);
@@ -21997,7 +22038,7 @@ export class CampScene extends Phaser.Scene {
this.add.text(
x + 998,
y + 11,
this.compactText(`첫 공훈 · ${definition.label} · 군자금 +${definition.rewardGold} · ${definition.rewardItems.join(', ')}`, 44),
this.compactText(`첫 공훈 · ${definition.label} · 추가 보상 반영 완료`, 44),
this.textStyle(9, '#fff2b8', true)
)
).setOrigin(0.5);
@@ -22008,6 +22049,14 @@ export class CampScene extends Phaser.Scene {
const recommendationOutcome = review ? this.reportSortieRecommendationOutcome(review) : undefined;
const historyRecords = this.reportFormationHistoryRecords();
const actions: { label: string; action: () => void; assign: (button: Phaser.GameObjects.Rectangle) => void }[] = [];
if (report.outcome === 'victory') {
const pendingRewardCount = getPendingCampaignVictoryRewardCategories(report.battleId).length;
actions.push({
label: pendingRewardCount > 0 ? `보상 장부 NEW ${pendingRewardCount}` : '보상 장부',
action: () => this.showVictoryRewardAcknowledgement(report),
assign: (button) => { this.reportVictoryRewardLedgerButton = button; }
});
}
if (review) {
actions.push({
label: recommendationOutcome ? `추천 검증 ${recommendationOutcome.statusLabel}` : `최근 평가 ${review.grade}`,
@@ -22404,25 +22453,6 @@ export class CampScene extends Phaser.Scene {
return background;
}
private reportCampaignRewardLine(report: FirstBattleReport) {
const rewards = report.campaignRewards;
if (!rewards) {
return '캠페인 보상: 기존 전리품이 군영 장부에 반영되었습니다.';
}
const recruits = rewards.recruits ?? [];
const equipment = rewards.equipment ?? [];
const reputation = rewards.reputation ?? [];
const unlocks = rewards.unlocks ?? [];
const parts = [
recruits.length > 0 ? `합류 ${recruits.map((recruit) => recruit.name).join(', ')}` : '',
equipment.length > 0 ? `장비 ${equipment.join(', ')}` : '',
reputation.length > 0 ? `명성 ${reputation.join(', ')}` : '',
unlocks.length > 0 ? `다음 ${unlocks.map((unlock) => unlock.title).join(', ')}` : ''
].filter(Boolean);
return parts.length > 0 ? `캠페인 반영: ${parts.join(' · ')}` : '캠페인 보상: 추가 해금 없음';
}
private showReportFormationReview() {
if (!this.reportFormationReview()) {
return;
@@ -26770,9 +26800,24 @@ export class CampScene extends Phaser.Scene {
return {
scene: this.scene.key,
visualMotionReduced: this.visualMotionReduced,
victorySettlementArrival: this.victorySettlementArrival
? {
battleId: this.victorySettlementArrival.battleId,
battleTitle: this.victorySettlementArrival.battleTitle,
message: this.victorySettlementArrival.message,
settledBattleIds: [...this.victorySettlementArrival.settledBattleIds],
pendingCategories: [...this.victorySettlementArrival.pendingCategories],
passiveCategoriesAcknowledged: [...this.victorySettlementArrival.passiveCategoriesAcknowledged],
blocking: false
}
: null,
victoryRewardAcknowledgement: {
role: 'on-demand-ledger',
available: this.report?.outcome === 'victory',
visible: this.victoryRewardObjects.length > 0,
battleId: this.pendingVictoryRewardReport?.battleId ?? null,
battleId: this.victoryRewardLedgerReport?.battleId ?? this.report?.battleId ?? null,
toggleButtonBounds: this.sortieObjectBoundsDebug(this.reportVictoryRewardLedgerButton),
toggleInteractive: Boolean(this.reportVictoryRewardLedgerButton?.input?.enabled),
cards: this.victoryRewardCards.map((card) => ({
id: card.id,
label: card.label,

View File

@@ -1,7 +1,6 @@
import Phaser from 'phaser';
import { soundDirector } from '../audio/SoundDirector';
import { musicTracks, type MusicTrackKey } from '../audio/audioAssets';
import { equipmentItemIdForInventoryLabel } from '../data/battleItems';
import {
battleUiIconFrames,
battleUiIconMicroTextureKey,
@@ -57,11 +56,7 @@ import {
} from '../data/storyCutscenes';
import { prologueOpeningPages } from '../data/prologueVillage';
import { type PortraitKey, type StoryPage } from '../data/scenario';
import {
campaignVictoryRewardPresentation,
getCampaignState,
getFirstBattleReport
} from '../state/campaignState';
import { getCampaignState, getFirstBattleReport } from '../state/campaignState';
import { releaseTextureSource } from '../render/loaderLifecycle';
import { isVisualMotionReduced } from '../settings/visualMotion';
import { fitDialogueFacePortrait } from '../ui/dialoguePortrait';
@@ -1981,51 +1976,12 @@ export class StoryScene extends Phaser.Scene {
return fallback;
}
const rewards = report.campaignRewards;
const presentation = campaignVictoryRewardPresentation(report);
const categorizedRewardItems = new Set([
...(rewards?.supplies ?? []),
...(rewards?.equipment ?? []),
...(rewards?.reputation ?? [])
]);
const legacyExtraItems = report.itemRewards.filter((item) => !categorizedRewardItems.has(item));
const supplyItems = rewards
? [
...rewards.supplies,
...legacyExtraItems,
...(presentation.sortieOrderBonus?.rewardItems ?? [])
]
: [...presentation.itemRewards];
if (!rewards && page.id !== 'first-victory-village') {
return fallback;
}
let items: StoryRewardDisplayItem[] = [];
if (page.id === 'first-victory-village') {
items = [this.goldRewardItem(presentation.rewardGold), ...supplyItems.map((label, index) => this.supplyRewardItem(label, index))];
} else if (page.id === 'first-victory-liu-bei' && rewards) {
items = [
...rewards.equipment.map((label, index) => this.equipmentRewardItem(label, index)),
...rewards.reputation.map((label, index) => this.reputationRewardItem(label, index))
];
} else if (page.id === 'first-victory-guan-yu' && rewards) {
items = rewards.recruits
.filter((recruit) => recruit.unitId === 'guan-yu')
.map((recruit) => this.recruitRewardItem(recruit.unitId, recruit.name));
} else if (page.id === 'first-victory-zhang-fei' && rewards) {
items = rewards.recruits
.filter((recruit) => recruit.unitId !== 'guan-yu')
.map((recruit) => this.recruitRewardItem(recruit.unitId, recruit.name));
} else if (page.id === 'first-victory-next' && rewards) {
items = rewards.unlocks.map((unlock) => this.unlockRewardItem(unlock.battleId, unlock.title));
}
return {
pageId: page.id,
source: 'campaign',
battleId: report.battleId,
rewardGold: presentation.rewardGold,
items
rewardGold: null,
items: []
};
}
@@ -2051,77 +2007,6 @@ export class StoryScene extends Phaser.Scene {
return { pageId: null, source: 'none', battleId: null, rewardGold: null, items: [] };
}
private goldRewardItem(amount: number): StoryRewardDisplayItem {
return {
id: 'gold',
category: 'gold',
eyebrow: '군자금',
label: `+${Math.max(0, amount).toLocaleString('ko-KR')}`,
tone: 'reward',
icon: { kind: 'mark', fallbackMark: '金' }
};
}
private supplyRewardItem(label: string, index: number): StoryRewardDisplayItem {
const iconKey: BattleUiIconKey = /탁주/.test(label) ? 'wine' : /상처약/.test(label) ? 'salve' : 'bean';
return {
id: `supply-${index}-${label}`,
category: 'supply',
eyebrow: '보급품',
label,
tone: 'reward',
icon: { kind: 'battle-ui', iconKey, fallbackMark: /탁주/.test(label) ? '酒' : /상처약/.test(label) ? '藥' : '糧' }
};
}
private equipmentRewardItem(label: string, index: number): StoryRewardDisplayItem {
const inventoryLabel = label.replace(/\s+(?:[x×]\s*)?\d+\s*$/u, '').trim();
const itemId = equipmentItemIdForInventoryLabel(inventoryLabel);
return {
id: `equipment-${index}-${label}`,
category: 'equipment',
eyebrow: '장비 획득',
label,
tone: 'reward',
icon: itemId
? { kind: 'item', textureKey: `item-${itemId}`, fallbackMark: '具' }
: { kind: 'battle-ui', iconKey: 'sword', fallbackMark: '具' }
};
}
private reputationRewardItem(label: string, index: number): StoryRewardDisplayItem {
return {
id: `reputation-${index}-${label}`,
category: 'reputation',
eyebrow: '명성 상승',
label,
tone: 'bond',
icon: { kind: 'mark', fallbackMark: '名' }
};
}
private recruitRewardItem(unitId: string, name: string): StoryRewardDisplayItem {
return {
id: `recruit-${unitId}`,
category: 'recruit',
eyebrow: '신규 합류',
label: `${name} 출전 가능`,
tone: 'bond',
icon: { kind: 'mark', fallbackMark: '將' }
};
}
private unlockRewardItem(battleId: string, title: string): StoryRewardDisplayItem {
return {
id: `unlock-${battleId}`,
category: 'unlock',
eyebrow: '전장 해금',
label: title,
tone: 'next',
icon: { kind: 'battle-ui', iconKey: 'success', fallbackMark: '開' }
};
}
private renderRewardIcon(icon: StoryRewardIcon, x: number, y: number, size: number, toneColor: number) {
if (icon.kind === 'item' && this.textures.exists(icon.textureKey)) {
const image = this.add.image(x, y, icon.textureKey);