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

@@ -3695,6 +3695,59 @@ export const fiftySeventhBattleVictoryPages: StoryPage[] = [
}
];
export const fiftyEighthBattleIntroPages: StoryPage[] = [
{
id: 'fifty-eighth-qishan-retreat-council',
bgm: 'story-dark',
chapter: '기산 퇴각 명령',
background: 'story-militia',
speaker: '제갈량',
text: '가정의 물길은 가까스로 지켰지만 북방의 군량 길은 이미 오래 늘어졌습니다. 지금 억지로 깊게 밀고 들어가면 승리가 아니라 고립을 얻게 됩니다. 기산으로 돌아가는 수레 길을 열고, 병사와 장부를 먼저 살려야 합니다.'
},
{
id: 'fifty-eighth-jiang-wei-first-sortie',
bgm: 'battle-prep',
chapter: '강유의 첫 출전',
background: 'story-sortie',
speaker: '강유',
text: '제가 알던 천수의 낮은 길이 이제 촉한의 퇴로가 되었습니다. 아직 모두가 저를 믿지는 못하겠지만, 오늘은 말보다 길로 증명하겠습니다. 조운과 마대가 회수 길을 지키고, 왕평이 물길을 보면 위군의 추격을 끊을 수 있습니다.'
},
{
id: 'fifty-eighth-sima-yi-shadow',
bgm: 'story-dark',
chapter: '새로운 그림자',
background: 'story-three-heroes',
speaker: '마량',
text: '장합의 추격만 막으면 끝나는 전장이 아닙니다. 위군 본영 뒤에서 사마의가 군량 수레가 어디로 빠지는지 보고 있습니다. 강유를 내보내면 길은 넓어지지만, 누군가는 조운이나 왕평의 자리를 양보해야 합니다.'
}
];
export const fiftyEighthBattleVictoryPages: StoryPage[] = [
{
id: 'fifty-eighth-victory-retreat-road',
bgm: 'militia-theme',
chapter: '기산 퇴로 확보',
background: 'story-sortie',
text: '촉한군은 군량 수레를 잃지 않고 기산의 낮은 길로 물러났습니다. 장합의 추격은 물길 앞에서 꺾였고, 사마의의 본대는 더 깊게 들어오지 못했습니다. 패배를 승리로 꾸밀 수는 없지만, 다음 북벌을 위한 병사와 장부는 지켜냈습니다.'
},
{
id: 'fifty-eighth-victory-jiang-wei-reviewed',
bgm: 'story-dark',
chapter: '강유의 첫 평가',
background: 'story-militia',
speaker: '제갈량',
text: '강유는 천수의 길을 숨기지 않았고, 조운과 마대는 병사들이 돌아올 시간을 벌었습니다. 왕평의 물길 판단도 여전히 군의 목숨줄입니다. 이제부터 출전 명단은 단순히 강한 장수를 고르는 일이 아니라, 어떤 길을 택할지 정하는 일이 됩니다.'
},
{
id: 'fifty-eighth-next-northern-plan',
bgm: 'story-dark',
chapter: '다음 북벌을 위하여',
background: 'story-three-heroes',
speaker: '강유',
text: '이번 후퇴로 저는 촉한의 군율과 병사들의 숨을 배웠습니다. 다음에는 물러나는 길이 아니라 나아가는 길을 열겠습니다. 승상께서 다시 북쪽을 보실 때, 제가 먼저 그 길을 살피겠습니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -4182,6 +4235,12 @@ export const fiftySeventhBattleMap: BattleMap = {
terrain: createFiftySeventhBattleTerrain()
};
export const fiftyEighthBattleMap: BattleMap = {
width: 116,
height: 98,
terrain: createFiftyEighthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -17126,6 +17185,97 @@ export const fiftySeventhBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('northern-third-leader-zhang-he', '장합', '위군 역습 총대장', 'rebelLeader', 187, 1400, 332, 109, 24)
];
const fiftyEighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'guan-yu': { x: 20, y: 85 },
'zhang-fei': { x: 26, y: 86 },
'jian-yong': { x: 10, y: 92 },
'mi-zhu': { x: 14, y: 93 },
'sun-qian': { x: 19, y: 93 },
'zhao-yun': { x: 32, y: 82 },
'zhuge-liang': { x: 18, y: 81 },
'ma-liang': { x: 23, y: 78 },
'yi-ji': { x: 9, y: 95 },
'gong-zhi': { x: 27, y: 93 },
'huang-zhong': { x: 38, y: 81 },
'wei-yan': { x: 44, y: 84 },
'pang-tong': { x: 15, y: 77 },
'fa-zheng': { x: 27, y: 76 },
'wu-yi': { x: 45, y: 91 },
'yan-yan': { x: 49, y: 84 },
'li-yan': { x: 35, y: 75 },
'huang-quan': { x: 12, y: 80 },
'ma-chao': { x: 52, y: 80 },
'ma-dai': { x: 56, y: 87 },
'wang-ping': { x: 40, y: 75 },
'jiang-wei': { x: 33, y: 78 }
};
export const fiftyEighthBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.id !== 'liu-bei'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits,
...chengduSurrenderRecruitUnits,
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits,
...jiangWeiRecruitUnits
].map((unit) => placeScenarioUnit(unit, fiftyEighthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('northern-fourth-scout-a', '위군 퇴로 정찰병', '서쪽 물길 정찰', 'bandit', 186, 920, 262, 47, 76),
createThirtyFirstEnemyUnit('northern-fourth-scout-b', '위군 퇴로 정찰병', '기산 수레 길 정찰', 'bandit', 186, 920, 262, 58, 73),
createThirtyFirstEnemyUnit('northern-fourth-scout-c', '위군 퇴로 정찰병', '중앙 낮은 길 정찰', 'bandit', 187, 932, 264, 70, 66),
createThirtyFirstEnemyUnit('northern-fourth-scout-d', '위군 퇴로 정찰병', '동쪽 강변 정찰', 'bandit', 187, 932, 264, 83, 58),
createThirtyFirstEnemyUnit('northern-fourth-scout-e', '위군 퇴로 정찰병', '사마의 본영 정찰', 'bandit', 188, 944, 266, 99, 44),
createThirtyFirstEnemyUnit('northern-fourth-scout-f', '위군 퇴로 정찰병', '북쪽 능선 정찰', 'bandit', 188, 944, 266, 111, 29),
createThirtyFirstEnemyUnit('northern-fourth-infantry-a', '위군 퇴로 봉쇄병', '서쪽 길목 봉쇄', 'yellowTurban', 187, 1280, 312, 52, 72),
createThirtyFirstEnemyUnit('northern-fourth-infantry-b', '위군 퇴로 봉쇄병', '수레 길 봉쇄', 'yellowTurban', 187, 1280, 312, 62, 68),
createThirtyFirstEnemyUnit('northern-fourth-infantry-c', '위군 퇴로 봉쇄병', '중앙 낮은 길 봉쇄', 'yellowTurban', 188, 1300, 316, 74, 61),
createThirtyFirstEnemyUnit('northern-fourth-infantry-d', '위군 퇴로 봉쇄병', '물길 앞 봉쇄', 'yellowTurban', 188, 1300, 316, 86, 55),
createThirtyFirstEnemyUnit('northern-fourth-infantry-e', '위군 퇴로 봉쇄병', '동쪽 고개 봉쇄', 'yellowTurban', 189, 1320, 320, 98, 48),
createThirtyFirstEnemyUnit('northern-fourth-infantry-f', '위군 퇴로 봉쇄병', '사마의 본영 보병', 'yellowTurban', 189, 1320, 320, 108, 34),
createThirtyFirstEnemyUnit('northern-fourth-infantry-g', '위군 퇴로 봉쇄병', '군량 차단 보병', 'yellowTurban', 190, 1340, 324, 93, 73),
createThirtyFirstEnemyUnit('northern-fourth-infantry-h', '위군 퇴로 봉쇄병', '북쪽 능선 보병', 'yellowTurban', 190, 1340, 324, 111, 21),
createThirtyFirstEnemyUnit('northern-fourth-archer-a', '위군 추격 궁병', '서쪽 물길 궁병', 'archer', 187, 968, 274, 56, 64),
createThirtyFirstEnemyUnit('northern-fourth-archer-b', '위군 추격 궁병', '수레 길 궁병', 'archer', 187, 968, 274, 68, 59),
createThirtyFirstEnemyUnit('northern-fourth-archer-c', '위군 추격 궁병', '중앙 언덕 궁병', 'archer', 188, 980, 276, 82, 51),
createThirtyFirstEnemyUnit('northern-fourth-archer-d', '위군 추격 궁병', '강변 궁병', 'archer', 189, 992, 278, 94, 43),
createThirtyFirstEnemyUnit('northern-fourth-archer-e', '위군 추격 궁병', '사마의 본영 궁병', 'archer', 189, 992, 278, 106, 31),
createThirtyFirstEnemyUnit('northern-fourth-archer-f', '위군 추격 궁병', '북쪽 능선 궁병', 'archer', 190, 1004, 280, 112, 18),
createThirtyFirstEnemyUnit('northern-fourth-cavalry-a', '장합 추격 기병', '서쪽 추격 기병', 'cavalry', 188, 1328, 326, 48, 66),
createThirtyFirstEnemyUnit('northern-fourth-cavalry-b', '장합 추격 기병', '수레 길 추격 기병', 'cavalry', 188, 1328, 326, 62, 63),
createThirtyFirstEnemyUnit('northern-fourth-cavalry-c', '장합 추격 기병', '중앙 돌파 기병', 'cavalry', 189, 1350, 330, 76, 56),
createThirtyFirstEnemyUnit('northern-fourth-cavalry-d', '장합 추격 기병', '동쪽 우회 기병', 'cavalry', 190, 1372, 334, 90, 49),
createThirtyFirstEnemyUnit('northern-fourth-cavalry-e', '장합 추격 기병', '사마의 본영 기병', 'cavalry', 190, 1372, 334, 103, 41),
createThirtyFirstEnemyUnit('northern-fourth-cavalry-f', '장합 추격 기병', '북쪽 추격 기병', 'cavalry', 191, 1394, 338, 112, 25),
createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-a', '기산 능선 수비대', '서쪽 능선 수비', 'yellowTurban', 188, 1300, 318, 43, 52),
createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-b', '기산 능선 수비대', '중앙 능선 수비', 'yellowTurban', 188, 1300, 318, 57, 45),
createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-c', '기산 능선 수비대', '물길 위 수비', 'yellowTurban', 189, 1320, 322, 73, 37),
createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-d', '기산 능선 수비대', '동쪽 능선 수비', 'yellowTurban', 189, 1320, 322, 88, 30),
createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-e', '기산 능선 수비대', '본영 앞 수비', 'yellowTurban', 190, 1340, 326, 101, 23),
createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-f', '기산 능선 수비대', '북쪽 절벽 수비', 'yellowTurban', 190, 1340, 326, 113, 16),
createThirtyFirstEnemyUnit('northern-fourth-strategist-a', '위군 퇴로 군리', '수레 길 계산 군리', 'strategist', 188, 1032, 286, 70, 53),
createThirtyFirstEnemyUnit('northern-fourth-strategist-b', '위군 퇴로 군리', '물길 기록 군리', 'strategist', 189, 1044, 288, 84, 44),
createThirtyFirstEnemyUnit('northern-fourth-strategist-c', '위군 퇴로 군리', '사마의 본영 군리', 'strategist', 189, 1044, 288, 98, 36),
createThirtyFirstEnemyUnit('northern-fourth-strategist-d', '위군 퇴로 군리', '군량 차단 군리', 'strategist', 190, 1056, 290, 104, 61),
createThirtyFirstEnemyUnit('northern-fourth-officer-zhang-he', '장합', '퇴로 추격 선봉장', 'rebelLeader', 192, 1440, 342, 94, 38),
createThirtyFirstEnemyUnit('northern-fourth-officer-fei-yao', '비요', '수레 길 압박 장수', 'rebelLeader', 190, 1400, 334, 102, 54),
createThirtyFirstEnemyUnit('northern-fourth-officer-guo-huai', '곽회', '가정 방면 장수', 'rebelLeader', 190, 1400, 334, 108, 66),
createThirtyFirstEnemyUnit('northern-fourth-officer-supply', '위군 보급 차단장', '군량 수레 차단 장수', 'rebelLeader', 189, 1380, 330, 78, 72),
createThirtyFirstEnemyUnit('northern-fourth-officer-ridge', '위군 능선 장수', '북쪽 능선 장수', 'rebelLeader', 190, 1400, 334, 110, 19),
createThirtyFirstEnemyUnit('northern-fourth-leader-sima-yi', '사마의', '위군 본대 총지휘관', 'rebelLeader', 193, 1500, 352, 112, 27)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -18323,6 +18473,46 @@ export const fiftySeventhBattleBonds: BattleBond[] = [
...northernThirdBattleBonds
].map(cloneBattleBondForScenario);
export const northernFourthBattleBonds: BattleBond[] = [
{
id: 'zhuge-liang__jiang-wei_northern-fourth',
unitIds: ['zhuge-liang', 'jiang-wei'],
title: '새 길을 맡긴 마음',
level: 48,
exp: 0,
description: '제갈량의 큰 계산과 강유의 천수 길 지식이 맞물리면 퇴각전에서도 군량 수레와 병사를 함께 살릴 수 있습니다.'
},
{
id: 'jiang-wei__wang-ping_northern-fourth',
unitIds: ['jiang-wei', 'wang-ping'],
title: '낮은 길과 물길',
level: 44,
exp: 0,
description: '강유가 아는 낮은 길과 왕평의 물길 판단이 합쳐지면 추격을 끊고 안전한 퇴로를 열 확률이 높아집니다.'
},
{
id: 'jiang-wei__zhao-yun_northern-fourth',
unitIds: ['jiang-wei', 'zhao-yun'],
title: '새 장수와 백마',
level: 42,
exp: 0,
description: '강유의 길 안내와 조운의 안정된 회수 기동이 어울리면 빠르게 들어온 위군 기병을 늦출 수 있습니다.'
},
{
id: 'ma-dai__jiang-wei_northern-fourth',
unitIds: ['ma-dai', 'jiang-wei'],
title: '서량 말과 천수 길',
level: 40,
exp: 0,
description: '마대의 서량 기병 감각과 강유의 천수 지형 지식이 맞물리면 좁은 길에서도 추격을 흩어 놓을 수 있습니다.'
}
];
export const fiftyEighthBattleBonds: BattleBond[] = [
...fiftySeventhBattleBonds,
...northernFourthBattleBonds
].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
Array.from({ length: 26 }, (_, x): TerrainType => {
@@ -21415,6 +21605,82 @@ function createFiftySeventhBattleTerrain(): TerrainType[][] {
);
}
function createFiftyEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 98 }, (_, y) =>
Array.from({ length: 116 }, (_, x): TerrainType => {
if (
(x >= 36 && x <= 43 && y >= 0 && y <= 97) ||
(x >= 64 && x <= 71 && y >= 18 && y <= 97) ||
(x >= 0 && x <= 115 && y >= 88)
) {
return 'river';
}
if (
(x >= 6 && x <= 76 && y >= 82 && y <= 87) ||
(x >= 18 && x <= 88 && y >= 70 && y <= 78) ||
(x >= 52 && x <= 102 && y >= 58 && y <= 67) ||
(x >= 72 && x <= 110 && y >= 43 && y <= 53) ||
(x >= 88 && x <= 115 && y >= 29 && y <= 39) ||
(x >= 24 && x <= 32 && y >= 64 && y <= 92) ||
(x >= 48 && x <= 56 && y >= 50 && y <= 84) ||
(x >= 78 && x <= 86 && y >= 36 && y <= 72)
) {
return 'road';
}
if (
(x >= 0 && x <= 38 && y >= 55 && y <= 86) ||
(x >= 46 && x <= 78 && y >= 67 && y <= 88) ||
(x >= 88 && x <= 115 && y >= 59 && y <= 84) ||
(x >= 12 && x <= 34 && y >= 38 && y <= 56)
) {
return 'forest';
}
if (
(x >= 0 && x <= 44 && y <= 34) ||
(x >= 44 && x <= 86 && y <= 28) ||
(x >= 84 && x <= 115 && y <= 45) ||
(x >= 96 && x <= 115 && y >= 38 && y <= 64)
) {
return 'hill';
}
if (
(x <= 2 && y <= 88) ||
(x >= 113 && y <= 82) ||
(x >= 32 && x <= 37 && y >= 0 && y <= 58) ||
(x >= 72 && x <= 78 && y >= 0 && y <= 40) ||
(x >= 58 && x <= 64 && y >= 78 && y <= 97)
) {
return 'cliff';
}
if (
(x >= 9 && x <= 24 && y >= 78 && y <= 88) ||
(x >= 42 && x <= 58 && y >= 66 && y <= 77) ||
(x >= 75 && x <= 91 && y >= 51 && y <= 63) ||
(x >= 96 && x <= 110 && y >= 36 && y <= 48)
) {
return 'village';
}
if (
(x >= 18 && x <= 40 && y >= 72 && y <= 84) ||
(x >= 52 && x <= 74 && y >= 58 && y <= 70) ||
(x >= 84 && x <= 104 && y >= 43 && y <= 56) ||
(x >= 92 && x <= 112 && y >= 26 && y <= 38)
) {
return 'camp';
}
if (
(x >= 47 && x <= 63 && y >= 40 && y <= 54) ||
(x >= 70 && x <= 90 && y >= 30 && y <= 44) ||
(x >= 92 && x <= 110 && y >= 44 && y <= 58) ||
(x >= 99 && x <= 114 && y >= 14 && y <= 28)
) {
return 'fort';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),