feat: carry camp scouting into battle deployment

This commit is contained in:
2026-07-27 07:52:47 +09:00
parent 17486efa8b
commit 89c755ca42
7 changed files with 954 additions and 11 deletions

View File

@@ -34,6 +34,10 @@ import {
resolveFirstBattleCamaraderieMemory,
type FirstBattleCamaraderieMemory
} from '../data/firstBattleCamaraderieMemory';
import {
firstPursuitScoutVisitId,
resolveFirstPursuitScoutMemory
} from '../data/firstPursuitScoutMemory';
import {
findCityStayAfterBattle,
type CityEquipmentOffer,
@@ -8396,6 +8400,35 @@ const campDialogues: CampDialogue[] = [
];
const campVisits: CampVisitDefinition[] = [
{
id: 'first-pursuit-scout-tent',
title: '북문 추격로 정찰',
location: '탁현 북문 정찰막',
availableAfterBattleIds: [campBattleIds.first],
bondId: 'liu-bei__jian-yong',
description: '간옹과 황건 잔당의 두 갈래 길을 살피고 정찰 기록을 출진 준비에 올립니다.',
lines: [
'간옹: 발자국이 강가와 마을길로 갈렸습니다.',
'유비: 매복도 찾고 백성의 길도 지켜야 하오.',
'정찰 지도에 두 길을 나란히 표시했습니다.'
],
choices: [
{
id: 'trace-river-ambush',
label: '강가 매복 흔적을 쫓는다',
response: '간옹: 강변의 꺾인 갈대와 버려진 횃불을 표시했습니다. 상처약도 챙겨 이 기록을 출진 준비에 올리겠습니다.',
bondExp: 10,
itemRewards: ['상처약 1']
},
{
id: 'mark-village-relief',
label: '마을 구호 길을 표시한다',
response: '간옹: 피난민이 돌아올 샛길과 군량을 나눌 곳을 적었습니다. 콩 한 자루와 이 기록을 다음 전투 준비에 보태겠습니다.',
bondExp: 10,
itemRewards: ['콩 1']
}
]
},
{
id: 'jingzhou-scholar-rumors',
title: '형주 선비들의 모임',
@@ -11419,6 +11452,7 @@ export class CampScene extends Phaser.Scene {
private sortiePrimaryActionButton?: CampActionButtonView;
private sortieNextActionGuideBackground?: Phaser.GameObjects.Rectangle;
private sortieNextActionGuideText?: Phaser.GameObjects.Text;
private sortieBriefingScoutSummaryText?: Phaser.GameObjects.Text;
private firstBattleFollowupCtaButton?: Phaser.GameObjects.Rectangle;
private firstBattleFollowupCard?: Phaser.GameObjects.Rectangle;
private firstBattleFollowupSummaryText?: Phaser.GameObjects.Text;
@@ -12824,6 +12858,14 @@ export class CampScene extends Phaser.Scene {
return resolveFirstBattleCamaraderieMemory(this.campaign);
}
private firstPursuitScoutMemoryForNextSortie() {
const campaign = this.campaign;
const scenario = this.nextSortieScenario();
return campaign && scenario
? resolveFirstPursuitScoutMemory({ campaign, battleId: scenario.id })
: undefined;
}
private firstBattleCamaraderieDialogue(): CampDialogue | undefined {
if (!this.isFirstSortiePrepSlice()) {
return undefined;
@@ -12926,7 +12968,7 @@ export class CampScene extends Phaser.Scene {
this.showCampSaveSlotPanel();
});
const flow = this.currentSortieFlow();
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '다음 이야기';
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '출진 준비';
this.addCommandButton(storyButtonLabel, 1152, 38, 142, () => {
soundDirector.playSelect();
if (this.isFinalEpilogueFlow(flow)) {
@@ -16248,13 +16290,25 @@ export class CampScene extends Phaser.Scene {
const objectiveLabel = hasBattle ? '목표' : '진행';
const terrainLabel = hasBattle ? '지형' : '장소';
const recommendationLabel = hasBattle ? '추천' : '동행';
const scoutMemory = this.firstPursuitScoutMemoryForNextSortie();
this.trackSortie(this.add.text(x + 18, y + 12, briefing.eyebrow, this.textStyle(15, '#9fb0bf', true))).setDepth(depth + 1);
this.trackSortie(this.add.text(x + 18, y + 34, briefing.title, this.textStyle(22, '#f2e3bf', true))).setDepth(depth + 1);
if (scoutMemory) {
this.sortieBriefingScoutSummaryText = this.trackSortie(
this.add.text(
x + 18,
y + 64,
`${scoutMemory.campSummaryLine} · 배치 단계 적 의도 선공개`,
this.textStyle(12, '#a8ffd0', true)
)
);
this.sortieBriefingScoutSummaryText.setDepth(depth + 1);
}
this.trackSortie(
this.add.text(x + 18, y + 65, briefing.description, {
...this.textStyle(14, '#d4dce6'),
this.add.text(x + 18, y + (scoutMemory ? 84 : 65), briefing.description, {
...this.textStyle(scoutMemory ? 12 : 14, '#d4dce6'),
wordWrap: { width: width - 36, useAdvancedWrap: true },
lineSpacing: 3
lineSpacing: scoutMemory ? 1 : 3
})
).setDepth(depth + 1);
const chipY = y + height - 46;
@@ -20416,6 +20470,7 @@ export class CampScene extends Phaser.Scene {
this.sortiePrimaryActionButton = undefined;
this.sortieNextActionGuideBackground = undefined;
this.sortieNextActionGuideText = undefined;
this.sortieBriefingScoutSummaryText = undefined;
this.firstBattleFollowupCtaButton = undefined;
this.firstBattleFollowupCard = undefined;
this.firstBattleFollowupSummaryText = undefined;
@@ -23225,7 +23280,7 @@ export class CampScene extends Phaser.Scene {
bg.setStrokeStyle(1, palette.gold, 0.58);
const locationLabel = this.currentCityStay()?.city.name ?? this.campSkinSelection?.skin.locationLabel ?? '현지';
this.track(this.add.text(x + 24, y + 22, `${locationLabel} 방문`, this.textStyle(24, '#f2e3bf', true)));
this.track(this.add.text(x + 24, y + 56, '군영 밖 사람들을 만나 정보, 보급, 공명 보상을 얻습니다. 방문은 한 번만 완료할 수 있습니다.', this.textStyle(14, '#d4dce6')));
this.track(this.add.text(x + 24, y + 56, '획득한 정보·보급·공명은 출진 준비와 다음 전투로 이어집니다. 방문은 한 번만 완료니다.', this.textStyle(14, '#d4dce6')));
const visits = this.availableCampVisits();
if (!visits.some((visit) => visit.id === this.selectedVisitId)) {
@@ -23334,7 +23389,7 @@ export class CampScene extends Phaser.Scene {
const bg = this.track(this.add.rectangle(x, y, width, 104, 0x0d141c, 0.9));
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.blue, 0.42);
this.track(this.add.text(x + 14, y + 12, '현지 준비', this.textStyle(17, '#f2e3bf', true)));
this.track(this.add.text(x + 14, y + 12, '현지 준비 · 다음 전투 반영', this.textStyle(17, '#f2e3bf', true)));
this.track(this.add.text(x + 14, y + 40, `방문 ${completed.length}/${visits.length} 완료`, this.textStyle(13, completed.length >= visits.length && visits.length > 0 ? '#a8ffd0' : '#d4dce6', true)));
const isWolongSearch = this.currentSortieFlow().nextBattleId === campBattleIds.seventeenth;
const informationLabel = isWolongSearch
@@ -25005,6 +25060,10 @@ export class CampScene extends Phaser.Scene {
const firstBattleCampFollowup = this.firstBattleCampFollowup();
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
const firstPursuitScoutVisit = this.availableCampVisits().find(
(visit) => visit.id === firstPursuitScoutVisitId
);
const firstPursuitScoutMemory = this.firstPursuitScoutMemoryForNextSortie();
const availableCampDialogues = this.availableCampDialogues();
const selectedCampDialogue = availableCampDialogues.find(
(dialogue) => dialogue.id === this.selectedDialogueId
@@ -25164,6 +25223,25 @@ export class CampScene extends Phaser.Scene {
}
}
: null,
firstPursuitScoutMemory: {
visitId: firstPursuitScoutVisitId,
available: Boolean(firstPursuitScoutVisit),
completed: this.completedCampVisits().includes(firstPursuitScoutVisitId),
choiceId: this.campaign?.campVisitChoiceIds[firstPursuitScoutVisitId] ?? null,
choices: firstPursuitScoutVisit?.choices.map((choice) => ({
id: choice.id,
label: choice.label,
response: choice.response,
rewardText: this.visitRewardText(choice)
})) ?? [],
activeForNextSortie: Boolean(firstPursuitScoutMemory),
targetBattleId: firstPursuitScoutMemory?.targetBattleId ?? null,
choiceLabel: firstPursuitScoutMemory?.choiceLabel ?? null,
deploymentIntentPreview: firstPursuitScoutMemory?.deploymentIntentPreview ?? false,
campSummaryLine: firstPursuitScoutMemory?.campSummaryLine ?? null,
openingLines: firstPursuitScoutMemory ? [...firstPursuitScoutMemory.openingLines] : [],
briefingSummaryBounds: this.sortieTextBoundsDebug(this.sortieBriefingScoutSummaryText)
},
selectedDialogue: selectedCampDialogue
? {
id: selectedCampDialogue.id,