From d95954519a9b65aae1984224d76c277263c09bed Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 4 Jul 2026 02:56:52 +0900 Subject: [PATCH] Polish early campaign flow --- src/game/data/campaignFlow.ts | 11 ++-- src/game/data/scenario.ts | 104 ++++++++++++++++++++++++++++++++-- src/game/scenes/CampScene.ts | 19 ++++++- 3 files changed, 121 insertions(+), 13 deletions(-) diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 9d9c9f3..28836f7 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -136,7 +136,6 @@ import { fifteenthBattleVictoryPages, fourteenthBattleIntroPages, fourteenthBattleVictoryPages, - firstBattleVictoryPages, fourthBattleIntroPages, fourthBattleVictoryPages, hanzhongKingCouncilPages, @@ -223,18 +222,18 @@ const sortieFlows: Record = { afterBattleId: defaultBattleScenario.id, eyebrow: '다음 전장', title: secondBattleScenario.title, - description: '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 중앙 나루를 확보하며 첫 승리의 기세를 몰아 추격에 나섭니다.', - rewardHint: '예상 보상: 군자금, 소모품, 의용군 명성', + description: '탁현 방어 직후 달아난 황건 잔당이 북쪽 나루와 마을을 붙잡고 있습니다. 첫 승리 보상으로 정비를 마친 뒤, 세 형제가 다시 전열을 나누어 추격합니다.', + rewardHint: '지난 보상: 콩, 탁주, 연습검 · 다음 보상: 소모품, 황건 부적, 의용군 명성', nextBattleId: secondBattleScenario.id, campaignStep: 'second-battle', - pages: [...firstBattleVictoryPages, ...secondBattleIntroPages] + pages: secondBattleIntroPages }, [secondBattleScenario.id]: { afterBattleId: secondBattleScenario.id, eyebrow: '다음 전장', title: thirdBattleScenario.title, - description: '탁현을 넘어 광종으로 향하는 길목에서 황건 전령이 관군의 움직임을 본대로 전하려 합니다. 강가 요새와 좁은 길을 돌파해야 합니다.', - rewardHint: `예상 보상: ${thirdBattleScenario.title} 개방`, + description: '북쪽 마을을 구했지만 황건 전령이 광종 본영으로 향하고 있습니다. 나루 추격전에서 얻은 보급과 부적을 점검하고, 강가 요새로 이어지는 길을 끊어야 합니다.', + rewardHint: '지난 보상: 콩, 상처약, 탁주, 황건 부적 · 다음 보상: 단궁, 의용군 명성', nextBattleId: thirdBattleScenario.id, campaignStep: 'third-battle', pages: [...secondBattleVictoryPages, ...thirdBattleIntroPages] diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 61b5263..95b7e32 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -436,7 +436,32 @@ export const secondBattleIntroPages: StoryPage[] = [ background: 'story-yellow-pursuit', speaker: '장비', portrait: 'zhangFei', - text: '좋다! 숨어 있는 놈들도 모조리 끌어내 주마. 이번에는 도망칠 틈도 주지 않겠다!' + text: '좋다! 숨어 있는 놈들도 모조리 끌어내 주마. 이번에는 도망칠 틈도 주지 않겠다!', + cutscene: { + kind: 'operation', + title: '북쪽 나루 추격 작전', + subtitle: '첫 승리 직후 달아난 잔당을 나루와 마을 사이에서 끊어냅니다.', + actors: [ + { unitId: 'liu-bei', x: 0.25, y: 0.58, size: 104, direction: 'east', label: '후방 지휘' }, + { unitId: 'guan-yu', x: 0.35, y: 0.55, size: 112, direction: 'east', label: '나루 전열' }, + { unitId: 'zhang-fei', x: 0.42, y: 0.64, size: 112, direction: 'east', label: '남쪽 돌파' }, + { unitId: 'rebel-leader', x: 0.76, y: 0.42, size: 104, direction: 'west', label: '잔당 두령' } + ], + markers: [ + { x: 0.23, y: 0.72, label: '아군 시작', side: 'ally' }, + { x: 0.44, y: 0.56, label: '나루 길목', side: 'objective' }, + { x: 0.58, y: 0.48, label: '궁병 엄호', side: 'enemy' }, + { x: 0.73, y: 0.3, label: '북쪽 마을', side: 'objective' } + ], + briefing: { + title: '전열 의도', + lines: ['관우가 나루 앞 전열을 잡는다.', '장비가 남쪽에서 잔당과 궁병을 압박한다.', '유비는 뒤에서 생존과 마을 확보를 맡는다.'] + }, + rewards: [ + { label: '목표: 두령 격파 · 북쪽 마을 확보', tone: 'next' }, + { label: '보상: 상처약 · 황건 부적', tone: 'reward' } + ] + } } ]; @@ -462,7 +487,30 @@ export const secondBattleVictoryPages: StoryPage[] = [ bgm: 'battle-prep', chapter: '넓어지는 전장', background: 'story-yellow-pursuit', - text: '탁현 인근의 소란은 잦아들었지만, 황건의 난은 아직 천하 곳곳에서 타오르고 있었다. 세 형제의 길은 이제 더 멀리 이어진다.' + text: '탁현 인근의 소란은 잦아들었지만, 황건의 난은 아직 천하 곳곳에서 타오르고 있었다. 세 형제의 길은 이제 더 멀리 이어진다.', + cutscene: { + kind: 'victory', + title: '추격전 정리', + subtitle: '나루를 확보했지만 광종으로 향하는 전령의 움직임이 포착됩니다.', + actors: [ + { unitId: 'liu-bei', x: 0.31, y: 0.54, size: 120, direction: 'east', label: '유비' }, + { unitId: 'guan-yu', x: 0.43, y: 0.53, size: 126, direction: 'east', label: '관우' }, + { unitId: 'zhang-fei', x: 0.55, y: 0.55, size: 126, direction: 'west', label: '장비' } + ], + markers: [ + { x: 0.28, y: 0.68, label: '북쪽 마을', side: 'ally' }, + { x: 0.55, y: 0.48, label: '강가 요새', side: 'objective' }, + { x: 0.76, y: 0.34, label: '광종 본대', side: 'enemy' } + ], + briefing: { + title: '다음 정비', + lines: ['획득 보급을 다음 출전에 배정한다.', '황건 부적과 장비 상태를 확인한다.', '강가 요새의 좁은 길을 대비한다.'] + }, + rewards: [ + { label: '다음: 광종 구원로', tone: 'next' }, + { label: '정비: 보급 · 장비 확인', tone: 'reward' } + ] + } } ]; @@ -499,7 +547,32 @@ export const thirdBattleIntroPages: StoryPage[] = [ background: 'story-yellow-pursuit', speaker: '장비', portrait: 'zhangFei', - text: '전령이든 두령이든 내 앞길에 서면 다 똑같소! 형님, 명만 내리시오. 길목을 시원하게 열어 버리겠소!' + text: '전령이든 두령이든 내 앞길에 서면 다 똑같소! 형님, 명만 내리시오. 길목을 시원하게 열어 버리겠소!', + cutscene: { + kind: 'operation', + title: '광종 구원로 차단 작전', + subtitle: '황건 전령이 강가 요새를 지나 본영으로 향하기 전에 길목을 끊습니다.', + actors: [ + { unitId: 'liu-bei', x: 0.24, y: 0.58, size: 104, direction: 'east', label: '후방 지휘' }, + { unitId: 'guan-yu', x: 0.36, y: 0.55, size: 112, direction: 'east', label: '강가 전열' }, + { unitId: 'zhang-fei', x: 0.43, y: 0.65, size: 112, direction: 'east', label: '추격 출발' }, + { unitId: 'rebel-leader', x: 0.77, y: 0.38, size: 104, direction: 'west', label: '전령 마원' } + ], + markers: [ + { x: 0.22, y: 0.74, label: '아군 시작', side: 'ally' }, + { x: 0.46, y: 0.6, label: '강가 길목', side: 'objective' }, + { x: 0.6, y: 0.44, label: '요새 궁병', side: 'enemy' }, + { x: 0.77, y: 0.27, label: '전령 퇴로', side: 'enemy' } + ], + briefing: { + title: '공략 방침', + lines: ['관우가 요새 앞 견제를 받아낸다.', '장비가 좁은 길을 빠르게 압박한다.', '유비는 뒤에서 전열 붕괴와 퇴각 위험을 막는다.'] + }, + rewards: [ + { label: '목표: 전령 마원 격파', tone: 'next' }, + { label: '보상: 단궁 · 의용군 명성', tone: 'reward' } + ] + } } ]; @@ -525,7 +598,30 @@ export const thirdBattleVictoryPages: StoryPage[] = [ bgm: 'battle-prep', chapter: '광종으로', background: 'story-yellow-pursuit', - text: '세 형제는 병사를 추스르고 광종 방면으로 눈을 돌렸다. 황건의 큰 물결은 아직 꺾이지 않았다.' + text: '세 형제는 병사를 추스르고 광종 방면으로 눈을 돌렸다. 황건의 큰 물결은 아직 꺾이지 않았다.', + cutscene: { + kind: 'victory', + title: '구원로 확보', + subtitle: '전령의 길은 끊겼고, 다음 목표는 광종 본영의 중심입니다.', + actors: [ + { unitId: 'liu-bei', x: 0.29, y: 0.55, size: 118, direction: 'east', label: '지휘' }, + { unitId: 'guan-yu', x: 0.42, y: 0.54, size: 126, direction: 'east', label: '전열' }, + { unitId: 'zhang-fei', x: 0.55, y: 0.56, size: 126, direction: 'west', label: '돌파' } + ], + markers: [ + { x: 0.31, y: 0.68, label: '구원로', side: 'ally' }, + { x: 0.58, y: 0.47, label: '광종 외곽', side: 'objective' }, + { x: 0.76, y: 0.32, label: '황건 본영', side: 'enemy' } + ], + briefing: { + title: '다음 정비', + lines: ['획득한 단궁과 보급품을 확인한다.', '본영전은 장기전이 될 수 있어 회복 수단을 배정한다.', '세 형제의 역할을 유지하되 시작 전열을 다시 확인한다.'] + }, + rewards: [ + { label: '다음: 광종 본영전', tone: 'next' }, + { label: '정비: 단궁 · 보급품 확인', tone: 'reward' } + ] + } } ]; diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index cdbe7d0..a257def 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -12208,7 +12208,7 @@ export class CampScene extends Phaser.Scene { scenario.units, this.selectedSortieUnitIds, this.sortieFormationAssignments, - (unit) => this.defaultSortieFormationRole(unit), + (unit) => this.sortieRecommendation(unit.id, scenario)?.role ?? this.defaultSortieFormationRole(unit), this.nextSortieRule(scenario).deploymentSlots ); } @@ -12326,8 +12326,8 @@ export class CampScene extends Phaser.Scene { }; } - private sortieFormationRole(unit: UnitData): SortieFormationRole { - return this.sortieFormationAssignments[unit.id] ?? this.defaultSortieFormationRole(unit); + private sortieFormationRole(unit: UnitData, scenario = this.nextSortieScenario()): SortieFormationRole { + return this.sortieFormationAssignments[unit.id] ?? this.sortieRecommendation(unit.id, scenario)?.role ?? this.defaultSortieFormationRole(unit); } private defaultSortieFormationRole(unit: UnitData): SortieFormationRole { @@ -12559,6 +12559,14 @@ export class CampScene extends Phaser.Scene { const selected = this.selectedSortieUnits(); const recommendedClassLine = this.sortieRecommendedClassLine(); const recommendedClassComplete = this.coversRecommendedClasses(selected); + const roleCounts = selected.reduce( + (counts, unit) => { + counts[this.sortieFormationRole(unit)] += 1; + return counts; + }, + { front: 0, flank: 0, support: 0, reserve: 0 } as Record + ); + const rolesReady = selected.length <= 1 || (roleCounts.front > 0 && roleCounts.flank > 0 && roleCounts.support > 0); return [ { label: requiredLabel, @@ -12580,6 +12588,11 @@ export class CampScene extends Phaser.Scene { complete: recommendedClassComplete, detail: recommendedClassLine }, + { + label: '전열 역할', + complete: rolesReady, + detail: `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support}` + }, { label: '전투 보급', complete: assignedSupplyCount > 0 || supplyCount > 0,