Add Qishan retreat campaign chapter

This commit is contained in:
2026-06-24 10:05:38 +09:00
parent a3f56bb787
commit f9689c0149
10 changed files with 839 additions and 72 deletions

View File

@@ -51,6 +51,7 @@ import {
mengHuoPacificationBonds,
mengHuoSecondCaptureBonds,
nanzhongRecoveryBonds,
northernFourthBattleBonds,
northernFirstBattleBonds,
northernSecondBattleBonds,
northernThirdBattleBonds,
@@ -439,9 +440,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
battleIds: [
'fifty-fifth-battle-northern-qishan-road',
'fifty-sixth-battle-tianshui-advance',
'fifty-seventh-battle-jieting-crisis'
'fifty-seventh-battle-jieting-crisis',
'fifty-eighth-battle-qishan-retreat'
],
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류']
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '다음 북벌 재정비']
}
];
@@ -502,7 +504,8 @@ const campBattleIds = {
fiftyFourth: 'fifty-fourth-battle-meng-huo-final-capture',
fiftyFifth: 'fifty-fifth-battle-northern-qishan-road',
fiftySixth: 'fifty-sixth-battle-tianshui-advance',
fiftySeventh: 'fifty-seventh-battle-jieting-crisis'
fiftySeventh: 'fifty-seventh-battle-jieting-crisis',
fiftyEighth: 'fifty-eighth-battle-qishan-retreat'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1206,6 +1209,21 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-dai', reason: '좁은 고개에서 기병을 지나치게 깊이 보내지 않고 되돌리는 역할에 어울립니다.' }
],
note: '가정 전투는 공격력이 높은 장수만으로는 버티기 어렵습니다. 제갈량과 왕평을 중심으로 물길, 수레 길, 설득, 기동 회수 역할을 7명 안에 담으십시오.'
},
'fifty-eighth-battle-qishan-retreat': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '기산 후퇴로의 총지휘관입니다. 무리한 추격보다 군량 수레와 병사를 살리는 판단이 필요합니다.' },
{ unitId: 'jiang-wei', reason: '합류 후 첫 출전 후보입니다. 천수의 낮은 길을 알아 퇴각로 확보와 새 공명 조합을 만듭니다.' },
{ unitId: 'wang-ping', reason: '물길과 수레 길을 함께 보는 장수입니다. 후퇴전에서 보급선 붕괴를 막는 핵심입니다.' },
{ unitId: 'zhao-yun', reason: '장합의 빠른 추격을 끊고 흩어진 병사를 회수하는 기동 담당입니다.' },
{ unitId: 'ma-dai', reason: '서량 기병의 감각으로 좁은 길을 우회하는 위군 기병을 늦출 수 있습니다.' },
{ unitId: 'huang-quan', reason: '군량 장부와 후방 진영 수습 목표를 안정시키는 보급 판단 담당입니다.' },
{ unitId: 'ma-liang', reason: '강유의 첫 출전과 후퇴 명분을 병사들에게 설득력 있게 정리합니다.' }
],
note: '기산 후퇴로는 강유를 처음 편성에 넣는 전투입니다. 제갈량을 중심으로 강유, 왕평, 조운, 마대, 보급·문서 담당 중 7명을 골라 퇴로를 지키십시오.'
}
};
@@ -1224,6 +1242,21 @@ const sortieRulesByCampaignStep: Partial<Record<CampaignStep, SortieRuleDefiniti
{ unitId: 'wei-yan', reason: '거친 돌파력을 어디까지 전방에 둘지 미리 판단해야 할 장수입니다.' }
],
note: '북벌 준비 회의는 전투가 아니지만 다음 장기전의 편성을 정하는 단계입니다. 제갈량을 중심으로 보급, 산길, 기동, 돌파 역할을 균형 있게 고르십시오.'
},
'fifty-eighth-camp': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '기산 후퇴 이후 북벌 재정비의 중심입니다. 다음 출정을 무리 없이 준비해야 합니다.' },
{ unitId: 'jiang-wei', reason: '첫 출전 평가가 필요한 새 장수입니다. 천수의 낮은 길을 다음 북벌 계획에 녹여야 합니다.' },
{ unitId: 'wang-ping', reason: '가정과 기산에서 배운 물길 원칙을 다음 진영 배치 기준으로 정리합니다.' },
{ unitId: 'zhao-yun', reason: '오래 지켜 온 회수 기동으로 후퇴전의 교훈을 병사들에게 훈련시킵니다.' },
{ unitId: 'ma-dai', reason: '서량 기병의 우회와 귀환선을 강유의 낮은 길과 비교해 다음 기병 운용을 다듬습니다.' },
{ unitId: 'huang-quan', reason: '장부와 보급을 재점검해 다음 북벌의 무리한 진군을 막습니다.' },
{ unitId: 'ma-liang', reason: '강유의 첫 출전과 후퇴 명분을 군영의 말과 기록으로 정리합니다.' }
],
note: '기산 후퇴 이후는 엔딩이 아니라 북벌 재정비 단계입니다. 제갈량과 강유를 중심으로 다음 출정에 남길 교훈을 정리하십시오.'
}
};
@@ -6093,6 +6126,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 25
}
]
},
{
id: 'northern-fourth-zhuge-jiangwei-first-road',
title: '새 장수에게 맡긴 길',
availableAfterBattleIds: [campBattleIds.fiftyEighth],
unitIds: ['zhuge-liang', 'jiang-wei'],
bondId: 'zhuge-liang__jiang-wei_northern-fourth',
rewardExp: 92,
lines: [
'강유: 승상, 오늘은 진군이 아니라 후퇴를 열었습니다. 제가 촉한에 온 첫 전공이 물러나는 길이라 마음이 무겁습니다.',
'제갈량: 병사를 살리고 장부를 지키는 일도 전공입니다. 나아갈 길을 아는 장수라면 물러날 길 또한 알아야 합니다.',
'강유: 다음 북방 길에서는 먼저 병사의 숨을 보겠습니다. 제가 아는 천수의 길을 촉한의 길로 만들겠습니다.'
],
choices: [
{
id: 'entrust-low-road',
label: '낮은 길을 맡긴다',
response: '제갈량은 강유에게 천수와 기산을 잇는 낮은 길 정리를 맡겼고, 강유는 새 장수로서 첫 책임을 받아들였습니다.',
rewardExp: 31
},
{
id: 'record-first-duty',
label: '첫 임무를 장부에 남긴다',
response: '강유의 첫 임무가 군량 수레와 병사 회수로 적혔습니다. 새 이름은 전공보다 책임으로 먼저 자리 잡았습니다.',
rewardExp: 29
}
]
},
{
id: 'northern-fourth-jiangwei-wangping-low-water',
title: '낮은 길과 물길',
availableAfterBattleIds: [campBattleIds.fiftyEighth],
unitIds: ['jiang-wei', 'wang-ping'],
bondId: 'jiang-wei__wang-ping_northern-fourth',
rewardExp: 88,
lines: [
'왕평: 천수의 낮은 길은 지도보다 사람의 발이 먼저 기억합니다. 오늘 강유 장군의 말이 늦었다면 수레가 막혔을 겁니다.',
'강유: 왕평 장군의 물길 표시가 없었다면 제가 아는 길도 군대의 길이 되지 못했습니다.',
'왕평: 다음에는 낮은 길과 물길을 한 장에 묶읍시다. 고지만 보는 실수는 다시 하지 않아야 합니다.'
],
choices: [
{
id: 'merge-low-road-waterline',
label: '낮은 길과 물길을 합친다',
response: '왕평과 강유는 낮은 길과 물길을 한 장의 지도에 묶었고, 기산 후퇴의 교훈은 다음 북벌의 표준이 되었습니다.',
rewardExp: 30
},
{
id: 'train-cart-turns',
label: '수레 회전 지점을 훈련한다',
response: '강유가 길을 읽고 왕평이 멈출 자리를 정하자, 군량 수레는 좁은 길에서도 흩어지지 않는 법을 익혔습니다.',
rewardExp: 28
}
]
},
{
id: 'northern-fourth-zhao-madai-jiangwei-return',
title: '기병의 귀환선',
availableAfterBattleIds: [campBattleIds.fiftyEighth],
unitIds: ['jiang-wei', 'zhao-yun'],
bondId: 'jiang-wei__zhao-yun_northern-fourth',
rewardExp: 86,
lines: [
'조운: 강유 장군, 빠른 길을 아는 것과 빠르게 돌아오는 것은 다릅니다. 오늘 그 차이를 함께 보았습니다.',
'강유: 자룡 장군의 회수 신호가 없었다면 제가 아는 길도 너무 깊은 길이 되었을 겁니다.',
'조운: 다음에는 마대와도 신호를 맞추십시오. 북방의 기병은 앞서가는 만큼 돌아오는 법을 알아야 합니다.'
],
choices: [
{
id: 'share-return-flags',
label: '귀환 깃발을 맞춘다',
response: '조운은 귀환 깃발의 순서를 강유에게 알려 주었고, 강유는 천수 길목마다 같은 표식을 남기기로 했습니다.',
rewardExp: 29
},
{
id: 'compare-mounted-routes',
label: '기병로를 비교한다',
response: '조운의 회수로, 마대의 우회로, 강유의 천수 낮은 길이 하나의 기병 운용표로 정리되었습니다.',
rewardExp: 27
}
]
}
];
@@ -8759,6 +8873,36 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['기병 편성표 1']
}
]
},
{
id: 'northern-fourth-qishan-ledger',
title: '기산 퇴각 장부',
location: '기산 군량 막사',
availableAfterBattleIds: [campBattleIds.fiftyEighth],
bondId: 'zhuge-liang__jiang-wei_northern-fourth',
description: '제갈량과 강유가 기산 후퇴로에서 지킨 병력, 군량, 지도 표식을 다음 북벌 장부로 정리합니다.',
lines: [
'제갈량: 오늘 지킨 것은 승리의 이름이 아니라 다음 출정을 가능하게 할 숫자입니다.',
'강유: 천수의 길을 숨기지 않겠습니다. 장부 위에 남기면 제 말도 군의 약속이 될 것입니다.',
'군량 막사에는 후퇴하며 지킨 수레 수, 잃지 않은 병력, 다음에 다시 건널 낮은 길이 함께 적혔습니다.'
],
choices: [
{
id: 'seal-qishan-retreat-ledger',
label: '퇴각 장부를 봉한다',
response: '제갈량은 퇴각 장부를 봉했고, 강유는 자신이 아는 천수 낮은 길을 다음 북벌의 참고로 남겼습니다.',
bondExp: 70,
itemRewards: ['기산 퇴각 장부 1']
},
{
id: 'assign-next-grain-route',
label: '다음 군량로를 배정한다',
response: '황권의 장부와 강유의 길 안내가 함께 묶였고, 다음 북방 보급로의 첫 후보가 정리되었습니다.',
bondExp: 66,
gold: 2480,
itemRewards: ['콩 28', '상처약 18']
}
]
}
];
@@ -8864,7 +9008,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fiftyFourth ||
battleId === campBattleIds.fiftyFifth ||
battleId === campBattleIds.fiftySixth ||
battleId === campBattleIds.fiftySeventh
battleId === campBattleIds.fiftySeventh ||
battleId === campBattleIds.fiftyEighth
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -8885,7 +9030,7 @@ export class CampScene extends Phaser.Scene {
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits,
...(battleId === campBattleIds.fiftySeventh ? jiangWeiRecruitUnits : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth ? jiangWeiRecruitUnits : [])
],
[
...xuzhouRecruitBonds,
@@ -8925,8 +9070,9 @@ export class CampScene extends Phaser.Scene {
...mengHuoFinalCaptureBonds,
...northernFirstBattleBonds,
...northernSecondBattleBonds,
...(battleId === campBattleIds.fiftySeventh ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh ? jiangWeiRecruitBonds : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth ? jiangWeiRecruitBonds : []),
...(battleId === campBattleIds.fiftyEighth ? northernFourthBattleBonds : [])
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -9567,6 +9713,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'northern-campaign-prep-camp') {
return '북벌 준비 군영';
}
if (battleId === campBattleIds.fiftyEighth) {
return '기산 후퇴로 후 군영';
}
if (battleId === campBattleIds.fiftySeventh) {
return '가정 배치 위기 후 군영';
}
@@ -9888,6 +10037,12 @@ export class CampScene extends Phaser.Scene {
const height = 676;
const x = Math.floor((this.scale.width - width) / 2);
const y = Math.floor((this.scale.height - height) / 2);
const flow = this.currentSortieFlow();
const isBattleSortie = Boolean(flow.nextBattleId);
const prepTitle = isBattleSortie ? '출진 준비' : '군영 의정';
const prepSubtitle = isBattleSortie
? '대화, 보급, 상인 정비를 마친 뒤 함께 출전할 무장을 선택합니다.'
: '군영 대화와 장부를 정리한 뒤 다음 장면에 함께할 무장을 선택합니다.';
const shade = this.trackSortie(this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.68));
shade.setOrigin(0);
@@ -9899,10 +10054,10 @@ export class CampScene extends Phaser.Scene {
panel.setDepth(depth + 1);
panel.setStrokeStyle(3, palette.gold, 0.92);
const title = this.trackSortie(this.add.text(x + 34, y + 28, '출진 준비', this.textStyle(30, '#f2e3bf', true)));
const title = this.trackSortie(this.add.text(x + 34, y + 28, prepTitle, this.textStyle(30, '#f2e3bf', true)));
title.setDepth(depth + 2);
const subtitle = this.trackSortie(
this.add.text(x + 34, y + 70, '대화, 보급, 상인 정비를 마친 뒤 함께 출전할 무장을 선택합니다.', this.textStyle(15, '#d4dce6'))
this.add.text(x + 34, y + 70, prepSubtitle, this.textStyle(15, '#d4dce6'))
);
subtitle.setDepth(depth + 2);
@@ -9917,7 +10072,7 @@ export class CampScene extends Phaser.Scene {
soundDirector.playSelect();
this.hideSortiePrep();
}, depth + 3);
this.addSortieButton('출진', x + width - 152, y + height - 42, 124, () => {
this.addSortieButton(isBattleSortie ? '출진' : '진행', x + width - 152, y + height - 42, 124, () => {
soundDirector.playSelect();
this.startVictoryStory();
}, depth + 3);
@@ -10147,7 +10302,7 @@ export class CampScene extends Phaser.Scene {
private sortieTerrainLine(unit: UnitData) {
const scenario = this.nextSortieScenario();
if (!scenario) {
return `${getUnitClass(unit.classKey).role}`;
return `지형 의정 · ${getUnitClass(unit.classKey).role}`;
}
const terrainCounts = this.passableTerrainCounts(scenario);

View File

@@ -13,6 +13,7 @@ import {
fiftyFifthBattleScenario,
fiftySixthBattleScenario,
fiftySeventhBattleScenario,
fiftyEighthBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -414,6 +415,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'fifty-fifth-camp' ||
campaign.step === 'fifty-sixth-camp' ||
campaign.step === 'fifty-seventh-camp' ||
campaign.step === 'fifty-eighth-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp' ||
campaign.step === 'baidi-entrustment-camp' ||
@@ -708,6 +710,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'fifty-eighth-battle') {
this.scene.start('BattleScene', { battleId: fiftyEighthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;