Add Bowang ambush sortie chapter

This commit is contained in:
2026-06-23 05:42:50 +09:00
parent 8d958f6932
commit 44009b3542
12 changed files with 1015 additions and 28 deletions

View File

@@ -281,7 +281,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '적벽대전',
period: '강동의 바람',
description: '제갈량의 계책과 강동의 힘이 맞물려 조조의 대군을 막아서는 큰 전환 장입니다.',
battleIds: [],
battleIds: ['eighteenth-battle-bowang-ambush'],
nextHints: ['강동 사절', '화공 준비', '조조 남하']
},
{
@@ -319,7 +319,8 @@ const campBattleIds = {
fourteenth: 'fourteenth-battle-cao-break',
fifteenth: 'fifteenth-battle-yuan-refuge-road',
sixteenth: 'sixteenth-battle-liu-biao-refuge',
seventeenth: 'seventeenth-battle-wolong-visit-road'
seventeenth: 'seventeenth-battle-wolong-visit-road',
eighteenth: 'eighteenth-battle-bowang-ambush'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -498,6 +499,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'mi-zhu', reason: '산길 장기전에 필요한 보급과 회복 운용을 맡습니다.' }
],
note: '와룡 단서를 모은 뒤 융중으로 향합니다. 출전 제한보다 무장이 많으니 조운의 기동, 책사의 지원, 보급 중 무엇을 중시할지 선택하십시오.'
},
'eighteenth-battle-bowang-ambush': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '박망파 매복전의 패배 조건이자 군의 중심입니다.' },
{ unitId: 'zhuge-liang', reason: '이번 전투의 핵심인 매복 계책과 화공 판단을 맡습니다.' },
{ unitId: 'zhao-yun', reason: '조조군 기병을 끊고 숲길 측면을 빠르게 흔듭니다.' },
{ unitId: 'guan-yu', reason: '좁은 길목에서 하후돈 친위를 받아낼 전열입니다.' },
{ unitId: 'zhang-fei', reason: '매복 후 역습으로 적 선봉을 밀어내는 돌파 역할입니다.' },
{ unitId: 'sun-qian', reason: '강동 사절로 이어질 명분과 전후 보고를 정리합니다.' }
],
note: '제갈량의 첫 실전 계책입니다. 책략 담당을 포함해 전열, 기동, 보급 중 무엇을 남길지 결정해야 합니다.'
}
};
@@ -1932,6 +1945,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 8
}
]
},
{
id: 'liu-zhuge-after-bowang-ambush',
title: '불길 뒤의 큰길',
availableAfterBattleIds: [campBattleIds.eighteenth],
unitIds: ['liu-bei', 'zhuge-liang'],
bondId: 'liu-bei__zhuge-liang',
rewardExp: 30,
lines: [
'유비: 오늘 박망파에서 선생의 계책이 아니었다면, 조조군의 선봉을 작은 군영으로 막지 못했을 것이오.',
'제갈량: 주공께서 의심하지 않고 맡기셨기에 장수들도 움직였습니다. 계책은 사람의 마음이 모일 때 비로소 전장이 됩니다.',
'유비: 그렇다면 다음에는 더 큰 마음을 모아야겠소. 강동과 손잡는 길도 선생과 함께 열겠소.'
],
choices: [
{
id: 'trust-grand-strategy',
label: '큰 계책을 계속 맡긴다',
response: '유비는 제갈량에게 다음 외교와 군사 판단을 함께 맡겼고, 군영 안의 의심은 크게 줄어들었다.',
rewardExp: 12
},
{
id: 'share-command-with-generals',
label: '장수들에게 계책을 설명하게 한다',
response: '제갈량은 장수들 앞에서 박망파의 순서를 차분히 되짚었고, 다음 명령은 더 빠르게 받아들여질 준비가 되었다.',
rewardExp: 10
}
]
},
{
id: 'zhang-zhuge-after-bowang-ambush',
title: '호통과 부채',
availableAfterBattleIds: [campBattleIds.eighteenth],
unitIds: ['zhang-fei', 'zhuge-liang'],
bondId: 'zhang-fei__zhuge-liang',
rewardExp: 20,
lines: [
'장비: 군사 양반, 오늘은 인정하겠소. 불길이 오르니 적들이 제 창보다 먼저 겁을 먹더군.',
'제갈량: 익덕 장군의 창이 뒤에서 기다린다는 것을 적도 알았기 때문입니다. 계책만 있고 칼끝이 없으면 적은 물러나지 않습니다.',
'장비: 좋소. 다음에도 먼저 설명만 해 주시오. 내가 어디서 소리쳐야 적이 더 잘 넘어지는지만 알면 됩니다.'
],
choices: [
{
id: 'plan-zhang-fei-ambush-call',
label: '매복 호령을 맞춘다',
response: '제갈량은 장비가 뛰쳐나올 때와 물러설 때를 정했고, 장비는 드물게 끝까지 듣고 고개를 끄덕였다.',
rewardExp: 9
},
{
id: 'praise-bold-charge',
label: '돌파의 때를 칭찬한다',
response: '장비는 계책 안에서도 자신의 용맹이 필요하다는 말에 크게 웃었고, 다음 전장에서도 군사의 손짓을 보겠다고 했다.',
rewardExp: 8
}
]
},
{
id: 'sun-zhuge-after-bowang-ambush',
title: '강동으로 보낼 말',
availableAfterBattleIds: [campBattleIds.eighteenth],
unitIds: ['sun-qian', 'zhuge-liang'],
bondId: 'sun-qian__zhuge-liang',
rewardExp: 22,
lines: [
'손건: 박망파의 승리는 작은 군이 큰 군을 늦출 수 있다는 증거입니다. 강동에 보낼 말에도 힘이 생겼습니다.',
'제갈량: 강동은 말보다 형세를 볼 것입니다. 조조가 멈추지 않는다는 사실과, 주공이 백성을 버리지 않는다는 사실을 함께 전해야 합니다.',
'손건: 그렇다면 문서는 제가 다듬고, 큰 논리는 선생께서 세우십시오. 말과 계책이 같은 방향을 보아야 합니다.'
],
choices: [
{
id: 'draft-jiangdong-letter',
label: '강동 서신을 초안한다',
response: '손건은 제갈량의 형세 판단을 바탕으로 강동에 보낼 첫 서신의 골격을 잡았다.',
rewardExp: 10
},
{
id: 'record-cao-vanguard-delay',
label: '조조 선봉 지연을 기록한다',
response: '박망파에서 벌어들인 시간을 기록으로 남기자, 유비군의 작은 승리가 다음 외교의 근거가 되었다.',
rewardExp: 9
}
]
}
];
@@ -2082,6 +2176,65 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['콩 2']
}
]
},
{
id: 'bowang-battlefield-inspection',
title: '박망파 전장 점검',
location: '박망파 숲길',
availableAfterBattleIds: [campBattleIds.eighteenth],
bondId: 'liu-bei__zhuge-liang',
description: '박망파의 불길이 지나간 숲길을 살피며 다음 조조군 남하에 대비합니다.',
lines: [
'제갈량: 불길은 적의 발을 묶었지만, 다음에는 더 큰 군세가 올 것입니다. 길목마다 피난로와 매복지를 함께 살펴야 합니다.',
'유비: 백성이 지나갈 길과 군이 버틸 길을 따로 표시해 두시오. 승리보다 중요한 것은 사람을 잃지 않는 것이오.',
'병사들은 탄 흔적을 지우며, 이 작은 승리가 더 큰 전장의 시작임을 느낍니다.'
],
choices: [
{
id: 'mark-refugee-routes',
label: '피난로를 표시한다',
response: '제갈량은 백성이 지나갈 길을 군의 이동로와 분리해 지도에 적었고, 유비군의 다음 퇴각 준비가 빨라졌다.',
bondExp: 22,
itemRewards: ['피난로 지도 1']
},
{
id: 'collect-ambush-lessons',
label: '매복 기록을 정리한다',
response: '박망파의 매복 순서를 다시 기록하자, 다음 전장에서 책략 명령을 더 빨리 전달할 근거가 마련되었다.',
bondExp: 20,
itemRewards: ['와룡의 서찰 1']
}
]
},
{
id: 'jiangdong-letter-prep',
title: '강동 서신 준비',
location: '신야 객사',
availableAfterBattleIds: [campBattleIds.eighteenth],
bondId: 'sun-qian__zhuge-liang',
description: '강동에 보낼 첫 서신과 조조군 남하 정보를 정리합니다.',
lines: [
'손건: 강동은 말만으로 움직이지 않을 것입니다. 조조군이 남하한다는 증거와 주공이 버티는 이유를 함께 담아야 합니다.',
'제갈량: 맞습니다. 손권에게 필요한 것은 두려움만이 아니라 싸울 수 있다는 계산입니다.',
'서신의 문장은 짧아지고, 지도 위에는 조조군의 길과 강동의 바람이 함께 표시됩니다.'
],
choices: [
{
id: 'send-cao-threat-report',
label: '조조 남하 보고를 쓴다',
response: '손건은 박망파의 전황과 조조군의 움직임을 정리했고, 강동 사절의 첫 명분이 분명해졌다.',
bondExp: 18,
itemRewards: ['강동 풍문 1']
},
{
id: 'reserve-envoy-funds',
label: '사절 군자금을 남긴다',
response: '불필요한 지출을 줄여 강동으로 보낼 사절 비용을 따로 묶어 두었다.',
bondExp: 14,
gold: 180,
itemRewards: ['탁주 1']
}
]
}
];
@@ -2167,7 +2320,7 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.seventeenth) {
if (battleId === campBattleIds.seventeenth || battleId === campBattleIds.eighteenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits, ...zhugeRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds, ...zhugeRecruitBonds]
@@ -2224,6 +2377,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.eighteenth) {
return '박망파 승전 후 군영';
}
if (battleId === campBattleIds.seventeenth) {
return '와룡 출려 후 군영';
}
@@ -2498,8 +2654,8 @@ export class CampScene extends Phaser.Scene {
const allies = this.sortieAllies();
const selectedCount = this.selectedSortieUnitIds.length;
const maxUnits = this.sortieMaxUnits();
const rowGap = allies.length > 5 ? 38 : allies.length > 4 ? 43 : 48;
const rowHeight = rowGap - 6;
const rowGap = allies.length > 7 ? 30 : allies.length > 5 ? 38 : allies.length > 4 ? 43 : 48;
const rowHeight = allies.length > 7 ? 24 : rowGap - 6;
this.trackSortie(
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${maxUnits}`, this.textStyle(18, '#f2e3bf', true))
).setDepth(depth + 1);
@@ -2871,11 +3027,22 @@ export class CampScene extends Phaser.Scene {
const completedDialogues = this.completedAvailableDialogues().length;
const availableVisits = this.availableCampVisits();
const completedVisits = this.completedAvailableVisits().length;
const inventory = this.inventoryLabels().join(', ');
const selectedNames = this.selectedSortieUnits().map((unit) => unit.name).join(', ');
const inventoryLabels = this.inventoryLabels();
const selectedUnits = this.selectedSortieUnits();
const selectedPreview = selectedUnits.slice(0, 3).map((unit) => unit.name).join(', ');
const selectedSummary = selectedUnits.length > 0 ? `${selectedUnits.length}명 (${selectedPreview}${selectedUnits.length > 3 ? ' 외' : ''})` : '없음';
const inventorySummary =
inventoryLabels.length > 0 ? `${inventoryLabels.slice(0, 4).join(', ')}${inventoryLabels.length > 4 ? ' 외' : ''}` : '없음';
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}/${availableDialogues.length} · 방문 ${completedVisits}/${availableVisits.length} · 출전 ${selectedNames || '없음'} · 보유 ${inventory || '없음'}`, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1);
this.trackSortie(
this.add.text(
x + 16,
y + 30,
`현재 준비: 대화 ${completedDialogues}/${availableDialogues.length} · 방문 ${completedVisits}/${availableVisits.length} · 출전 ${selectedSummary} · 보유 ${inventorySummary}`,
this.textStyle(12, '#d4dce6')
)
).setDepth(depth + 1);
}
private nextSortieBriefing() {
@@ -3206,6 +3373,17 @@ export class CampScene extends Phaser.Scene {
}
private activeTimelineChapterIndex(completedIds = this.completedBattleIds()) {
const latestBattleId = this.campaign?.latestBattleId ?? this.report?.battleId;
const nextBattleId = getSortieFlow(this.campaign?.latestBattleId).nextBattleId;
if (!nextBattleId && latestBattleId) {
const latestChapterIndex = campaignTimelineChapters.findIndex((chapter) =>
chapter.battleIds.includes(latestBattleId as BattleScenarioId)
);
if (latestChapterIndex >= 0) {
return latestChapterIndex;
}
}
const index = campaignTimelineChapters.findIndex((chapter) => {
if (chapter.battleIds.length === 0) {
return true;