feat: add second-victory relief exploration

This commit is contained in:
2026-07-27 10:53:20 +09:00
parent e9fcd27611
commit f07a45c62b
24 changed files with 4583 additions and 282 deletions

View File

@@ -39,6 +39,11 @@ import {
resolveFirstPursuitScoutMemory
} from '../data/firstPursuitScoutMemory';
import { firstPursuitScoutVisitDefinition } from '../data/firstPursuitScoutVisit';
import {
secondBattleReliefExplorationDefinition,
secondBattleReliefSourceBattleId,
secondBattleReliefVisitId
} from '../data/secondBattleReliefExploration';
import {
findCityStayAfterBattle,
type CityEquipmentOffer,
@@ -231,6 +236,7 @@ import {
collectCityStayInformation,
purchaseCityStayEquipment
} from '../state/cityStayActions';
import { secondBattleReliefProgress } from '../state/secondBattleReliefActions';
import { clearCampaignBattleSavesForSlot } from '../state/battleSaveKeys';
import { releaseTextureSource } from '../render/loaderLifecycle';
import { palette } from '../ui/palette';
@@ -300,6 +306,11 @@ type CampVisitChoice = {
itemRewards?: string[];
};
const directExplorationVisitIds = new Set([
firstPursuitScoutVisitId,
secondBattleReliefVisitId
]);
type CampTabButtonView = {
tab: CampTab;
bg: Phaser.GameObjects.Rectangle;
@@ -8410,6 +8421,26 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: [...choice.itemRewards]
}))
},
{
id: secondBattleReliefExplorationDefinition.id,
title: secondBattleReliefExplorationDefinition.title,
location: secondBattleReliefExplorationDefinition.location,
availableAfterBattleIds: [secondBattleReliefSourceBattleId],
availableDuringSteps: ['second-camp'],
description: secondBattleReliefExplorationDefinition.description,
lines: secondBattleReliefExplorationDefinition.introLines.map(
(line) => `${line.speaker}: ${line.text}`
),
choices: secondBattleReliefExplorationDefinition.choices.map(
(choice) => ({
id: choice.id,
label: choice.label,
response: choice.response,
bondExp: choice.bondExp,
itemRewards: [...choice.itemRewards]
})
)
},
{
id: 'jingzhou-scholar-rumors',
title: '형주 선비들의 모임',
@@ -11389,6 +11420,7 @@ export class CampScene extends Phaser.Scene {
private campDialogueBodyText?: Phaser.GameObjects.Text;
private selectedVisitId = campVisits[0].id;
private firstPursuitExplorationButton?: Phaser.GameObjects.Rectangle;
private secondBattleReliefExplorationButton?: Phaser.GameObjects.Rectangle;
private equipmentInventoryPage = 0;
private equipmentPanelBackground?: Phaser.GameObjects.Rectangle;
private equipmentInventoryPreviousButton?: Phaser.GameObjects.Rectangle;
@@ -11514,6 +11546,7 @@ export class CampScene extends Phaser.Scene {
this.campDialogueChoiceButtons = {};
this.campDialogueBodyText = undefined;
this.firstPursuitExplorationButton = undefined;
this.secondBattleReliefExplorationButton = undefined;
this.dialogueObjects = [];
this.sortieObjects = [];
this.sortieComparisonPanelView = undefined;
@@ -11625,12 +11658,13 @@ export class CampScene extends Phaser.Scene {
} else if (this.activeTab === 'city') {
this.activeTab = 'status';
}
const pendingFirstPursuitScoutVisit = this.availableCampVisits().some(
const pendingDirectExplorationVisit = this.availableCampVisits().find(
(visit) =>
visit.id === firstPursuitScoutVisitId &&
directExplorationVisitIds.has(visit.id) &&
!this.completedCampVisits().includes(visit.id)
);
if (!cityStay && pendingFirstPursuitScoutVisit && this.activeTab === 'status') {
if (!cityStay && pendingDirectExplorationVisit) {
this.selectedVisitId = pendingDirectExplorationVisit.id;
this.activeTab = 'visit';
}
soundDirector.playSoundscape({
@@ -23361,20 +23395,32 @@ export class CampScene extends Phaser.Scene {
return;
}
if (visit.id === firstPursuitScoutVisitId) {
if (directExplorationVisitIds.has(visit.id)) {
const secondRelief = visit.id === secondBattleReliefVisitId;
const reliefProgress = secondRelief && this.campaign
? secondBattleReliefProgress(this.campaign)
: undefined;
const guide = this.track(this.add.rectangle(x + 18, y + height - 102, width - 36, 84, 0x151f2a, 0.96));
guide.setOrigin(0);
guide.setStrokeStyle(1, palette.blue, 0.62);
this.track(this.add.text(
x + 32,
y + height - 90,
'유비를 움직여 간옹에게 직접 말을 거세요.',
secondRelief
? reliefProgress?.choiceReady
? '현장 확인 완료 · 간옹과 광종 진군 방침을 정하세요.'
: reliefProgress && reliefProgress.completedObjectiveIds.length > 0
? `유비를 움직여 현장 수습을 이어가세요 · ${Math.min(3, reliefProgress.completedObjectiveIds.length)}/3 기록됨`
: '유비를 움직여 북쪽 마을과 나루를 직접 살피세요.'
: '유비를 움직여 간옹에게 직접 말을 거세요.',
this.textStyle(13, '#d4dce6', true)
));
this.track(this.add.text(
x + 32,
y + height - 68,
'WASD·방향키 이동 · E/Space 대화',
secondRelief
? '관우 → 사공 → 장비 → 간옹 · 진행 자동 저장'
: 'WASD·방향키 이동 · E/Space 대화',
this.textStyle(11, '#9fb0bf')
));
@@ -23389,12 +23435,22 @@ export class CampScene extends Phaser.Scene {
button.on('pointerover', () => button.setFillStyle(0x654c25, 1));
button.on('pointerout', () => button.setFillStyle(0x4a371d, 0.98));
button.on('pointerdown', openExploration);
this.firstPursuitExplorationButton = button;
if (secondRelief) {
this.secondBattleReliefExplorationButton = button;
} else {
this.firstPursuitExplorationButton = button;
}
const label = this.track(this.add.text(
x + width / 2,
buttonY,
'정찰막으로 이동',
secondRelief
? reliefProgress?.choiceReady
? '광종 진군 방침 결정'
: reliefProgress && reliefProgress.completedObjectiveIds.length > 0
? '현장 수습 이어가기'
: '북쪽 마을·나루로 이동'
: '정찰막으로 이동',
this.textStyle(14, '#fff2b8', true)
));
label.setOrigin(0.5);
@@ -24942,6 +24998,7 @@ export class CampScene extends Phaser.Scene {
this.campDialogueChoiceButtons = {};
this.campDialogueBodyText = undefined;
this.firstPursuitExplorationButton = undefined;
this.secondBattleReliefExplorationButton = undefined;
this.campRosterLayout = undefined;
this.cityPanelBackground = undefined;
this.cityExploreButton = undefined;
@@ -25102,6 +25159,14 @@ export class CampScene extends Phaser.Scene {
(visit) => visit.id === firstPursuitScoutVisitId
);
const firstPursuitScoutMemory = this.firstPursuitScoutMemoryForNextSortie();
const secondBattleReliefVisit = this.availableCampVisits().find(
(visit) => visit.id === secondBattleReliefVisitId
);
const secondReliefProgress = this.campaign
? secondBattleReliefProgress(this.campaign)
: undefined;
const secondReliefChoiceId =
this.campaign?.campVisitChoiceIds[secondBattleReliefVisitId] ?? null;
const availableCampDialogues = this.availableCampDialogues();
const selectedCampDialogue = availableCampDialogues.find(
(dialogue) => dialogue.id === this.selectedDialogueId
@@ -25289,6 +25354,39 @@ export class CampScene extends Phaser.Scene {
openingLines: firstPursuitScoutMemory ? [...firstPursuitScoutMemory.openingLines] : [],
briefingSummaryBounds: this.sortieTextBoundsDebug(this.sortieBriefingScoutSummaryText)
},
secondBattleReliefExploration: {
visitId: secondBattleReliefVisitId,
sourceBattleId: secondBattleReliefSourceBattleId,
available: Boolean(secondBattleReliefVisit),
selected: this.selectedVisitId === secondBattleReliefVisitId,
completed: Boolean(secondReliefProgress?.completed),
mode: secondReliefProgress?.completed
? 'completed'
: secondBattleReliefVisit
? secondReliefProgress &&
secondReliefProgress.completedObjectiveIds.length > 0
? 'resume-walkable-relief'
: 'walkable-relief'
: 'unavailable',
explorationButtonBounds: this.sortieInteractiveObjectBoundsDebug(
this.secondBattleReliefExplorationButton
),
explorationInteractive: Boolean(
this.secondBattleReliefExplorationButton?.input?.enabled
),
completedObjectiveIds:
secondReliefProgress?.completedObjectiveIds ?? [],
nextObjectiveId: secondReliefProgress?.nextObjectiveId ?? null,
choiceReady: secondReliefProgress?.choiceReady ?? false,
choiceId: secondReliefChoiceId,
choices:
secondBattleReliefVisit?.choices.map((choice) => ({
id: choice.id,
label: choice.label,
response: choice.response,
rewardText: this.visitRewardText(choice)
})) ?? []
},
selectedDialogue: selectedCampDialogue
? {
id: selectedCampDialogue.id,