Add Qishan renewed offensive chapter

This commit is contained in:
2026-06-24 15:00:24 +09:00
parent 305ead1777
commit 44c017ab6c
16 changed files with 892 additions and 98 deletions

View File

@@ -6458,6 +6458,26 @@ export class BattleScene extends Phaser.Scene {
}
private enemyBehavior(enemy: UnitData): EnemyAiBehavior {
const lateNorthernPrefixes = [
'northern-fourth',
'northern-fifth',
'northern-sixth',
'northern-seventh',
'northern-eighth'
];
if (lateNorthernPrefixes.some((prefix) => enemy.id.startsWith(prefix))) {
if (
enemy.id.includes('-scout') ||
enemy.id.includes('-cavalry') ||
enemy.id === 'northern-eighth-officer-zhang-he'
) {
return 'aggressive';
}
if (enemy.id.includes('-archer') || enemy.id.includes('-strategist') || enemy.id.includes('-leader-')) {
return 'hold';
}
return 'guard';
}
if (
enemy.id.startsWith('northern-first-scout') ||
enemy.id.startsWith('northern-second-scout') ||

View File

@@ -18,6 +18,7 @@ import storyYizhouMountainPassUrl from '../../assets/images/story/12-yizhou-moun
import storyNorthernExpeditionUrl from '../../assets/images/story/13-northern-expedition.png';
import storyNanzhongPacificationUrl from '../../assets/images/story/14-nanzhong-pacification.png';
import storyYilingBaidiAftermathUrl from '../../assets/images/story/15-yiling-baidi-aftermath.png';
import storyQishanRenewedOffensiveUrl from '../../assets/images/story/16-qishan-renewed-offensive.png';
import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png';
import guanYuActionSheetUrl from '../../assets/images/units/unit-guan-yu-actions.png';
import guanYuUnitSheetUrl from '../../assets/images/units/unit-guan-yu.png';
@@ -79,6 +80,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('story-northern', storyNorthernExpeditionUrl);
this.load.image('story-nanzhong', storyNanzhongPacificationUrl);
this.load.image('story-yiling-baidi', storyYilingBaidiAftermathUrl);
this.load.image('story-qishan-renewed', storyQishanRenewedOffensiveUrl);
this.load.image('story-resolve', storyLiuBeiUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);

View File

@@ -51,6 +51,7 @@ import {
mengHuoPacificationBonds,
mengHuoSecondCaptureBonds,
nanzhongRecoveryBonds,
northernEighthBattleBonds,
northernFifthBattleBonds,
northernFourthBattleBonds,
northernFirstBattleBonds,
@@ -447,9 +448,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'fifty-eighth-battle-qishan-retreat',
'fifty-ninth-battle-chencang-siege',
'sixtieth-battle-wudu-yinping',
'sixty-first-battle-hanzhong-rain-defense'
'sixty-first-battle-hanzhong-rain-defense',
'sixty-second-battle-qishan-renewed-offensive'
],
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어']
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어', '기산 재출정']
}
];
@@ -514,7 +516,8 @@ const campBattleIds = {
fiftyEighth: 'fifty-eighth-battle-qishan-retreat',
fiftyNinth: 'fifty-ninth-battle-chencang-siege',
sixtieth: 'sixtieth-battle-wudu-yinping',
sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense'
sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense',
sixtySecond: 'sixty-second-battle-qishan-renewed-offensive'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1281,6 +1284,23 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-dai', reason: '서량 기병의 우회 감각으로 음평 물목과 조진 기병의 측면 압박을 견제합니다.' }
],
note: '한중 우로 방어전은 추천 무장이 9명으로 출전 제한보다 둘 많습니다. 새 고을 보급을 지킬 장부 담당, 빗길 표식, 반격 돌파, 기병 회수 중 무엇을 뒤에 남길지 정해야 합니다.'
},
'sixty-second-battle-qishan-renewed-offensive': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '노성 앞 지연선을 상대하며 전진과 회수, 보급과 민심을 동시에 나누어야 하는 총지휘관입니다.' },
{ unitId: 'jiang-wei', reason: '지켜 낸 무도·음평 길을 다시 공격로로 바꾸는 길잡이입니다. 사마의의 지연선을 먼저 읽어야 합니다.' },
{ unitId: 'wei-yan', reason: '노성 앞 지연선을 짧게 흔들 돌파 장수입니다. 다만 장합의 추격과 돌아올 길을 함께 계산해야 합니다.' },
{ unitId: 'zhao-yun', reason: '깊이 들어간 부대를 회수하고 예비대를 묶어 장합 기병의 추격을 늦춥니다.' },
{ unitId: 'wang-ping', reason: '기산 산길의 표식과 회수선을 세워 공격로가 퇴로를 잃지 않게 합니다.' },
{ unitId: 'huang-quan', reason: '한중 우로 장부를 공세 가능량으로 환산해 보급 붕괴를 막습니다.' },
{ unitId: 'ma-liang', reason: '노성 앞 마을과 새 보급로의 고시문을 정리해 북벌 명분을 실제 약속으로 보이게 합니다.' },
{ unitId: 'li-yan', reason: '수레와 운송 지연을 후방에서 관리합니다. 전장에 데려가면 보급 위기는 줄지만 기동 선택이 줄어듭니다.' },
{ unitId: 'ma-dai', reason: '서량 기병의 측면 감각으로 곽회의 차단로와 동쪽 물목을 견제합니다.' }
],
note: '기산 재출정전은 추천 무장이 9명입니다. 공격로를 열 장수, 돌아올 표식을 세울 장수, 후방 장부를 맡길 장수 중 둘을 반드시 군영에 남겨야 합니다.'
}
};
@@ -1362,6 +1382,23 @@ const sortieRulesByCampaignStep: Partial<Record<CampaignStep, SortieRuleDefiniti
{ unitId: 'ma-dai', reason: '음평 물목의 기병 우회로를 다음 측면 대응 기준으로 정리합니다.' }
],
note: '한중 우로 방어 이후도 엔딩이 아니라 방어 책임이 넓어진 다음 북벌 준비입니다. 지킨 길을 공격로로 바꾸려면 누가 전방에 나가고 누가 보급을 붙들지 다시 골라야 합니다.'
},
'sixty-second-camp': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '기산 재출정 이후 더 깊은 북방 계산을 다시 세워야 하는 중심입니다.' },
{ unitId: 'jiang-wei', reason: '노성 앞에서 확인한 위군 지연선을 다음 공격로 지도로 정리합니다.' },
{ unitId: 'wei-yan', reason: '돌파가 성과를 냈을 때 어디까지 허용할지 다음 군령으로 남깁니다.' },
{ unitId: 'zhao-yun', reason: '장합 추격을 견딘 회수 기동을 예비대 훈련 기준으로 정리합니다.' },
{ unitId: 'wang-ping', reason: '기산 회수 표식과 진영 정지선을 다음 전선의 안전장치로 남깁니다.' },
{ unitId: 'huang-quan', reason: '노성 앞 공세가 실제로 소모한 군량과 남은 장부를 계산합니다.' },
{ unitId: 'ma-liang', reason: '전방 마을 안심 약속과 고시문을 다음 북벌 명분으로 이어 줍니다.' },
{ unitId: 'li-yan', reason: '운송 지연과 예비 수레 배정을 후방 기준으로 다시 정리합니다.' },
{ unitId: 'ma-dai', reason: '곽회 차단로 대응과 서량 기병 우회 기준을 다음 측면 운용에 반영합니다.' }
],
note: '기산 재출정 이후도 엔딩이 아니라 더 깊어진 북방 전선 준비입니다. 이긴 길을 지킬 사람과 더 깊이 들어갈 사람을 나누어 보십시오.'
}
};
@@ -6555,6 +6592,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 30
}
]
},
{
id: 'northern-eighth-jiangwei-weiyan-forward',
title: '앞길을 여는 칼끝',
availableAfterBattleIds: [campBattleIds.sixtySecond],
unitIds: ['jiang-wei', 'wei-yan'],
bondId: 'jiang-wei__wei-yan_northern-eighth',
rewardExp: 100,
lines: [
'강유: 노성 앞에서는 빠른 길이 곧 안전한 길은 아니었습니다. 먼저 열어도 돌아올 표식이 없으면 길이 아닙니다.',
'위연: 그렇다면 다음에는 그대가 길을 고르고 내가 길을 넓히겠소. 대신 멈출 곳도 처음부터 말해 주시오.',
'강유: 전진선과 회수선을 함께 그리겠습니다. 돌파가 장수의 고집이 아니라 군의 길이 되도록 하겠습니다.'
],
choices: [
{
id: 'mark-breakthrough-return-line',
label: '돌파선과 회수선을 함께 긋는다',
response: '강유는 낮은 길을 지도에 표시했고, 위연은 그 선을 넘어설 때 필요한 깃발 신호를 따로 정했습니다.',
rewardExp: 35
},
{
id: 'set-lucheng-pressure-rhythm',
label: '노성 압박 호흡을 맞춘다',
response: '위연은 성급한 돌파 대신 강유의 정찰 보고가 닿는 박자에 맞춰 전열을 흔들기로 했습니다.',
rewardExp: 32
}
]
},
{
id: 'northern-eighth-zhaoyun-wangping-return',
title: '회수선의 표식',
availableAfterBattleIds: [campBattleIds.sixtySecond],
unitIds: ['zhao-yun', 'wang-ping'],
bondId: 'zhao-yun__wang-ping_northern-eighth',
rewardExp: 98,
lines: [
'조운: 장합의 기병은 길이 열린 순간보다 돌아가는 순간을 노렸습니다.',
'왕평: 그래서 표식은 앞에만 세우면 안 됩니다. 돌아오는 병사가 어둠 속에서도 볼 수 있어야 합니다.',
'조운: 다음에는 예비대가 표식을 따라 먼저 움직이겠습니다. 병사들이 표식을 보면 제 깃발도 함께 보게 하겠습니다.'
],
choices: [
{
id: 'pair-reserve-with-road-markers',
label: '예비대와 표식을 묶는다',
response: '왕평은 산길 표식을 다시 세웠고, 조운은 예비대가 어느 표식을 보면 즉시 움직일지 훈련 명령에 적었습니다.',
rewardExp: 34
},
{
id: 'test-zhanghe-pursuit-drill',
label: '장합 추격 대비를 훈련한다',
response: '조운의 기동대가 일부러 늦게 물러났고, 왕평은 그 길목마다 병사들이 헷갈린 지점을 따로 고쳤습니다.',
rewardExp: 31
}
]
},
{
id: 'northern-eighth-huangquan-maliang-ledger',
title: '장부와 고시문',
availableAfterBattleIds: [campBattleIds.sixtySecond],
unitIds: ['huang-quan', 'ma-liang'],
bondId: 'huang-quan__ma-liang_northern-eighth',
rewardExp: 96,
lines: [
'황권: 공세가 성공해도 장부가 비면 마을은 승리보다 징발을 먼저 기억합니다.',
'마량: 그래서 고시문은 승전 뒤가 아니라 출정 전부터 준비되어야 합니다. 약속이 늦으면 민심도 늦습니다.',
'황권: 다음 장부에는 군량 수치 옆에 고시문 날짜를 함께 적겠습니다.'
],
choices: [
{
id: 'bind-ledger-to-proclamation',
label: '장부와 고시문 날짜를 묶는다',
response: '황권은 군량 배분표 옆에 마량의 고시문 날짜를 적었고, 마량은 약속한 배분이 실제 수레에 실리는지 확인했습니다.',
rewardExp: 33
},
{
id: 'prepare-front-village-reassurance',
label: '전방 마을 안심 문서를 준비한다',
response: '마량은 노성 앞 마을에 붙일 문서를 고쳤고, 황권은 그 문서가 지킬 수 있는 군량 수치를 함께 봉했습니다.',
rewardExp: 30
}
]
}
];
@@ -9341,6 +9459,36 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['쌀 34', '상처약 24']
}
]
},
{
id: 'northern-eighth-lucheng-supply-table',
title: '노성 앞 보급 탁자',
location: '기산 재출정 군막',
availableAfterBattleIds: [campBattleIds.sixtySecond],
bondId: 'huang-quan__ma-liang_northern-eighth',
description: '황권과 마량이 노성 앞 전진로, 마을 안심 약속, 다음 북벌의 실제 군량 여유를 하나의 탁자 위에서 맞춥니다.',
lines: [
'마량: 전투가 이긴 뒤에 붙이는 고시문은 늦습니다. 마을은 군량이 지나간 자리를 먼저 봅니다.',
'황권: 그렇다면 장부의 첫 줄을 병사 몫이 아니라 지나갈 마을 몫으로 두겠습니다.',
'군막의 탁자에는 노성 앞 길과 회수 진영, 전방 마을의 약속이 한 장의 지도로 묶였습니다.'
],
choices: [
{
id: 'seal-lucheng-supply-ledger',
label: '기산 재출정 장부를 봉한다',
response: '황권은 노성 앞에서 쓴 군량과 남은 수레를 봉했고, 마량은 그 장부를 다음 고시문 기준으로 삼았습니다.',
bondExp: 82,
itemRewards: ['기산 재출정 장부 1']
},
{
id: 'reserve-front-village-grain',
label: '전방 마을 예비 군량을 남긴다',
response: '마량은 마을 이름을 하나씩 불렀고, 황권은 그 이름 옆에 실제로 남길 군량 숫자를 적었습니다.',
bondExp: 76,
gold: 2860,
itemRewards: ['쌀 36', '상처약 26']
}
]
}
];
@@ -9450,7 +9598,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fiftyEighth ||
battleId === campBattleIds.fiftyNinth ||
battleId === campBattleIds.sixtieth ||
battleId === campBattleIds.sixtyFirst
battleId === campBattleIds.sixtyFirst ||
battleId === campBattleIds.sixtySecond
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -9471,7 +9620,7 @@ export class CampScene extends Phaser.Scene {
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits,
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? jiangWeiRecruitUnits : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? jiangWeiRecruitUnits : [])
],
[
...xuzhouRecruitBonds,
@@ -9511,12 +9660,13 @@ export class CampScene extends Phaser.Scene {
...mengHuoFinalCaptureBonds,
...northernFirstBattleBonds,
...northernSecondBattleBonds,
...(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 : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? jiangWeiRecruitBonds : []),
...(battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernFourthBattleBonds : []),
...(battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernFifthBattleBonds : []),
...(battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernSixthBattleBonds : []),
...(battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernSeventhBattleBonds : []),
...(battleId === campBattleIds.sixtySecond ? northernEighthBattleBonds : [])
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -10157,6 +10307,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'northern-campaign-prep-camp') {
return '북벌 준비 군영';
}
if (battleId === campBattleIds.sixtySecond) {
return '기산 재출정 후 군영';
}
if (battleId === campBattleIds.sixtyFirst) {
return '한중 우로 방어 후 군영';
}

View File

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