feat: unify victory reward experience
This commit is contained in:
@@ -158,14 +158,17 @@ import {
|
||||
type UnitData
|
||||
} from '../data/scenario';
|
||||
import {
|
||||
acknowledgeCampaignVictoryReward,
|
||||
applyCampVisitReward,
|
||||
applyCampBondExp,
|
||||
campaignVictoryRewardPresentation,
|
||||
campaignSortiePresetIds,
|
||||
campaignReserveTrainingFocusDefinitions,
|
||||
defaultCampaignReserveTrainingFocusId,
|
||||
ensureCampaignRosterUnits,
|
||||
getCampaignState,
|
||||
getFirstBattleReport,
|
||||
getPendingCampaignVictoryRewardReport,
|
||||
markCampaignStep,
|
||||
listCampaignSaveSlots,
|
||||
campaignSaveSlotCount,
|
||||
@@ -241,9 +244,27 @@ type CampTabButtonView = {
|
||||
bg: Phaser.GameObjects.Rectangle;
|
||||
text: Phaser.GameObjects.Text;
|
||||
indicator: Phaser.GameObjects.Rectangle;
|
||||
newBadge?: Phaser.GameObjects.Text;
|
||||
hovered: boolean;
|
||||
};
|
||||
|
||||
type VictoryRewardCardId = 'gold' | 'equipment' | 'supplies' | 'reputation' | 'recruits' | 'unlocks';
|
||||
|
||||
type VictoryRewardActionId = 'equipment' | 'supplies' | 'sortie' | 'close';
|
||||
|
||||
type VictoryRewardCardView = {
|
||||
id: VictoryRewardCardId;
|
||||
label: string;
|
||||
items: string[];
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
};
|
||||
|
||||
type VictoryRewardActionView = {
|
||||
id: VictoryRewardActionId;
|
||||
label: string;
|
||||
button: Phaser.GameObjects.Rectangle;
|
||||
};
|
||||
|
||||
type CampActionButtonView = {
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
label: Phaser.GameObjects.Text;
|
||||
@@ -11250,6 +11271,10 @@ export class CampScene extends Phaser.Scene {
|
||||
private saveSlotObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private saveSlotConfirmObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private pendingSaveSlot?: number;
|
||||
private pendingVictoryRewardReport?: FirstBattleReport;
|
||||
private victoryRewardObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private victoryRewardCards: VictoryRewardCardView[] = [];
|
||||
private victoryRewardActions: VictoryRewardActionView[] = [];
|
||||
private reportFormationReviewObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private reportFormationReviewVisible = false;
|
||||
private reportFormationReviewFeedback = '';
|
||||
@@ -11359,6 +11384,10 @@ export class CampScene extends Phaser.Scene {
|
||||
this.saveSlotObjects = [];
|
||||
this.saveSlotConfirmObjects = [];
|
||||
this.pendingSaveSlot = undefined;
|
||||
this.pendingVictoryRewardReport = undefined;
|
||||
this.victoryRewardObjects = [];
|
||||
this.victoryRewardCards = [];
|
||||
this.victoryRewardActions = [];
|
||||
this.reportFormationReviewObjects = [];
|
||||
this.reportFormationReviewVisible = false;
|
||||
this.reportFormationReviewFeedback = '';
|
||||
@@ -11379,6 +11408,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.visitedTabs = new Set(['status']);
|
||||
this.campaign = getCampaignState();
|
||||
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
||||
this.pendingVictoryRewardReport = getPendingCampaignVictoryRewardReport();
|
||||
if (!this.retrySortieBattleId) {
|
||||
this.ensureCurrentCampRecruitment();
|
||||
}
|
||||
@@ -11529,12 +11559,11 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.renderStaticButtons();
|
||||
this.render();
|
||||
if (this.openSortiePrepOnCreate) {
|
||||
if (this.pendingVictoryRewardReport) {
|
||||
this.time.delayedCall(0, () => this.showVictoryRewardAcknowledgement());
|
||||
} else if (this.openSortiePrepOnCreate) {
|
||||
this.time.delayedCall(0, () => {
|
||||
if (this.openSortieImprovementOnCreate) {
|
||||
this.prepareGuidedSortieImprovement();
|
||||
}
|
||||
this.showSortiePrep();
|
||||
this.showRequestedSortiePrep();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12862,6 +12891,10 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private handleSaveSlotKey(event: KeyboardEvent) {
|
||||
if (this.victoryRewardObjects.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.saveSlotConfirmObjects.length > 0) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
@@ -12895,7 +12928,228 @@ export class CampScene extends Phaser.Scene {
|
||||
this.pendingSaveSlot = undefined;
|
||||
}
|
||||
|
||||
private showVictoryRewardAcknowledgement() {
|
||||
const report = this.pendingVictoryRewardReport ?? getPendingCampaignVictoryRewardReport();
|
||||
if (!report || report.outcome !== 'victory' || this.sortieObjects.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingVictoryRewardReport = report;
|
||||
this.hideVictoryRewardAcknowledgement();
|
||||
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];
|
||||
const depth = 80;
|
||||
const panelX = 198;
|
||||
const panelY = 90;
|
||||
const panelWidth = 884;
|
||||
const panelHeight = 546;
|
||||
|
||||
const shade = this.trackVictoryReward(
|
||||
this.add.rectangle(0, 0, campLegacyCanvasWidth, campLegacyCanvasHeight, 0x020406, 0.78)
|
||||
);
|
||||
shade.setOrigin(0);
|
||||
shade.setDepth(depth);
|
||||
shade.setInteractive();
|
||||
|
||||
const panel = this.trackVictoryReward(
|
||||
this.add.rectangle(panelX, panelY, panelWidth, panelHeight, 0x0d151e, 0.995)
|
||||
);
|
||||
panel.setOrigin(0);
|
||||
panel.setDepth(depth + 1);
|
||||
panel.setStrokeStyle(3, this.campSkinSelection?.skin.accentColor ?? palette.gold, 0.96);
|
||||
panel.setInteractive();
|
||||
|
||||
const eyebrow = this.trackVictoryReward(
|
||||
this.add.text(panelX + 34, panelY + 24, 'VICTORY REWARD', 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))
|
||||
);
|
||||
title.setDepth(depth + 2);
|
||||
|
||||
const closeHint = this.trackVictoryReward(
|
||||
this.add.text(panelX + panelWidth - 34, panelY + 28, 'Esc 닫기', this.textStyle(12, '#9fb0bf', true))
|
||||
);
|
||||
closeHint.setOrigin(1, 0);
|
||||
closeHint.setDepth(depth + 2);
|
||||
|
||||
const subtitle = this.trackVictoryReward(
|
||||
this.add.text(
|
||||
panelX + 34,
|
||||
panelY + 88,
|
||||
`${report.battleTitle} 승리 보상이 군영 자산에 반영되었습니다.`,
|
||||
this.textStyle(14, '#d4dce6', true)
|
||||
)
|
||||
);
|
||||
subtitle.setDepth(depth + 2);
|
||||
|
||||
const cardDefinitions: Array<{ id: VictoryRewardCardId; label: string; items: string[] }> = [
|
||||
{
|
||||
id: 'gold',
|
||||
label: '군자금',
|
||||
items: [
|
||||
`+${presentation.rewardGold} 군자금`,
|
||||
...(presentation.sortieOrderBonus
|
||||
? [`${presentation.sortieOrderBonus.label} 첫 공훈 +${presentation.sortieOrderBonus.rewardGold} 포함`]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
{ id: 'equipment', label: '장비', items: [...(rewards?.equipment ?? [])] },
|
||||
{ id: 'supplies', label: '보급', items: supplyItems },
|
||||
{ id: 'reputation', label: '명성·기록', items: [...(rewards?.reputation ?? [])] },
|
||||
{ id: 'recruits', label: '합류 무장', items: rewards?.recruits.map((recruit) => recruit.name) ?? [] },
|
||||
{ id: 'unlocks', label: '새 전장', items: rewards?.unlocks.map((unlock) => unlock.title) ?? [] }
|
||||
];
|
||||
const cardWidth = 252;
|
||||
const cardHeight = 104;
|
||||
cardDefinitions.forEach((definition, index) => {
|
||||
const column = index % 3;
|
||||
const row = Math.floor(index / 3);
|
||||
const cardX = panelX + 34 + column * 268;
|
||||
const cardY = panelY + 128 + row * 120;
|
||||
const background = this.trackVictoryReward(
|
||||
this.add.rectangle(cardX, cardY, cardWidth, cardHeight, definition.id === 'gold' ? 0x2d2619 : 0x141f2a, 0.98)
|
||||
);
|
||||
background.setOrigin(0);
|
||||
background.setDepth(depth + 2);
|
||||
background.setStrokeStyle(1, definition.id === 'gold' ? palette.gold : palette.blue, definition.items.length > 0 ? 0.78 : 0.38);
|
||||
|
||||
const label = this.trackVictoryReward(
|
||||
this.add.text(cardX + 16, cardY + 13, definition.label, this.textStyle(13, definition.id === 'gold' ? '#ffdf7b' : '#9fc8e8', true))
|
||||
);
|
||||
label.setDepth(depth + 3);
|
||||
|
||||
const itemText = definition.items.length > 0 ? definition.items.join(' · ') : '변동 없음';
|
||||
const value = this.trackVictoryReward(
|
||||
this.add.text(cardX + 16, cardY + 40, itemText, {
|
||||
...this.textStyle(definition.id === 'gold' ? 17 : 12, definition.items.length > 0 ? '#f2e3bf' : '#77818c', true),
|
||||
wordWrap: { width: cardWidth - 32, useAdvancedWrap: true },
|
||||
lineSpacing: 4
|
||||
})
|
||||
);
|
||||
value.setDepth(depth + 3);
|
||||
this.victoryRewardCards.push({ ...definition, background });
|
||||
});
|
||||
|
||||
const note = rewards?.note?.trim();
|
||||
const noteText = this.trackVictoryReward(
|
||||
this.add.text(
|
||||
panelX + 34,
|
||||
panelY + 378,
|
||||
note ? `전황 기록 · ${note}` : '보상 확인 후 필요한 정비 화면으로 바로 이동할 수 있습니다.',
|
||||
{
|
||||
...this.textStyle(12, note ? '#ffdf7b' : '#9fb0bf', Boolean(note)),
|
||||
wordWrap: { width: panelWidth - 68, useAdvancedWrap: true }
|
||||
}
|
||||
)
|
||||
);
|
||||
noteText.setDepth(depth + 2);
|
||||
|
||||
const actionDefinitions: Array<{ id: VictoryRewardActionId; label: string; primary?: boolean }> = [
|
||||
{ id: 'equipment', label: '장비 확인' },
|
||||
{ id: 'supplies', label: '보급 확인' },
|
||||
{ id: 'sortie', label: '출진 준비', primary: true },
|
||||
{ id: 'close', label: '닫기' }
|
||||
];
|
||||
const buttonWidth = 158;
|
||||
const buttonHeight = 48;
|
||||
const actionGap = 14;
|
||||
const totalActionWidth = actionDefinitions.length * buttonWidth + (actionDefinitions.length - 1) * actionGap;
|
||||
const actionStartX = panelX + Math.floor((panelWidth - totalActionWidth) / 2);
|
||||
actionDefinitions.forEach((definition, index) => {
|
||||
const buttonX = actionStartX + index * (buttonWidth + actionGap);
|
||||
const buttonY = panelY + panelHeight - 76;
|
||||
const restingFill = definition.primary ? 0x654c25 : 0x1a2834;
|
||||
const hoverFill = definition.primary ? 0x7a5b2b : 0x294052;
|
||||
const button = this.trackVictoryReward(
|
||||
this.add.rectangle(buttonX, buttonY, buttonWidth, buttonHeight, restingFill, 0.99)
|
||||
);
|
||||
button.setOrigin(0);
|
||||
button.setDepth(depth + 2);
|
||||
button.setStrokeStyle(definition.primary ? 2 : 1, palette.gold, definition.primary ? 0.96 : 0.68);
|
||||
button.setInteractive({ useHandCursor: true });
|
||||
|
||||
const label = this.trackVictoryReward(
|
||||
this.add.text(buttonX + buttonWidth / 2, buttonY + buttonHeight / 2, definition.label, this.textStyle(14, definition.primary ? '#fff2b8' : '#f2e3bf', true))
|
||||
);
|
||||
label.setOrigin(0.5);
|
||||
label.setDepth(depth + 3);
|
||||
label.setInteractive({ useHandCursor: true });
|
||||
|
||||
const run = () => this.completeVictoryRewardAcknowledgement(definition.id);
|
||||
[button, label].forEach((target) => {
|
||||
target.on('pointerover', () => button.setFillStyle(hoverFill, 0.99));
|
||||
target.on('pointerout', () => button.setFillStyle(restingFill, 0.99));
|
||||
target.on('pointerdown', run);
|
||||
});
|
||||
this.victoryRewardActions.push({ id: definition.id, label: definition.label, button });
|
||||
});
|
||||
}
|
||||
|
||||
private completeVictoryRewardAcknowledgement(action: VictoryRewardActionId) {
|
||||
const report = this.pendingVictoryRewardReport;
|
||||
if (report) {
|
||||
this.campaign = acknowledgeCampaignVictoryReward(report.battleId);
|
||||
if (this.campaign.acknowledgedVictoryRewardBattleIds.includes(report.battleId)) {
|
||||
this.pendingVictoryRewardReport = undefined;
|
||||
}
|
||||
}
|
||||
soundDirector.playSelect();
|
||||
this.hideVictoryRewardAcknowledgement();
|
||||
this.updateTabButtons();
|
||||
|
||||
if (action === 'equipment') {
|
||||
this.activeTab = 'equipment';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
if (action === 'supplies') {
|
||||
this.activeTab = 'supplies';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
if (action === 'sortie' || (action === 'close' && this.openSortiePrepOnCreate)) {
|
||||
this.showRequestedSortiePrep();
|
||||
}
|
||||
}
|
||||
|
||||
private showRequestedSortiePrep() {
|
||||
if (this.openSortieImprovementOnCreate) {
|
||||
this.prepareGuidedSortieImprovement();
|
||||
}
|
||||
this.openSortiePrepOnCreate = false;
|
||||
this.openSortieImprovementOnCreate = false;
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private hideVictoryRewardAcknowledgement() {
|
||||
this.victoryRewardObjects.forEach((object) => object.destroy());
|
||||
this.victoryRewardObjects = [];
|
||||
this.victoryRewardCards = [];
|
||||
this.victoryRewardActions = [];
|
||||
}
|
||||
|
||||
private handleEscapeKey() {
|
||||
if (this.victoryRewardObjects.length > 0) {
|
||||
this.completeVictoryRewardAcknowledgement('close');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.saveSlotConfirmObjects.length > 0) {
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveConfirm();
|
||||
@@ -12936,6 +13190,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private handleSortieEnterKey(event: KeyboardEvent) {
|
||||
if (
|
||||
this.sortieObjects.length === 0 ||
|
||||
this.victoryRewardObjects.length > 0 ||
|
||||
this.saveSlotObjects.length > 0 ||
|
||||
this.saveSlotConfirmObjects.length > 0 ||
|
||||
!this.usesStagedSortiePrep()
|
||||
@@ -12977,7 +13232,14 @@ export class CampScene extends Phaser.Scene {
|
||||
}));
|
||||
text.setOrigin(0.5);
|
||||
text.setInteractive({ useHandCursor: true });
|
||||
const view: CampTabButtonView = { tab, bg, text, indicator, hovered: false };
|
||||
const newBadge = tab === 'supplies' || tab === 'equipment'
|
||||
? this.scaleLegacyCampUi(this.add.text(x + 29, y - 17, 'NEW', {
|
||||
...this.textStyle(9, '#fff2b8', true),
|
||||
backgroundColor: '#8a3f25',
|
||||
padding: { x: 4, y: 2 }
|
||||
})).setOrigin(0.5).setDepth(6)
|
||||
: undefined;
|
||||
const view: CampTabButtonView = { tab, bg, text, indicator, newBadge, hovered: false };
|
||||
const setHovered = (hovered: boolean) => {
|
||||
view.hovered = hovered;
|
||||
this.updateTabButtons();
|
||||
@@ -12993,7 +13255,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private updateTabButtons() {
|
||||
const accentColor = this.campSkinSelection?.skin.accentColor ?? palette.gold;
|
||||
this.tabButtons.forEach(({ tab, bg, text, indicator, hovered }) => {
|
||||
this.tabButtons.forEach(({ tab, bg, text, indicator, newBadge, hovered }) => {
|
||||
const active = this.activeTab === tab;
|
||||
bg.setFillStyle(active ? 0x2b4052 : hovered ? 0x243747 : 0x182431, active || hovered ? 0.98 : 0.94);
|
||||
bg.setStrokeStyle(active || hovered ? 2 : 1, active || hovered ? accentColor : palette.blue, active ? 0.96 : hovered ? 0.82 : 0.62);
|
||||
@@ -13001,6 +13263,9 @@ export class CampScene extends Phaser.Scene {
|
||||
indicator.setFillStyle(accentColor, 1);
|
||||
indicator.setVisible(active || hovered);
|
||||
indicator.setAlpha(active ? 1 : 0.66);
|
||||
newBadge?.setVisible(Boolean(
|
||||
this.pendingVictoryRewardReport && (tab === 'supplies' || tab === 'equipment')
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22673,6 +22938,12 @@ export class CampScene extends Phaser.Scene {
|
||||
return object;
|
||||
}
|
||||
|
||||
private trackVictoryReward<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||
this.scaleLegacyCampUi(object);
|
||||
this.victoryRewardObjects.push(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
private track<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||
this.scaleLegacyCampUi(object);
|
||||
this.contentObjects.push(object);
|
||||
@@ -22820,6 +23091,22 @@ export class CampScene extends Phaser.Scene {
|
||||
const equipmentInventoryPageCount = Math.max(1, Math.ceil(equipmentInventory.length / equipmentInventoryPageSize));
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
victoryRewardAcknowledgement: {
|
||||
visible: this.victoryRewardObjects.length > 0,
|
||||
battleId: this.pendingVictoryRewardReport?.battleId ?? null,
|
||||
cards: this.victoryRewardCards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
items: [...card.items],
|
||||
bounds: this.sortieObjectBoundsDebug(card.background)
|
||||
})),
|
||||
actions: this.victoryRewardActions.map((action) => ({
|
||||
id: action.id,
|
||||
label: action.label,
|
||||
bounds: this.sortieObjectBoundsDebug(action.button),
|
||||
interactive: Boolean(action.button.input?.enabled)
|
||||
}))
|
||||
},
|
||||
activeTab: this.activeTab,
|
||||
campTabs: this.tabButtons.map((button) => ({
|
||||
id: button.tab,
|
||||
@@ -22829,6 +23116,7 @@ export class CampScene extends Phaser.Scene {
|
||||
bounds: this.sortieObjectBoundsDebug(button.bg),
|
||||
interactive: Boolean(button.bg.input?.enabled && button.text.input?.enabled),
|
||||
indicatorVisible: button.indicator.visible,
|
||||
newBadgeVisible: Boolean(button.newBadge?.visible),
|
||||
fillColor: button.bg.fillColor,
|
||||
strokeColor: button.bg.strokeColor,
|
||||
lineWidth: button.bg.lineWidth
|
||||
|
||||
Reference in New Issue
Block a user