feat: add first-victory camp exploration

This commit is contained in:
2026-07-27 09:17:09 +09:00
parent 89c755ca42
commit e9fcd27611
17 changed files with 4557 additions and 113 deletions

View File

@@ -38,6 +38,7 @@ import {
firstPursuitScoutVisitId,
resolveFirstPursuitScoutMemory
} from '../data/firstPursuitScoutMemory';
import { firstPursuitScoutVisitDefinition } from '../data/firstPursuitScoutVisit';
import {
findCityStayAfterBattle,
type CityEquipmentOffer,
@@ -8401,33 +8402,13 @@ 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']
}
]
...firstPursuitScoutVisitDefinition,
availableAfterBattleIds: [...firstPursuitScoutVisitDefinition.availableAfterBattleIds],
lines: [...firstPursuitScoutVisitDefinition.lines],
choices: firstPursuitScoutVisitDefinition.choices.map((choice) => ({
...choice,
itemRewards: [...choice.itemRewards]
}))
},
{
id: 'jingzhou-scholar-rumors',
@@ -11407,6 +11388,7 @@ export class CampScene extends Phaser.Scene {
private campDialogueChoiceButtons: Record<string, Phaser.GameObjects.Rectangle> = {};
private campDialogueBodyText?: Phaser.GameObjects.Text;
private selectedVisitId = campVisits[0].id;
private firstPursuitExplorationButton?: Phaser.GameObjects.Rectangle;
private equipmentInventoryPage = 0;
private equipmentPanelBackground?: Phaser.GameObjects.Rectangle;
private equipmentInventoryPreviousButton?: Phaser.GameObjects.Rectangle;
@@ -11531,6 +11513,7 @@ export class CampScene extends Phaser.Scene {
this.campDialogueRowButtons = {};
this.campDialogueChoiceButtons = {};
this.campDialogueBodyText = undefined;
this.firstPursuitExplorationButton = undefined;
this.dialogueObjects = [];
this.sortieObjects = [];
this.sortieComparisonPanelView = undefined;
@@ -11642,6 +11625,14 @@ export class CampScene extends Phaser.Scene {
} else if (this.activeTab === 'city') {
this.activeTab = 'status';
}
const pendingFirstPursuitScoutVisit = this.availableCampVisits().some(
(visit) =>
visit.id === firstPursuitScoutVisitId &&
!this.completedCampVisits().includes(visit.id)
);
if (!cityStay && pendingFirstPursuitScoutVisit && this.activeTab === 'status') {
this.activeTab = 'visit';
}
soundDirector.playSoundscape({
musicKey: this.campSoundscape.music.trackKey,
ambienceKey: this.campSoundscape.ambience.trackKey,
@@ -13883,7 +13874,7 @@ export class CampScene extends Phaser.Scene {
: width < 70
? 'N'
: 'NEW';
const newBadge = tab === 'city' || tab === 'dialogue' || tab === 'supplies' || tab === 'equipment'
const newBadge = tab === 'city' || tab === 'dialogue' || tab === 'visit' || tab === 'supplies' || tab === 'equipment'
? this.scaleLegacyCampUi(this.add.text(x + width / 2 - 10, y - 17, badgeLabel, {
...this.textStyle(9, '#fff2b8', true),
backgroundColor: '#8a3f25',
@@ -13911,6 +13902,9 @@ export class CampScene extends Phaser.Scene {
const active = this.activeTab === tab;
const pendingFirstBattleFollowup = tab === 'dialogue' && this.hasPendingFirstBattleCampFollowup();
const pendingCamaraderieMemory = tab === 'dialogue' && this.hasPendingFirstBattleCamaraderieMemory();
const pendingCampVisit = tab === 'visit' && this.availableCampVisits().some(
(visit) => !this.completedCampVisits().includes(visit.id)
);
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);
text.setColor(active || hovered ? '#fff2b8' : '#f2e3bf');
@@ -13923,6 +13917,7 @@ export class CampScene extends Phaser.Scene {
newBadge?.setVisible(Boolean(
(tab === 'city' && this.hasPendingCityStayActions()) ||
(tab === 'dialogue' && (pendingFirstBattleFollowup || pendingCamaraderieMemory)) ||
pendingCampVisit ||
(tab === 'supplies' && pendingCategories.has('supplies')) ||
(tab === 'equipment' && pendingCategories.has('equipment'))
));
@@ -23366,6 +23361,48 @@ export class CampScene extends Phaser.Scene {
return;
}
if (visit.id === firstPursuitScoutVisitId) {
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,
'유비를 움직여 간옹에게 직접 말을 거세요.',
this.textStyle(13, '#d4dce6', true)
));
this.track(this.add.text(
x + 32,
y + height - 68,
'WASD·방향키 이동 · E/Space 대화',
this.textStyle(11, '#9fb0bf')
));
const buttonY = y + height - 36;
const button = this.track(this.add.rectangle(x + width / 2, buttonY, width - 64, 34, 0x4a371d, 0.98));
button.setStrokeStyle(2, palette.gold, 0.94);
button.setInteractive({ useHandCursor: true });
const openExploration = () => {
soundDirector.playSelect();
this.startCampNavigation('CampVisitExplorationScene', { visitId: visit.id });
};
button.on('pointerover', () => button.setFillStyle(0x654c25, 1));
button.on('pointerout', () => button.setFillStyle(0x4a371d, 0.98));
button.on('pointerdown', openExploration);
this.firstPursuitExplorationButton = button;
const label = this.track(this.add.text(
x + width / 2,
buttonY,
'정찰막으로 이동',
this.textStyle(14, '#fff2b8', true)
));
label.setOrigin(0.5);
label.setInteractive({ useHandCursor: true });
label.on('pointerdown', openExploration);
return;
}
visit.choices.forEach((choice, index) => {
const choiceY = y + height - 80 + index * 42;
const button = this.track(this.add.rectangle(x + width / 2, choiceY, width - 36, 34, 0x1a2630, 0.96));
@@ -24904,6 +24941,7 @@ export class CampScene extends Phaser.Scene {
this.campDialogueRowButtons = {};
this.campDialogueChoiceButtons = {};
this.campDialogueBodyText = undefined;
this.firstPursuitExplorationButton = undefined;
this.campRosterLayout = undefined;
this.cityPanelBackground = undefined;
this.cityExploreButton = undefined;
@@ -25227,6 +25265,15 @@ export class CampScene extends Phaser.Scene {
visitId: firstPursuitScoutVisitId,
available: Boolean(firstPursuitScoutVisit),
completed: this.completedCampVisits().includes(firstPursuitScoutVisitId),
mode: this.completedCampVisits().includes(firstPursuitScoutVisitId)
? 'completed'
: firstPursuitScoutVisit
? 'walk-to-npc'
: 'unavailable',
explorationButtonBounds: this.sortieInteractiveObjectBoundsDebug(
this.firstPursuitExplorationButton
),
explorationInteractive: Boolean(this.firstPursuitExplorationButton?.input?.enabled),
choiceId: this.campaign?.campVisitChoiceIds[firstPursuitScoutVisitId] ?? null,
choices: firstPursuitScoutVisit?.choices.map((choice) => ({
id: choice.id,