From 8e9bddcacf3d53bf2c8b37956adc42dcddfb8f44 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 00:08:25 +0900 Subject: [PATCH] Add battle-specific camp conversations --- docs/roadmap.md | 3 +- scripts/verify-flow.mjs | 19 +- src/game/scenes/CampScene.ts | 407 ++++++++++++++++++++++++++++++++++- 3 files changed, 418 insertions(+), 11 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 9c92838..bb94d8e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -22,12 +22,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Third battle expansion path toward Guangzong with a new battlefield, story bridge, campaign step persistence, and title continue support - Fourth battle Guangzong main camp confrontation with Zhang Jue, expanded enemy formation, campaign persistence, and anti-Dong Zhuo setup - Fifth battle Sishui Gate vanguard path starting the anti-Dong Zhuo chapter, with campaign persistence and title continue support +- Battle-specific camp conversations with selectable bond-growth choices from the first camp through the Gongsun Zan camp - Flow verification script from title through the fifth battle victory and camp save state ## Next Steps 1. Build the Gongsun Zan camp chapter after the Sishui Gate vanguard battle -2. Broaden camp events so bonds, supplies, and equipment choices matter between battles +2. Add the next Gongsun Zan-era battle and the story bridge toward Xu Province 3. Apply treasure equipment effects to damage, defense, recovery, and command rules 4. Add more distinctive victory/defeat branches and optional objectives 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index d9f2baa..57e7b63 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -272,6 +272,13 @@ try { if (!campState || campState.scene !== 'CampScene' || campState.report?.rewardGold <= 0) { throw new Error(`Expected CampScene report after victory: ${JSON.stringify(campState)}`); } + if ( + campState.campBattleId !== 'first-battle-zhuo-commandery' || + campState.availableDialogueIds?.length !== 3 || + !campState.availableDialogueIds.every((id) => id.endsWith('first-battle')) + ) { + throw new Error(`Expected first camp to expose first-battle dialogue set: ${JSON.stringify(campState)}`); + } await page.evaluate(() => { const campScene = window.__HEROS_GAME__?.scene.getScene('CampScene'); @@ -320,7 +327,7 @@ try { await page.waitForTimeout(260); const campStateAfterDialogue = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); - if (!campStateAfterDialogue.report?.completedCampDialogues?.length) { + if (!campStateAfterDialogue.report?.completedCampDialogues?.length || campStateAfterDialogue.completedAvailableDialogues?.length !== 1) { throw new Error(`Expected camp dialogue to award bond exp: ${JSON.stringify(campStateAfterDialogue)}`); } @@ -646,6 +653,16 @@ try { throw new Error(`Expected campaign save to persist fifth battle victory: ${JSON.stringify(campaignSaveAfterFifthBattle)}`); } + const fifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fifthCampState?.campBattleId !== 'fifth-battle-sishui-vanguard' || + fifthCampState.campTitle !== '공손찬 진영 군영' || + fifthCampState.availableDialogueIds?.length !== 3 || + !fifthCampState.availableDialogueIds.every((id) => id.endsWith('sishui')) + ) { + throw new Error(`Expected fifth camp to expose Gongsun Zan/Sishui dialogue set: ${JSON.stringify(fifthCampState)}`); + } + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index c3fde42..18bf2b9 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -21,6 +21,7 @@ type CampTab = 'status' | 'dialogue' | 'supplies'; type CampDialogue = { id: string; title: string; + availableAfterBattleIds: string[]; unitIds: [string, string]; bondId: string; rewardExp: number; @@ -121,10 +122,19 @@ const merchantItems: MerchantItemDefinition[] = [ } ]; +const campBattleIds = { + first: defaultBattleScenario.id, + second: 'second-battle-yellow-turban-pursuit', + third: 'third-battle-guangzong-road', + fourth: 'fourth-battle-guangzong-camp', + fifth: 'fifth-battle-sishui-vanguard' +} as const; + const campDialogues: CampDialogue[] = [ { id: 'liu-guan-after-first-battle', title: '의리를 다지는 밤', + availableAfterBattleIds: [campBattleIds.first], unitIds: ['liu-bei', 'guan-yu'], bondId: 'liu-bei__guan-yu', rewardExp: 14, @@ -151,6 +161,7 @@ const campDialogues: CampDialogue[] = [ { id: 'liu-zhang-after-first-battle', title: '거친 용기를 붙잡다', + availableAfterBattleIds: [campBattleIds.first], unitIds: ['liu-bei', 'zhang-fei'], bondId: 'liu-bei__zhang-fei', rewardExp: 14, @@ -177,6 +188,7 @@ const campDialogues: CampDialogue[] = [ { id: 'guan-zhang-after-first-battle', title: '칼과 창의 약속', + availableAfterBattleIds: [campBattleIds.first], unitIds: ['guan-yu', 'zhang-fei'], bondId: 'guan-yu__zhang-fei', rewardExp: 12, @@ -199,6 +211,330 @@ const campDialogues: CampDialogue[] = [ rewardExp: 4 } ] + }, + { + id: 'liu-guan-after-pursuit', + title: '추격 뒤의 절제', + availableAfterBattleIds: [campBattleIds.second], + unitIds: ['liu-bei', 'guan-yu'], + bondId: 'liu-bei__guan-yu', + rewardExp: 15, + lines: [ + '유비: 잔당을 쫓는 싸움일수록 칼끝이 흐려지기 쉽소. 오늘 운장의 판단이 군을 지켰소.', + '관우: 형님이 백성의 길을 먼저 살피라 하셨기에 무리한 추격을 멈출 수 있었습니다.', + '유비: 이기되 어지럽히지 않는 것, 그것이 우리가 지켜야 할 싸움이오.' + ], + choices: [ + { + id: 'steady-line', + label: '전열의 절제를 칭찬한다', + response: '관우는 전공보다 전열의 안정이 먼저라는 말을 마음에 새겼다.', + rewardExp: 6 + }, + { + id: 'entrust-village', + label: '마을 수비를 맡긴다', + response: '관우는 다음 싸움에서도 백성이 있는 곳을 먼저 지키겠다고 답했다.', + rewardExp: 5 + } + ] + }, + { + id: 'liu-zhang-after-pursuit', + title: '성급함과 선봉', + availableAfterBattleIds: [campBattleIds.second], + unitIds: ['liu-bei', 'zhang-fei'], + bondId: 'liu-bei__zhang-fei', + rewardExp: 15, + lines: [ + '장비: 형님, 추격전에서는 한 발만 더 빨랐다면 놈들을 몽땅 묶어 둘 수 있었습니다.', + '유비: 익덕의 빠른 창은 큰 힘이오. 다만 혼자 먼저 닿으면 뒤따르는 병사들이 흔들리오.', + '장비: 좋소. 다음엔 앞서 달리되, 형님 깃발이 보이는 거리 안에서 치겠습니다.' + ], + choices: [ + { + id: 'controlled-charge', + label: '통제된 돌격을 약속받는다', + response: '장비는 선봉의 힘과 군율이 함께 가야 한다는 말을 받아들였다.', + rewardExp: 6 + }, + { + id: 'praise-speed', + label: '빠른 결단을 칭찬한다', + response: '믿음을 받은 장비는 다음 전장에서 더 날카로운 선봉이 되겠다고 웃었다.', + rewardExp: 5 + } + ] + }, + { + id: 'guan-zhang-after-pursuit', + title: '길목을 나누다', + availableAfterBattleIds: [campBattleIds.second], + unitIds: ['guan-yu', 'zhang-fei'], + bondId: 'guan-yu__zhang-fei', + rewardExp: 13, + lines: [ + '관우: 익덕, 네가 길목을 막아 준 덕에 궁병들이 숨을 돌렸다.', + '장비: 운장 형님이 옆을 눌러 주니 제가 마음 놓고 앞을 칠 수 있었지요.', + '관우: 다음에도 누가 먼저 나서든, 남은 한 사람이 뒤를 끊어 주면 된다.' + ], + choices: [ + { + id: 'split-roads', + label: '길목 분담을 정한다', + response: '두 사람은 좁은 길에서 서로의 창끝과 칼끝이 부딪히지 않을 간격을 익혔다.', + rewardExp: 6 + }, + { + id: 'bold-competition', + label: '전공을 겨루게 둔다', + response: '가벼운 경쟁심이 두 맹장의 호흡을 더 뜨겁게 만들었다.', + rewardExp: 4 + } + ] + }, + { + id: 'liu-guan-after-guangzong-road', + title: '광종 길목의 침묵', + availableAfterBattleIds: [campBattleIds.third], + unitIds: ['liu-bei', 'guan-yu'], + bondId: 'liu-bei__guan-yu', + rewardExp: 16, + lines: [ + '유비: 광종으로 가는 길은 좁고, 적은 지형을 믿고 버티고 있소.', + '관우: 험한 길에서는 말보다 마음이 먼저 흔들립니다. 제가 앞에서 병사들의 숨을 고르겠습니다.', + '유비: 운장이 앞을 잡아 준다면, 나도 뒤에서 군의 뜻을 잃지 않게 하겠소.' + ], + choices: [ + { + id: 'lead-narrow-road', + label: '좁은 길의 선봉을 맡긴다', + response: '관우는 험지를 뚫는 임무를 조용히 받아들였다.', + rewardExp: 7 + }, + { + id: 'keep-discipline', + label: '군율 유지를 당부한다', + response: '관우는 승리보다 흐트러지지 않는 행군이 더 중요하다고 답했다.', + rewardExp: 5 + } + ] + }, + { + id: 'liu-zhang-after-guangzong-road', + title: '강가의 큰소리', + availableAfterBattleIds: [campBattleIds.third], + unitIds: ['liu-bei', 'zhang-fei'], + bondId: 'liu-bei__zhang-fei', + rewardExp: 16, + lines: [ + '장비: 강가 길목이 답답해서 창을 휘두를 맛이 덜했습니다.', + '유비: 답답한 싸움에서도 익덕이 버텨 주니 병사들이 물러서지 않았소.', + '장비: 다음엔 답답한 길도 제가 먼저 웃으며 열어 보이겠습니다.' + ], + choices: [ + { + id: 'laugh-through', + label: '호방함으로 사기를 올린다', + response: '장비의 큰 웃음이 군영의 무거운 공기를 풀었다.', + rewardExp: 6 + }, + { + id: 'guard-bridgehead', + label: '교두보 수비를 맡긴다', + response: '장비는 한 발도 물러서지 않는 수비 역시 선봉의 일이라며 고개를 끄덕였다.', + rewardExp: 5 + } + ] + }, + { + id: 'guan-zhang-after-guangzong-road', + title: '험지의 호흡', + availableAfterBattleIds: [campBattleIds.third], + unitIds: ['guan-yu', 'zhang-fei'], + bondId: 'guan-yu__zhang-fei', + rewardExp: 14, + lines: [ + '관우: 숲과 언덕에서는 한 걸음 늦어도 전열 전체가 늦어진다.', + '장비: 그럴 땐 형님이 길을 재고, 제가 길을 넓히면 되지 않겠습니까?', + '관우: 좋은 말이다. 칼이 재고 창이 연다면 험지도 길이 된다.' + ], + choices: [ + { + id: 'measure-and-break', + label: '관우가 재고 장비가 뚫는다', + response: '두 사람은 험지 돌파의 역할을 명확히 나누었다.', + rewardExp: 7 + }, + { + id: 'watch-each-other', + label: '서로의 후방을 살핀다', + response: '성급한 돌파보다 빈틈을 지우는 호흡이 깊어졌다.', + rewardExp: 5 + } + ] + }, + { + id: 'liu-guan-after-zhang-jue', + title: '황건의 깃발이 꺾인 밤', + availableAfterBattleIds: [campBattleIds.fourth], + unitIds: ['liu-bei', 'guan-yu'], + bondId: 'liu-bei__guan-yu', + rewardExp: 18, + lines: [ + '유비: 장각이 쓰러졌으나 천하가 바로 맑아진 것은 아니오.', + '관우: 큰 난이 끝난 뒤에도 작은 탐욕은 남습니다. 형님의 뜻이 흔들리지 않게 제가 곁에 있겠습니다.', + '유비: 운장과 익덕이 있으니, 더 큰 혼란에도 나아갈 용기가 생기오.' + ], + choices: [ + { + id: 'prepare-larger-chaos', + label: '더 큰 혼란을 준비한다', + response: '관우는 반동탁의 소문을 들으며 다시 칼을 정비했다.', + rewardExp: 7 + }, + { + id: 'honor-fallen-people', + label: '희생된 백성을 기린다', + response: '두 사람은 전공보다 백성의 상처를 먼저 기억하기로 했다.', + rewardExp: 6 + } + ] + }, + { + id: 'liu-zhang-after-zhang-jue', + title: '끝난 싸움, 남은 분노', + availableAfterBattleIds: [campBattleIds.fourth], + unitIds: ['liu-bei', 'zhang-fei'], + bondId: 'liu-bei__zhang-fei', + rewardExp: 18, + lines: [ + '장비: 장각을 꺾었는데도 속이 시원하지만은 않습니다. 아직 백성을 괴롭히는 놈들이 많습니다.', + '유비: 그 분노가 백성을 향하지 않고 악한 자에게만 향하도록 붙잡아야 하오.', + '장비: 형님이 길을 가리키면, 제 창은 그 길 밖으로 나가지 않겠습니다.' + ], + choices: [ + { + id: 'temper-anger', + label: '분노를 다스리게 한다', + response: '장비는 분노를 창끝에만 싣고 마음에는 남기지 않겠다고 했다.', + rewardExp: 7 + }, + { + id: 'promise-next-vanguard', + label: '다음 선봉을 약속한다', + response: '장비는 반동탁의 길에서도 가장 먼저 적진을 흔들겠다고 웃었다.', + rewardExp: 6 + } + ] + }, + { + id: 'guan-zhang-after-zhang-jue', + title: '맹장들의 전공', + availableAfterBattleIds: [campBattleIds.fourth], + unitIds: ['guan-yu', 'zhang-fei'], + bondId: 'guan-yu__zhang-fei', + rewardExp: 16, + lines: [ + '장비: 운장 형님, 오늘은 제가 더 크게 적진을 흔든 것 같습니다.', + '관우: 전공을 다투는 마음이 백성을 구하는 데 쓰인다면 나쁠 것이 없다.', + '장비: 그럼 다음 전장에서도 누가 더 크게 이름을 떨치는지 봅시다!' + ], + choices: [ + { + id: 'clean-rivalry', + label: '깨끗한 경쟁을 인정한다', + response: '두 맹장은 웃으며 전공 경쟁을 다음 전장의 활력으로 삼았다.', + rewardExp: 6 + }, + { + id: 'brothers-before-glory', + label: '명성보다 형제를 앞세운다', + response: '관우와 장비는 전공보다 서로의 생존을 먼저 지키기로 했다.', + rewardExp: 7 + } + ] + }, + { + id: 'liu-guan-after-sishui', + title: '공손찬의 깃발 아래', + availableAfterBattleIds: [campBattleIds.fifth], + unitIds: ['liu-bei', 'guan-yu'], + bondId: 'liu-bei__guan-yu', + rewardExp: 18, + lines: [ + '유비: 공손찬의 진영은 크고, 제후들의 생각은 저마다 다르오. 우리 뜻을 잃지 않는 것이 더 어려워졌소.', + '관우: 큰 깃발 아래 설수록 작은 의리를 더 단단히 붙잡아야 합니다.', + '유비: 운장의 말이 옳소. 남의 군영에 있어도 우리가 지킬 길은 변하지 않소.' + ], + choices: [ + { + id: 'hold-oath-in-coalition', + label: '연합 속에서도 맹세를 지킨다', + response: '관우는 도원에서 맺은 뜻을 낯선 군영에서도 흐리지 않겠다고 답했다.', + rewardExp: 7 + }, + { + id: 'observe-warlords', + label: '제후들의 속내를 살핀다', + response: '관우는 싸움만이 아니라 사람의 뜻을 살피는 눈도 필요하다고 받아들였다.', + rewardExp: 6 + } + ] + }, + { + id: 'liu-zhang-after-sishui', + title: '낯선 군영의 술잔', + availableAfterBattleIds: [campBattleIds.fifth], + unitIds: ['liu-bei', 'zhang-fei'], + bondId: 'liu-bei__zhang-fei', + rewardExp: 18, + lines: [ + '장비: 형님, 공손찬 군영엔 말도 많고 술도 많고, 괜히 시비 걸 놈들도 많습니다.', + '유비: 익덕의 기개는 좋으나, 지금은 싸워야 할 적과 참아야 할 말을 구분해야 하오.', + '장비: 하하, 형님이 그렇게 말하면 술잔은 들되 주먹은 참아 보겠습니다.' + ], + choices: [ + { + id: 'restrain-in-camp', + label: '군영 안의 절제를 당부한다', + response: '장비는 시비보다 형님의 체면을 먼저 지키겠다고 약속했다.', + rewardExp: 7 + }, + { + id: 'trust-his-honesty', + label: '솔직함을 믿어 준다', + response: '믿음을 받은 장비는 낯선 제후들 사이에서도 형님의 편에 서겠다고 웃었다.', + rewardExp: 6 + } + ] + }, + { + id: 'guan-zhang-after-sishui', + title: '연합군의 두 맹장', + availableAfterBattleIds: [campBattleIds.fifth], + unitIds: ['guan-yu', 'zhang-fei'], + bondId: 'guan-yu__zhang-fei', + rewardExp: 16, + lines: [ + '관우: 이곳에는 이름난 장수도 많고, 허세뿐인 자도 많다.', + '장비: 그럼 우리가 전장에서 보여 주면 되지요. 말보다 칼과 창이 빠릅니다.', + '관우: 다만 형님의 이름이 먼저 서야 한다. 우리의 무공은 그 뜻을 받치는 것이니.' + ], + choices: [ + { + id: 'raise-liu-bei-name', + label: '유비의 이름을 세운다', + response: '두 사람은 자신의 전공보다 유비의 명성을 먼저 드러내기로 뜻을 모았다.', + rewardExp: 7 + }, + { + id: 'test-famous-generals', + label: '이름난 장수들을 의식한다', + response: '장비의 승부욕과 관우의 침착함이 묘한 균형을 이루었다.', + rewardExp: 5 + } + ] } ]; @@ -227,6 +563,7 @@ export class CampScene extends Phaser.Scene { this.campaign = getCampaignState(); this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport(); this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei'; + this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id; soundDirector.playMusic('militia-theme'); const { width, height } = this.scale; @@ -234,7 +571,7 @@ export class CampScene extends Phaser.Scene { this.add.rectangle(0, 0, width, height, 0x06090d, 0.54).setOrigin(0); this.add.rectangle(0, 0, width, 84, 0x06090d, 0.52).setOrigin(0); - this.add.text(42, 28, '탁현 의용군 군영', { + this.add.text(42, 28, this.currentCampTitle(), { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '30px', color: '#f2e3bf', @@ -278,7 +615,39 @@ export class CampScene extends Phaser.Scene { const achieved = this.report.objectives.filter((objective) => objective.achieved).length; const gold = this.campaign?.gold ?? this.report.rewardGold; - return `첫 전투 ${this.report.turnNumber}턴 승리 · 군자금 ${gold} · 목표 ${achieved}/${this.report.objectives.length} · 격파 ${this.report.defeatedEnemies}/${this.report.totalEnemies}`; + return `${this.report.battleTitle} ${this.report.turnNumber}턴 승리 · 군자금 ${gold} · 목표 ${achieved}/${this.report.objectives.length} · 격파 ${this.report.defeatedEnemies}/${this.report.totalEnemies}`; + } + + private currentCampTitle() { + const battleId = this.currentCampBattleId(); + if (battleId === campBattleIds.fifth) { + return '공손찬 진영 군영'; + } + if (battleId === campBattleIds.fourth) { + return '광종 토벌 후 군영'; + } + if (battleId === campBattleIds.third) { + return '광종 구원로 군영'; + } + if (battleId === campBattleIds.second) { + return '황건 잔당 추격 군영'; + } + return '탁현 의용군 군영'; + } + + private currentCampBattleId() { + return this.campaign?.latestBattleId ?? this.report?.battleId ?? defaultBattleScenario.id; + } + + private availableCampDialogues() { + const battleId = this.currentCampBattleId(); + const dialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId)); + return dialogues.length > 0 ? dialogues : campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(defaultBattleScenario.id)); + } + + private completedAvailableDialogues() { + const completed = this.completedCampDialogues(); + return this.availableCampDialogues().filter((dialogue) => completed.includes(dialogue.id)); } private renderStaticButtons() { @@ -482,11 +851,12 @@ export class CampScene extends Phaser.Scene { bg.setOrigin(0); bg.setDepth(depth); bg.setStrokeStyle(1, palette.gold, 0.4); - const completedDialogues = this.completedCampDialogues().length; + const availableDialogues = this.availableCampDialogues(); + const completedDialogues = this.completedAvailableDialogues().length; const inventory = this.inventoryLabels().join(', '); const reward = getSortieFlow(this.campaign?.latestBattleId).rewardHint; this.trackSortie(this.add.text(x + 16, y + 10, reward, this.textStyle(13, '#f2e3bf', true))).setDepth(depth + 1); - this.trackSortie(this.add.text(x + 16, y + 30, `현재 준비: 대화 ${completedDialogues}/${campDialogues.length} · 보유 ${inventory || '없음'}`, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1); + this.trackSortie(this.add.text(x + 16, y + 30, `현재 준비: 대화 ${completedDialogues}/${availableDialogues.length} · 보유 ${inventory || '없음'}`, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1); } private nextSortieBriefing() { @@ -503,7 +873,8 @@ export class CampScene extends Phaser.Scene { const liuBei = units.find((unit) => unit.id === 'liu-bei'); const injured = units.filter((unit) => unit.hp < unit.maxHp); const supplyCount = campSupplies.reduce((total, supply) => total + this.inventoryAmount(supply.label), 0); - const completedDialogues = this.completedCampDialogues().length; + const availableDialogues = this.availableCampDialogues(); + const completedDialogues = this.completedAvailableDialogues().length; return [ { label: '유비 생존', @@ -522,8 +893,8 @@ export class CampScene extends Phaser.Scene { }, { label: '공명 대화', - complete: completedDialogues >= campDialogues.length, - detail: `${completedDialogues}/${campDialogues.length} 완료` + complete: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length, + detail: `${completedDialogues}/${availableDialogues.length} 완료` }, { label: '장비 상태', @@ -700,7 +1071,12 @@ export class CampScene extends Phaser.Scene { this.track(this.add.text(x + 24, y + 22, '장수 대화', this.textStyle(24, '#f2e3bf', true))); this.track(this.add.text(x + 24, y + 56, '출진 전 대화에서 선택지를 고르면 해당 장수들의 공명 경험치가 오릅니다.', this.textStyle(14, '#d4dce6'))); - campDialogues.forEach((dialogue, index) => { + const dialogues = this.availableCampDialogues(); + if (!dialogues.some((dialogue) => dialogue.id === this.selectedDialogueId)) { + this.selectedDialogueId = dialogues[0]?.id ?? campDialogues[0].id; + } + + dialogues.forEach((dialogue, index) => { const completed = this.completedCampDialogues().includes(dialogue.id); const rowY = y + 96 + index * 64; const selected = this.selectedDialogueId === dialogue.id; @@ -723,7 +1099,16 @@ export class CampScene extends Phaser.Scene { } private renderSelectedDialogue(x: number, y: number, width: number, height: number) { - const dialogue = campDialogues.find((candidate) => candidate.id === this.selectedDialogueId) ?? campDialogues[0]; + const dialogues = this.availableCampDialogues(); + const dialogue = dialogues.find((candidate) => candidate.id === this.selectedDialogueId) ?? dialogues[0]; + if (!dialogue) { + const empty = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92)); + empty.setOrigin(0); + empty.setStrokeStyle(1, palette.blue, 0.42); + this.track(this.add.text(x + 18, y + 16, '대화 없음', this.textStyle(20, '#f2e3bf', true))); + this.track(this.add.text(x + 18, y + 54, '현재 군영에서 가능한 장수 대화가 없습니다.', this.textStyle(14, '#9fb0bf'))); + return; + } const completed = this.completedCampDialogues().includes(dialogue.id); const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92)); bg.setOrigin(0); @@ -1207,6 +1592,10 @@ export class CampScene extends Phaser.Scene { sortieVisible: this.sortieObjects.length > 0, selectedUnitId: this.selectedUnitId, selectedDialogueId: this.selectedDialogueId, + campBattleId: this.currentCampBattleId(), + campTitle: this.currentCampTitle(), + availableDialogueIds: this.availableCampDialogues().map((dialogue) => dialogue.id), + completedAvailableDialogues: this.completedAvailableDialogues().map((dialogue) => dialogue.id), campaign: this.campaign ? { step: this.campaign.step,