feat: streamline victory debrief flow
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user