feat: guide early stays and remember player choices

This commit is contained in:
2026-07-27 19:41:10 +09:00
parent eec4896002
commit c22f77b4e1
11 changed files with 2121 additions and 93 deletions

View File

@@ -344,7 +344,7 @@ type CampTabButtonView = {
type VictoryRewardCardId = CampaignVictoryRewardCategoryId;
type VictoryRewardActionId = 'equipment' | 'supplies' | 'sortie' | 'close';
type VictoryRewardActionId = 'equipment' | 'supplies' | 'guided' | 'sortie' | 'close';
type VictoryRewardIcon =
| { kind: 'mark'; mark: string }
@@ -368,6 +368,7 @@ type VictoryRewardActionView = {
id: VictoryRewardActionId;
label: string;
button: Phaser.GameObjects.Rectangle;
primary: boolean;
};
type CampActionButtonView = {
@@ -377,6 +378,23 @@ type CampActionButtonView = {
hovered: boolean;
};
type FirstCampGuidedAction =
| {
kind: 'visit';
label: '북문 정찰막 둘러보기';
targetId: typeof firstPursuitScoutVisitId;
}
| {
kind: 'dialogue';
label: string;
targetId: string;
}
| {
kind: 'sortie';
label: '출진 준비';
targetId: null;
};
type EquipmentSwapRequest = {
unitId: string;
slot: EquipmentSlot;
@@ -11517,6 +11535,9 @@ export class CampScene extends Phaser.Scene {
private reportFormationHistoryUndoState?: ReportFormationHistoryUndoState;
private tabButtons: CampTabButtonView[] = [];
private sortieCommandButton?: Phaser.GameObjects.Rectangle;
private sortieCommandLabel?: Phaser.GameObjects.Text;
private sortieBypassButton?: Phaser.GameObjects.Rectangle;
private sortieBypassLabel?: Phaser.GameObjects.Text;
private sortiePrimaryActionButton?: CampActionButtonView;
private sortieNextActionGuideBackground?: Phaser.GameObjects.Rectangle;
private sortieNextActionGuideText?: Phaser.GameObjects.Text;
@@ -11672,6 +11693,10 @@ export class CampScene extends Phaser.Scene {
this.reportFormationHistoryView = undefined;
this.reportFormationHistoryUndoState = undefined;
this.tabButtons = [];
this.sortieCommandButton = undefined;
this.sortieCommandLabel = undefined;
this.sortieBypassButton = undefined;
this.sortieBypassLabel = undefined;
this.sortiePrimaryActionButton = undefined;
this.sortieNextActionGuideBackground = undefined;
this.sortieNextActionGuideText = undefined;
@@ -13025,6 +13050,85 @@ export class CampScene extends Phaser.Scene {
);
}
private firstCampGuidedAction(): FirstCampGuidedAction | undefined {
if (
!this.isFirstSortiePrepSlice() ||
this.currentCampBattleId() !== campBattleIds.first
) {
return undefined;
}
if (!this.completedCampVisits().includes(firstPursuitScoutVisitId)) {
return {
kind: 'visit',
label: '북문 정찰막 둘러보기',
targetId: firstPursuitScoutVisitId
};
}
const followup = this.firstBattleCampFollowup();
if (
followup &&
!this.completedCampDialogues().includes(followup.targetDialogueId)
) {
return {
kind: 'dialogue',
label: `${followup.mentorName}와 핵심 회고`,
targetId: followup.targetDialogueId
};
}
return {
kind: 'sortie',
label: '출진 준비',
targetId: null
};
}
private runFirstCampGuidedAction() {
const action = this.firstCampGuidedAction();
if (!action || action.kind === 'sortie') {
this.showRequestedSortiePrep();
return;
}
if (action.kind === 'visit') {
this.activeTab = 'visit';
this.selectedVisitId = action.targetId;
this.render();
this.startCampNavigation('CampVisitExplorationScene', { visitId: action.targetId });
return;
}
this.activeTab = 'dialogue';
this.selectedDialogueId = action.targetId;
this.render();
}
private campPrimaryActionLabel(flow = this.currentSortieFlow()) {
if (this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep) {
return '엔딩 보기';
}
const guidedAction = this.firstCampGuidedAction();
return guidedAction?.kind === 'visit'
? '정찰막 둘러보기'
: guidedAction?.label ?? '출진 준비';
}
private refreshCampPrimaryActionPresentation() {
const label = this.campPrimaryActionLabel();
const guidedAction = this.firstCampGuidedAction();
const bypassVisible = Boolean(guidedAction && guidedAction.kind !== 'sortie');
this.sortieCommandLabel
?.setText(label)
.setFontSize(
bypassVisible
? Array.from(label).length > 7 ? 12 : 14
: Array.from(label).length > 8 ? 13 : 16
);
[this.sortieBypassButton, this.sortieBypassLabel].forEach((object) => {
object?.setVisible(bypassVisible);
if (object?.input) {
object.input.enabled = bypassVisible;
}
});
}
private hasPendingFirstBattleCamaraderieMemory() {
const dialogue = this.firstBattleCamaraderieDialogue();
return Boolean(
@@ -13102,16 +13206,36 @@ export class CampScene extends Phaser.Scene {
soundDirector.playSelect();
this.showCampSaveSlotPanel();
});
const flow = this.currentSortieFlow();
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '출진 준비';
this.sortieCommandButton = this.addCommandButton(storyButtonLabel, 1152, 38, 142, () => {
const guidedAction = this.firstCampGuidedAction();
const splitSortieCommand = Boolean(guidedAction && guidedAction.kind !== 'sortie');
const sortieCommand = this.addCommandButton(
this.campPrimaryActionLabel(),
splitSortieCommand ? 1132 : 1152,
38,
splitSortieCommand ? 116 : 142,
() => {
soundDirector.playSelect();
const flow = this.currentSortieFlow();
if (this.isFinalEpilogueFlow(flow)) {
this.startVictoryStory();
return;
}
this.showSortiePrep();
}, true);
this.runFirstCampGuidedAction();
},
true
);
this.sortieCommandButton = sortieCommand.background;
this.sortieCommandLabel = sortieCommand.label;
if (splitSortieCommand) {
const sortieBypass = this.addCommandButton('바로 출진', 1230, 38, 72, () => {
soundDirector.playSelect();
this.showRequestedSortiePrep();
});
sortieBypass.label.setFontSize(13);
this.sortieBypassButton = sortieBypass.background;
this.sortieBypassLabel = sortieBypass.label;
}
this.refreshCampPrimaryActionPresentation();
}
private showCampSaveSlotPanel() {
@@ -13472,6 +13596,9 @@ 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)
@@ -13683,15 +13810,24 @@ export class CampScene extends Phaser.Scene {
);
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 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 buttonHeight = 48;
const actionGap = 14;
const actionGap = firstCampGuidedAction ? 8 : 14;
const totalActionWidth = actionDefinitions.length * buttonWidth + (actionDefinitions.length - 1) * actionGap;
const actionStartX = panelX + Math.floor((panelWidth - totalActionWidth) / 2);
actionDefinitions.forEach((definition, index) => {
@@ -13708,7 +13844,16 @@ export class CampScene extends Phaser.Scene {
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))
this.add.text(
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
)
)
);
label.setOrigin(0.5);
label.setDepth(depth + 3);
@@ -13723,7 +13868,12 @@ export class CampScene extends Phaser.Scene {
target.on('pointerout', () => button.setFillStyle(restingFill, 0.99));
target.on('pointerdown', run);
});
this.victoryRewardActions.push({ id: definition.id, label: definition.label, button });
this.victoryRewardActions.push({
id: definition.id,
label: definition.label,
button,
primary: Boolean(definition.primary)
});
});
soundDirector.playRewardRevealCue();
}
@@ -13759,6 +13909,10 @@ export class CampScene extends Phaser.Scene {
this.render();
return;
}
if (action === 'guided') {
this.runFirstCampGuidedAction();
return;
}
if (action === 'sortie' || (action === 'close' && this.openSortiePrepOnCreate)) {
this.showRequestedSortiePrep();
}
@@ -14110,7 +14264,7 @@ export class CampScene extends Phaser.Scene {
target.on('pointerout', () => setHovered(false));
target.on('pointerdown', action);
});
return bg;
return { background: bg, label: text };
}
private render() {
@@ -14124,6 +14278,7 @@ export class CampScene extends Phaser.Scene {
this.acknowledgePendingVictoryRewardCategories(['supplies']);
}
this.visitedTabs.add(this.activeTab);
this.refreshCampPrimaryActionPresentation();
this.updateTabButtons();
this.renderUnitColumn();
this.renderReportPanel();
@@ -26042,6 +26197,7 @@ export class CampScene extends Phaser.Scene {
: false;
const cityEquipmentSortieStatus = this.cityEquipmentSortieStatus();
const firstBattleCampFollowup = this.firstBattleCampFollowup();
const firstCampGuidedAction = this.firstCampGuidedAction();
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
@@ -26090,6 +26246,7 @@ export class CampScene extends Phaser.Scene {
actions: this.victoryRewardActions.map((action) => ({
id: action.id,
label: action.label,
primary: action.primary,
bounds: this.sortieObjectBoundsDebug(action.button),
interactive: Boolean(action.button.input?.enabled)
}))
@@ -26148,8 +26305,28 @@ export class CampScene extends Phaser.Scene {
lineWidth: button.bg.lineWidth
})),
sortieCommand: {
label: this.sortieCommandLabel?.text ?? null,
guidedKind: firstCampGuidedAction?.kind ?? null,
targetId: firstCampGuidedAction?.targetId ?? null,
bounds: this.sortieObjectBoundsDebug(this.sortieCommandButton),
interactive: Boolean(this.sortieCommandButton?.input?.enabled)
interactive: Boolean(
this.sortieCommandButton?.input?.enabled &&
this.sortieCommandLabel?.input?.enabled
),
bypass:
this.sortieBypassButton?.visible &&
this.sortieBypassLabel?.visible
? {
label: this.sortieBypassLabel.text,
bounds: this.sortieObjectBoundsDebug(
this.sortieBypassButton
),
interactive: Boolean(
this.sortieBypassButton.input?.enabled &&
this.sortieBypassLabel.input?.enabled
)
}
: null
},
firstVictoryFollowup: firstBattleCampFollowup
? {