Add Wolong recruitment chapter

This commit is contained in:
2026-06-23 05:09:14 +09:00
parent c209107b43
commit 64af9c1f68
12 changed files with 1033 additions and 22 deletions

View File

@@ -13,6 +13,8 @@ import {
liuBiaoRecruitUnits,
xuzhouRecruitBonds,
xuzhouRecruitUnits,
zhugeRecruitBonds,
zhugeRecruitUnits,
type PortraitKey,
type UnitData
} from '../data/scenario';
@@ -218,7 +220,8 @@ const campBattleIds = {
thirteenth: 'thirteenth-battle-xiapi-final',
fourteenth: 'fourteenth-battle-cao-break',
fifteenth: 'fifteenth-battle-yuan-refuge-road',
sixteenth: 'sixteenth-battle-liu-biao-refuge'
sixteenth: 'sixteenth-battle-liu-biao-refuge',
seventeenth: 'seventeenth-battle-wolong-visit-road'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -385,6 +388,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'mi-zhu', reason: '큰 맵의 장기전을 대비한 보급과 회복 운용에 유리합니다.' }
],
note: '유표 의탁로부터는 합류 무장이 출전 제한보다 많아집니다. 조운의 기동력을 시험할지, 책사와 보급을 두껍게 둘지 직접 고르십시오.'
},
'seventeenth-battle-wolong-visit-road': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '와룡을 직접 찾아가는 주장입니다.' },
{ unitId: 'zhao-yun', reason: '산길 척후와 기병을 빠르게 끊는 기동 역할입니다.' },
{ unitId: 'guan-yu', reason: '초가 앞 수비병을 정면에서 밀어낼 전열 핵심입니다.' },
{ unitId: 'zhang-fei', reason: '좁은 길목을 뚫고 채순을 압박하는 돌파 역할입니다.' },
{ unitId: 'sun-qian', reason: '와룡 방문의 예와 명분을 정리하는 책사 역할입니다.' },
{ unitId: 'mi-zhu', reason: '산길 장기전에 필요한 보급과 회복 운용을 맡습니다.' }
],
note: '와룡 단서를 모은 뒤 융중으로 향합니다. 출전 제한보다 무장이 많으니 조운의 기동, 책사의 지원, 보급 중 무엇을 중시할지 선택하십시오.'
}
};
@@ -1738,6 +1753,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 9
}
]
},
{
id: 'liu-zhuge-after-wolong-visit',
title: '초가에서 열린 큰 그림',
availableAfterBattleIds: [campBattleIds.seventeenth],
unitIds: ['liu-bei', 'zhuge-liang'],
bondId: 'liu-bei__zhuge-liang',
rewardExp: 28,
lines: [
'제갈량: 주공께서 세 번이나 초가를 찾으신 것은 한 사람을 얻기 위함이 아니라, 백성을 살릴 길을 묻기 위함이라 보았습니다.',
'유비: 나는 아직 작은 군영 하나도 온전히 지키기 어려운 몸이오. 그러나 선생의 말은 우리 군이 가야 할 길을 눈앞에 펼쳤소.',
'제갈량: 그 길은 서두르면 흐트러지고, 늦추면 빼앗깁니다. 이제부터는 뜻과 때를 함께 살피겠습니다.'
],
choices: [
{
id: 'share-three-kingdoms-plan',
label: '천하삼분의 뜻을 묻는다',
response: '제갈량은 형주와 익주, 그리고 북벌의 때를 차분히 설명했고, 유비는 그 말을 군영의 첫 방침으로 삼았다.',
rewardExp: 12
},
{
id: 'entrust-camp-strategy',
label: '군영의 계책을 맡긴다',
response: '유비는 제갈량에게 군영의 큰 판단을 맡겼고, 공명은 병사와 장수의 이름부터 조용히 살피기 시작했다.',
rewardExp: 10
}
]
},
{
id: 'zhao-zhuge-after-wolong-visit',
title: '말발굽과 부채',
availableAfterBattleIds: [campBattleIds.seventeenth],
unitIds: ['zhao-yun', 'zhuge-liang'],
bondId: 'zhao-yun__zhuge-liang',
rewardExp: 22,
lines: [
'조운: 선생의 계책은 멀리 보되, 실제 길목의 먼지까지 살피는군요. 제 말이 달릴 곳을 미리 본 듯했습니다.',
'제갈량: 자룡의 창은 빠르지만 앞서 나가도 군의 마음을 놓치지 않습니다. 그래서 빠른 구원이 가능해집니다.',
'조운: 그렇다면 다음 전장에서는 선생의 지시에 맞춰, 가장 필요한 곳에 먼저 닿겠습니다.'
],
choices: [
{
id: 'map-relief-routes',
label: '구원로를 함께 살핀다',
response: '제갈량은 지형 위에 구원로를 표시했고, 조운은 말이 지나갈 수 있는 폭과 위험한 샛길을 덧붙였다.',
rewardExp: 10
},
{
id: 'coordinate-feint-charge',
label: '유인 돌격을 맞춘다',
response: '조운은 일부러 빈틈을 보이는 돌격법을 익혔고, 제갈량은 그 틈에 적을 몰아넣는 순서를 정했다.',
rewardExp: 9
}
]
},
{
id: 'guan-zhuge-after-wolong-visit',
title: '의와 계책의 첫 인사',
availableAfterBattleIds: [campBattleIds.seventeenth],
unitIds: ['guan-yu', 'zhuge-liang'],
bondId: 'guan-yu__zhuge-liang',
rewardExp: 20,
lines: [
'관우: 선생의 말은 크고 깊으나, 전장에서는 한 걸음의 늦음도 군을 잃게 합니다. 그 점을 잊지 않으셨으면 하오.',
'제갈량: 운장께서 전열을 지키는 까닭도 결국 주공의 뜻을 잃지 않기 위함입니다. 저는 그 의를 가벼이 여기지 않겠습니다.',
'관우: 그렇다면 나 또한 선생의 계책을 먼저 듣겠소. 의가 길을 잃지 않게 해 주시오.'
],
choices: [
{
id: 'align-duty-and-plan',
label: '의와 계책을 맞춘다',
response: '관우는 제갈량의 병력 운용을 듣고 전열의 기준을 새로 세웠고, 제갈량은 관우가 지켜야 할 선을 존중했다.',
rewardExp: 9
},
{
id: 'promise-clear-orders',
label: '명령의 기준을 분명히 한다',
response: '제갈량은 전장 명령의 우선순위를 문서로 정리했고, 관우는 그 분명함을 보고 조용히 고개를 끄덕였다.',
rewardExp: 8
}
]
}
];
@@ -1829,6 +1925,65 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['탁주 1']
}
]
},
{
id: 'longzhong-cottage-records',
title: '융중 초가 정리',
location: '융중 초가',
availableAfterBattleIds: [campBattleIds.seventeenth],
bondId: 'liu-bei__zhuge-liang',
description: '제갈량과 함께 초가의 지도와 서책을 정리하며 다음 큰 흐름을 준비합니다.',
lines: [
'제갈량: 이 지도들은 아직 군영의 전장 지도가 아니라 천하의 형세입니다. 그러나 전장은 늘 여기서 시작됩니다.',
'유비: 우리 군이 지금 가진 것은 많지 않소. 그래서 더더욱 무엇을 먼저 지킬지 알아야 하오.',
'초가의 낮은 책상 위에는 형주와 강동, 북방의 길이 하나씩 펼쳐집니다.'
],
choices: [
{
id: 'study-red-cliff-winds',
label: '강동의 바람을 살핀다',
response: '제갈량은 강동과 조조군의 충돌 가능성을 짚었고, 유비군은 다음 큰 전장의 이름을 조심스럽게 떠올렸다.',
bondExp: 24,
itemRewards: ['강동 풍문 1']
},
{
id: 'organize-wolong-scrolls',
label: '와룡의 서책을 정리한다',
response: '손상되기 쉬운 서책을 군영으로 옮기며 제갈량의 계책을 병사들도 이해할 수 있는 말로 옮기기 시작했다.',
bondExp: 20,
itemRewards: ['와룡의 서찰 1']
}
]
},
{
id: 'xinye-merchant-after-wolong',
title: '신야 상인 접견',
location: '신야 장터',
availableAfterBattleIds: [campBattleIds.seventeenth],
bondId: 'liu-bei__mi-zhu',
description: '제갈량 합류 소문을 듣고 찾아온 상인들과 다음 전투를 위한 보급 조건을 조율합니다.',
lines: [
'미축: 와룡이 주공의 군영에 들었다는 말이 퍼지자 상인들의 셈도 달라졌습니다. 지금 보급선을 고정할 기회입니다.',
'유비: 값이 싸다고 무리하게 사들이지는 마시오. 백성의 삶을 해치지 않는 선에서 군을 세워야 하오.',
'상인들은 유비군의 앞날에 걸지, 당장의 이익만 챙길지 서로 눈치를 봅니다.'
],
choices: [
{
id: 'secure-medicine-contract',
label: '약재 계약을 맺는다',
response: '미축은 오래 보관할 수 있는 약재 계약을 맺었고, 군영의 치료 물자가 안정되었다.',
bondExp: 14,
itemRewards: ['상처약 2']
},
{
id: 'preserve-war-funds',
label: '군자금을 아낀다',
response: '미축은 급하지 않은 거래를 미루고 군자금을 보존했다. 병사들은 검소한 군영의 기준을 이해했다.',
bondExp: 12,
gold: 160,
itemRewards: ['콩 2']
}
]
}
];
@@ -1914,6 +2069,15 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.seventeenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits, ...zhugeRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds, ...zhugeRecruitBonds]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (battleId === campBattleIds.fifteenth || battleId === campBattleIds.sixteenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits],
@@ -1962,6 +2126,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.seventeenth) {
return '와룡 출려 후 군영';
}
if (battleId === campBattleIds.sixteenth) {
return '형주 의탁 후 군영';
}
@@ -2701,6 +2868,14 @@ export class CampScene extends Phaser.Scene {
return;
}
if (flow.nextBattleId === campBattleIds.seventeenth && this.wolongClueCount() <= 0) {
this.hideSortiePrep();
this.activeTab = 'visit';
this.render();
this.showCampNotice('와룡의 단서를 먼저 모으십시오. 방문 탭에서 형주 선비들의 말을 들어 보세요.');
return;
}
markCampaignStep(flow.campaignStep);
this.scene.start('StoryScene', {
pages: flow.pages,
@@ -3058,7 +3233,7 @@ export class CampScene extends Phaser.Scene {
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 + 40, `방문 ${completed.length}/${visits.length} 완료`, this.textStyle(13, completed.length >= visits.length && visits.length > 0 ? '#a8ffd0' : '#d4dce6', true)));
const insightCount = this.inventoryAmount('와룡 소문') + this.inventoryAmount('민심 기록');
const insightCount = this.wolongClueCount();
this.track(this.add.text(x + 14, y + 66, `제갈량 단서 ${insightCount} · 군자금 ${this.campaign?.gold ?? 0}`, this.textStyle(13, '#9fb0bf', true)));
}
@@ -3586,6 +3761,10 @@ export class CampScene extends Phaser.Scene {
return this.campaign?.inventory[label] ?? 0;
}
private wolongClueCount() {
return this.inventoryAmount('와룡 소문') + this.inventoryAmount('민심 기록');
}
private drawBar(x: number, y: number, width: number, height: number, ratio: number, color: number) {
const track = this.track(this.add.rectangle(x, y, width, height, 0x070b10, 0.86));
track.setOrigin(0);