Add Hanzhong rainy pass campaign chapter

This commit is contained in:
2026-06-24 12:37:06 +09:00
parent f3d23a13be
commit 630be9969c
10 changed files with 859 additions and 78 deletions

View File

@@ -55,6 +55,7 @@ import {
northernFourthBattleBonds,
northernFirstBattleBonds,
northernSecondBattleBonds,
northernSeventhBattleBonds,
northernSixthBattleBonds,
northernThirdBattleBonds,
wulingRecruitBonds,
@@ -445,9 +446,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'fifty-seventh-battle-jieting-crisis',
'fifty-eighth-battle-qishan-retreat',
'fifty-ninth-battle-chencang-siege',
'sixtieth-battle-wudu-yinping'
'sixtieth-battle-wudu-yinping',
'sixty-first-battle-hanzhong-rain-defense'
],
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보']
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어']
}
];
@@ -511,7 +513,8 @@ const campBattleIds = {
fiftySeventh: 'fifty-seventh-battle-jieting-crisis',
fiftyEighth: 'fifty-eighth-battle-qishan-retreat',
fiftyNinth: 'fifty-ninth-battle-chencang-siege',
sixtieth: 'sixtieth-battle-wudu-yinping'
sixtieth: 'sixtieth-battle-wudu-yinping',
sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1261,6 +1264,23 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-liang', reason: '새로 확보할 고을의 백성에게 승리보다 약속을 먼저 설명할 문서 담당입니다.' }
],
note: '무도·음평 확보전은 추천 무장이 출전 제한보다 많습니다. 강유와 위연의 전진성, 조운·마대의 기동, 왕평·황권·마량의 제동과 안정 중 하나를 반드시 포기해야 합니다.'
},
'sixty-first-battle-hanzhong-rain-defense': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '한중 북문과 무도·음평 보급선을 동시에 조율해야 하는 총지휘관입니다. 비가 길을 끊기 전에 전선을 나누어야 합니다.' },
{ unitId: 'jiang-wei', reason: '새 고을과 한중 사이의 낮은 길을 읽어 사마의 별동대가 봉화를 잇기 전에 움직일 수 있습니다.' },
{ unitId: 'wang-ping', reason: '빗길 진영 표식과 물길 기준을 세워 병사들이 후퇴선과 보급선을 혼동하지 않게 합니다.' },
{ unitId: 'huang-quan', reason: '무도와 음평으로 갈라지는 군량 장부를 한중 창고와 맞춰 방어전의 장기 붕괴를 막습니다.' },
{ unitId: 'li-yan', reason: '수레와 운송 책임을 맡아 새 고을을 얻은 뒤 생긴 실제 부담을 전장에서 감당합니다.' },
{ unitId: 'ma-liang', reason: '새 고을의 고시문과 백성 안심 약속을 정리해 방어전이 점령군처럼 보이지 않게 합니다.' },
{ unitId: 'wei-yan', reason: '조진 본대가 깊이 들어오는 순간 짧게 반격할 돌파 장수입니다. 다만 비 오는 길에서는 제동 기준이 필요합니다.' },
{ unitId: 'zhao-yun', reason: '넓어진 전선에서 예비대를 회수하고 빈 고개를 메우는 안정적 기동 담당입니다.' },
{ unitId: 'ma-dai', reason: '서량 기병의 우회 감각으로 음평 물목과 조진 기병의 측면 압박을 견제합니다.' }
],
note: '한중 우로 방어전은 추천 무장이 9명으로 출전 제한보다 둘 많습니다. 새 고을 보급을 지킬 장부 담당, 빗길 표식, 반격 돌파, 기병 회수 중 무엇을 뒤에 남길지 정해야 합니다.'
}
};
@@ -1325,6 +1345,23 @@ const sortieRulesByCampaignStep: Partial<Record<CampaignStep, SortieRuleDefiniti
{ unitId: 'ma-dai', reason: '음평 물목과 서량 기병 운용을 다음 우회 전선 기준으로 남깁니다.' }
],
note: '무도·음평 확보 이후도 엔딩이 아니라 더 넓어진 북벌 책임입니다. 새 고을 안정과 다음 전진 기준을 함께 점검하십시오.'
},
'sixty-first-camp': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '한중 우로 방어 이후 다음 북벌의 공격로와 후방 방어선을 다시 나누어야 합니다.' },
{ unitId: 'jiang-wei', reason: '지켜 낸 무도·음평 길을 다음 공격로로 바꿀 수 있는지 판단해야 합니다.' },
{ unitId: 'wang-ping', reason: '빗길 표식과 진영 기준을 다음 출정의 후퇴선으로 정리합니다.' },
{ unitId: 'huang-quan', reason: '한중 창고와 새 고을 군량 흐름을 다음 출정 가능량으로 환산합니다.' },
{ unitId: 'li-yan', reason: '운송 책임과 지체 위험을 군영 단계에서 먼저 계산해야 합니다.' },
{ unitId: 'ma-liang', reason: '새 고을의 민심 문서를 다음 북벌 명분과 약속으로 이어 줍니다.' },
{ unitId: 'wei-yan', reason: '반격 돌파가 가능했던 지점과 더 기다려야 했던 지점을 구분합니다.' },
{ unitId: 'zhao-yun', reason: '예비대 회수 기준을 다음 넓은 전선의 안전장치로 남깁니다.' },
{ unitId: 'ma-dai', reason: '음평 물목의 기병 우회로를 다음 측면 대응 기준으로 정리합니다.' }
],
note: '한중 우로 방어 이후도 엔딩이 아니라 방어 책임이 넓어진 다음 북벌 준비입니다. 지킨 길을 공격로로 바꾸려면 누가 전방에 나가고 누가 보급을 붙들지 다시 골라야 합니다.'
}
};
@@ -6437,6 +6474,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 28
}
]
},
{
id: 'northern-seventh-liyan-huangquan-supply',
title: '빗길 장부',
availableAfterBattleIds: [campBattleIds.sixtyFirst],
unitIds: ['li-yan', 'huang-quan'],
bondId: 'li-yan__huang-quan_northern-seventh',
rewardExp: 98,
lines: [
'이엄: 비가 오면 군량은 명령보다 늦게 움직입니다. 늦은 수레를 죄로만 보면 다음 수레는 더 늦어집니다.',
'황권: 그래서 날짜와 고개를 함께 적어야 합니다. 어느 길이 막혔고 어느 길을 남겼는지 장부가 말해야 합니다.',
'이엄: 좋습니다. 다음에는 변명보다 먼저 도착할 장부를 만들겠습니다.'
],
choices: [
{
id: 'pair-rain-dates-ledgers',
label: '빗길 날짜와 장부를 묶는다',
response: '이엄은 수레가 늦어진 고개마다 날짜를 붙였고, 황권은 그 날짜를 다음 출정 가능량 옆에 옮겨 적었습니다.',
rewardExp: 34
},
{
id: 'mark-spare-grain-posts',
label: '예비 군량 표식을 세운다',
response: '황권은 한중 창고의 남은 군량을 나누었고, 이엄은 비가 그쳤을 때 가장 먼저 움직일 수레 자리를 따로 표시했습니다.',
rewardExp: 32
}
]
},
{
id: 'northern-seventh-jiangwei-wangping-road',
title: '지킬 길과 버릴 길',
availableAfterBattleIds: [campBattleIds.sixtyFirst],
unitIds: ['jiang-wei', 'wang-ping'],
bondId: 'jiang-wei__wang-ping_northern-seventh',
rewardExp: 96,
lines: [
'강유: 빗길에서는 빠른 길이 늘 옳지 않았습니다. 먼저 열린 길이 먼저 무너지는 곳도 있었습니다.',
'왕평: 그래서 표식은 전진보다 퇴각을 먼저 생각해야 합니다. 돌아올 길을 알아야 다음에 다시 나갑니다.',
'강유: 다음 북벌에서는 제가 보는 길마다 장군의 표식을 먼저 묻겠습니다.'
],
choices: [
{
id: 'mark-return-first-roads',
label: '돌아올 길부터 표시한다',
response: '강유는 공격로 앞에 왕평의 후퇴 표식을 먼저 세웠고, 왕평은 그 길을 다음 출정 장부의 첫 줄에 적었습니다.',
rewardExp: 33
},
{
id: 'compare-commandery-paths',
label: '두 고을 길을 비교한다',
response: '왕평은 무도와 음평의 물길 표식을 나란히 세웠고, 강유는 어느 길이 공격로가 되고 어느 길이 보급로가 될지 따로 그렸습니다.',
rewardExp: 31
}
]
},
{
id: 'northern-seventh-zhuge-weiyan-restraint',
title: '돌파와 제동',
availableAfterBattleIds: [campBattleIds.sixtyFirst],
unitIds: ['zhuge-liang', 'wei-yan'],
bondId: 'zhuge-liang__wei-yan_northern-seventh',
rewardExp: 94,
lines: [
'위연: 조진의 본대가 흔들릴 때 더 깊이 찔렀다면 큰 공을 세울 수 있었습니다.',
'제갈량: 그렇기에 멈추게 했습니다. 오늘의 깊은 공은 내일의 끊긴 수레가 될 수도 있습니다.',
'위연: 알겠소. 다만 다음에 멈추라면, 어디까지 찌른 뒤 멈출지도 함께 알려 주시오.'
],
choices: [
{
id: 'set-breakthrough-limit',
label: '돌파 한계선을 정한다',
response: '제갈량은 위연의 반격 한계선을 지도로 그었고, 위연은 그 선을 후퇴가 아니라 다음 돌파를 위한 약속으로 받아들였습니다.',
rewardExp: 32
},
{
id: 'reserve-counterattack-signal',
label: '반격 신호를 남긴다',
response: '위연은 빗길에서도 보이는 깃발 신호를 고쳤고, 제갈량은 그 신호가 보급선과 어긋나지 않도록 장부 옆에 붙였습니다.',
rewardExp: 30
}
]
}
];
@@ -9193,6 +9311,36 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['콩 32', '상처약 22']
}
]
},
{
id: 'northern-seventh-rain-granary-ledger',
title: '한중 우로 장부막',
location: '한중 북문 빗길 창고',
availableAfterBattleIds: [campBattleIds.sixtyFirst],
bondId: 'li-yan__huang-quan_northern-seventh',
description: '이엄과 황권이 한중 창고, 무도·음평 수레 길, 비에 늦어진 고개 기록을 다음 북벌 장부로 묶습니다.',
lines: [
'황권: 창고는 지켰지만 비가 남긴 빈칸이 많습니다. 이 빈칸을 숨기면 다음 출정이 먼저 무너집니다.',
'이엄: 빈칸까지 장부에 넣겠습니다. 늦은 길을 늦었다고 적어야 다음 수레가 억울하게 몰리지 않습니다.',
'장부막에는 한중 북문과 무도·음평의 수레 날짜가 한 줄로 이어졌고, 병사들은 다음 북벌이 아직 끝나지 않았음을 조용히 확인했습니다.'
],
choices: [
{
id: 'seal-hanzhong-rain-ledger',
label: '한중 우로 장부를 봉한다',
response: '황권은 한중 창고의 남은 군량을 봉했고, 이엄은 빗길 지연 기록을 붙여 다음 출정의 실제 기준으로 남겼습니다.',
bondExp: 78,
itemRewards: ['한중 우로 장부 1']
},
{
id: 'assign-commandery-relay-carts',
label: '두 고을 예비 수레를 배정한다',
response: '이엄은 무도와 음평에 예비 수레를 나누었고, 황권은 그 수레가 늦어질 때 대체할 고개 이름을 장부 옆에 적었습니다.',
bondExp: 72,
gold: 2760,
itemRewards: ['쌀 34', '상처약 24']
}
]
}
];
@@ -9301,7 +9449,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fiftySeventh ||
battleId === campBattleIds.fiftyEighth ||
battleId === campBattleIds.fiftyNinth ||
battleId === campBattleIds.sixtieth
battleId === campBattleIds.sixtieth ||
battleId === campBattleIds.sixtyFirst
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -9322,7 +9471,7 @@ export class CampScene extends Phaser.Scene {
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits,
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth ? jiangWeiRecruitUnits : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? jiangWeiRecruitUnits : [])
],
[
...xuzhouRecruitBonds,
@@ -9362,11 +9511,12 @@ export class CampScene extends Phaser.Scene {
...mengHuoFinalCaptureBonds,
...northernFirstBattleBonds,
...northernSecondBattleBonds,
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth ? jiangWeiRecruitBonds : []),
...(battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth ? northernFourthBattleBonds : []),
...(battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth ? northernFifthBattleBonds : []),
...(battleId === campBattleIds.sixtieth ? northernSixthBattleBonds : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? jiangWeiRecruitBonds : []),
...(battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernFourthBattleBonds : []),
...(battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernFifthBattleBonds : []),
...(battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernSixthBattleBonds : []),
...(battleId === campBattleIds.sixtyFirst ? northernSeventhBattleBonds : [])
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -10007,6 +10157,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'northern-campaign-prep-camp') {
return '북벌 준비 군영';
}
if (battleId === campBattleIds.sixtyFirst) {
return '한중 우로 방어 후 군영';
}
if (battleId === campBattleIds.sixtieth) {
return '무도·음평 확보 후 군영';
}

View File

@@ -16,6 +16,7 @@ import {
fiftyEighthBattleScenario,
fiftyNinthBattleScenario,
sixtiethBattleScenario,
sixtyFirstBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -420,6 +421,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'fifty-eighth-camp' ||
campaign.step === 'fifty-ninth-camp' ||
campaign.step === 'sixtieth-camp' ||
campaign.step === 'sixty-first-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp' ||
campaign.step === 'baidi-entrustment-camp' ||
@@ -729,6 +731,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'sixty-first-battle') {
this.scene.start('BattleScene', { battleId: sixtyFirstBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;