Add Meng Huo second capture battle

This commit is contained in:
2026-06-23 21:33:26 +09:00
parent 688b51f3cc
commit f4c50c7fca
11 changed files with 822 additions and 58 deletions

View File

@@ -48,6 +48,10 @@ import {
fortyEighthBattleMap,
fortyEighthBattleUnits,
fortyEighthBattleVictoryPages,
fortyNinthBattleBonds,
fortyNinthBattleMap,
fortyNinthBattleUnits,
fortyNinthBattleVictoryPages,
fortySixthBattleBonds,
fortySixthBattleMap,
fortySixthBattleUnits,
@@ -245,7 +249,8 @@ export type BattleScenarioId =
| 'forty-fifth-battle-yiling-vanguard'
| 'forty-sixth-battle-yiling-fire'
| 'forty-seventh-battle-nanzhong-stabilization'
| 'forty-eighth-battle-meng-huo-main-force';
| 'forty-eighth-battle-meng-huo-main-force'
| 'forty-ninth-battle-meng-huo-second-capture';
export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -3117,6 +3122,66 @@ export const fortyEighthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const fortyNinthBattleScenario: BattleScenarioDefinition = {
id: 'forty-ninth-battle-meng-huo-second-capture',
title: '칠종칠금 2차전',
victoryConditionLabel: '맹획 재생포와 마을 회유',
defeatConditionLabel: '제갈량 퇴각',
openingObjectiveLines: [
'맹획은 한 번 풀려났지만 곧바로 다시 산채를 모았습니다. 이번 싸움의 목적도 참수가 아니라 두 번째 생포와 더 넓은 회유 명분입니다.',
'강가 늪지와 산채 길목에 매복이 많습니다. 포로장과 마을을 지키면 맹획을 돌려보낸 뒤에도 남중 사람들이 촉한군의 군율을 기억할 것입니다.',
'제갈량은 반드시 생존해야 합니다. 왕평으로 산길을 읽고, 황권으로 포로 장부를 지키며, 조운과 황충으로 맹수 돌격과 고지 궁병을 동시에 억제하십시오.'
],
map: fortyNinthBattleMap,
units: fortyNinthBattleUnits,
bonds: fortyNinthBattleBonds,
mapTextureKey: 'battle-map-forty-ninth',
leaderUnitId: 'menghuo-second-leader',
quickVictoryTurnLimit: 86,
baseVictoryGold: 11140,
objectives: [
{
id: 'pacify-meng-huo-second',
kind: 'pacify-leader',
label: '맹획 두 번째 생포',
rewardGold: 7600,
unitId: 'menghuo-second-leader'
},
{
id: 'zhuge-liang',
kind: 'keep-unit-alive',
label: '제갈량 생존',
rewardGold: 1980,
unitId: 'zhuge-liang'
},
{
id: 'amnesty-villages',
kind: 'secure-terrain',
label: '회유 마을 유지',
rewardGold: 3740,
terrain: 'village'
},
{
id: 'captive-camps',
kind: 'secure-terrain',
label: '포로장 군율 유지',
rewardGold: 3020,
terrain: 'camp'
},
{
id: 'quick',
kind: 'quick-victory',
label: '86턴 이내 재생포',
rewardGold: 2520,
maxTurn: 86
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
itemRewards: ['콩 58', '상처약 44', '탁주 19', '칠종칠금 회유문 1', '포로 안심 장부 1'],
victoryPages: fortyNinthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -3167,7 +3232,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario,
'forty-sixth-battle-yiling-fire': fortySixthBattleScenario,
'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario,
'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario
'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario,
'forty-ninth-battle-meng-huo-second-capture': fortyNinthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -11,6 +11,7 @@ import {
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortyEighthBattleScenario,
fortyNinthBattleScenario,
fortySixthBattleScenario,
fortySeventhBattleScenario,
fifthBattleScenario,
@@ -72,6 +73,8 @@ import {
fortyFifthBattleVictoryPages,
fortyEighthBattleIntroPages,
fortyEighthBattleVictoryPages,
fortyNinthBattleIntroPages,
fortyNinthBattleVictoryPages,
fortySixthBattleIntroPages,
fortySeventhBattleIntroPages,
fortySeventhBattleVictoryPages,
@@ -703,13 +706,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[fortyEighthBattleScenario.id]: {
afterBattleId: fortyEighthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '칠종칠금의 첫 장',
eyebrow: '다음 장',
title: fortyNinthBattleScenario.title,
description:
'맹획을 처음 생포하고 돌려보낸 뒤, 남중의 호족들은 촉한군의 방식이 단순 토벌이 아님을 보기 시작했습니다. 다음 장에서는 풀려난 맹획 다시 세운 진영과, 그를 완전히 설득하기 위한 반복 회유 흐름을 다룹니다.',
rewardHint: '다음 장: 칠종칠금 회유전 준비 중',
pages: fortyEighthBattleVictoryPages,
unavailableNotice: '칠종칠금 회유전은 다음 작업에서 이어집니다. 지금은 맹획 본대전 뒤의 공명도, 포로 장부, 회유 선택지를 정비할 수 있습니다.'
'맹획을 처음 생포하고 돌려보낸 뒤, 남중의 호족들은 촉한군의 방식이 단순 토벌이 아님을 보기 시작했습니다. 풀려난 맹획 다시 산채를 모으지만, 이번에는 마을과 포로장이 촉한군의 군율을 증언할 수 있습니다.',
rewardHint: `예상 보상: ${fortyNinthBattleScenario.title} 개방 / 칠종칠금 회유문`,
nextBattleId: fortyNinthBattleScenario.id,
campaignStep: 'forty-ninth-battle',
pages: [...fortyEighthBattleVictoryPages, ...fortyNinthBattleIntroPages]
},
[fortyNinthBattleScenario.id]: {
afterBattleId: fortyNinthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '남중 회유의 확장',
description:
'맹획은 두 번째로 붙잡혔지만 아직 남중 전체가 굴복한 것은 아닙니다. 이제는 맹획 개인의 고집을 꺾는 것과 동시에, 호족과 마을이 어떤 조건에서 촉한의 질서를 받아들일지 다루어야 합니다.',
rewardHint: '다음 장: 칠종칠금 3차전과 남중 호족 선택지 준비 중',
pages: fortyNinthBattleVictoryPages,
unavailableNotice: '칠종칠금 3차전은 다음 작업에서 이어집니다. 지금은 두 번째 생포 뒤의 회유 공명, 포로 안심 장부, 남중 마을 선택지를 정비할 수 있습니다.'
}
};

View File

@@ -3191,6 +3191,59 @@ export const fortyEighthBattleVictoryPages: StoryPage[] = [
}
];
export const fortyNinthBattleIntroPages: StoryPage[] = [
{
id: 'forty-ninth-meng-huo-returns',
bgm: 'story-dark',
chapter: '돌아온 맹획',
background: 'story-militia',
speaker: '왕평',
text: '맹획은 풀려나자마자 다시 산채를 모았습니다. 그러나 지난 싸움과 달리 마을 사람들은 촉한군이 약탈하지 않았다는 사실을 알고 있습니다. 이번 싸움은 그 마음의 흔들림을 전장 위에서 붙잡는 일입니다.'
},
{
id: 'forty-ninth-second-capture-plan',
bgm: 'battle-prep',
chapter: '두 번째 생포',
background: 'story-three-heroes',
speaker: '제갈량',
text: '이번에도 베어서는 안 됩니다. 강가의 늪지와 산채 길목을 끊어 맹획을 다시 생포하고, 마을과 포로장을 지켜 그가 돌아갈 때 들을 말까지 남겨 두십시오.'
},
{
id: 'forty-ninth-sortie',
bgm: 'battle-prep',
chapter: '칠종칠금 2차전',
background: 'story-sortie',
speaker: '마량',
text: '맹획은 이번에 매복과 주술사를 앞세웠습니다. 왕평이 길을 읽고, 황권이 포로 장부를 지키며, 조운과 황충이 돌격과 고지 궁병을 끊어야 합니다. 회유의 말은 전열이 버텨야 닿습니다.'
}
];
export const fortyNinthBattleVictoryPages: StoryPage[] = [
{
id: 'forty-ninth-victory-second-capture',
bgm: 'militia-theme',
chapter: '두 번째 생포',
background: 'story-sortie',
text: '맹획은 다시 포위되었고, 이번에도 촉한군은 진영을 불태우지 않았습니다. 포로장은 닫혔지만 약탈은 없었고, 마을 사람들은 어느 군대가 오래 남을 수 있는지를 보기 시작했습니다.'
},
{
id: 'forty-ninth-victory-release-again',
bgm: 'story-dark',
chapter: '다시 풀어 보내다',
background: 'story-three-heroes',
speaker: '제갈량',
text: '두 번 잡아도 마음이 굽지 않는다면 다시 놓아 보낼 뿐입니다. 다만 이번에는 그가 돌아가는 길마다 촉한군이 지킨 마을과 포로들이 그의 말을 시험할 것입니다.'
},
{
id: 'forty-ninth-next-southern-minds',
bgm: 'story-dark',
chapter: '남중의 마음',
background: 'story-militia',
speaker: '황권',
text: '포로 명부와 군량 장부가 어긋나지 않았습니다. 다음 싸움에서는 맹획만이 아니라 그를 따르는 호족들의 마음도 흔들릴 것입니다. 회유는 이제 전투 뒤의 일이 아니라 전투 중의 목표가 되었습니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -3624,6 +3677,12 @@ export const fortyEighthBattleMap: BattleMap = {
terrain: createFortyEighthBattleTerrain()
};
export const fortyNinthBattleMap: BattleMap = {
width: 98,
height: 80,
terrain: createFortyNinthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -15762,6 +15821,86 @@ export const fortyEighthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('menghuo-leader', '맹획', '남만왕', 'rebelLeader', 138, 940, 232, 90, 38)
];
const fortyNinthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'guan-yu': { x: 12, y: 59 },
'zhang-fei': { x: 17, y: 61 },
'jian-yong': { x: 8, y: 65 },
'mi-zhu': { x: 10, y: 63 },
'sun-qian': { x: 14, y: 67 },
'zhao-yun': { x: 22, y: 56 },
'zhuge-liang': { x: 11, y: 56 },
'ma-liang': { x: 15, y: 53 },
'yi-ji': { x: 9, y: 70 },
'gong-zhi': { x: 18, y: 68 },
'huang-zhong': { x: 25, y: 57 },
'wei-yan': { x: 30, y: 61 },
'pang-tong': { x: 13, y: 50 },
'fa-zheng': { x: 19, y: 51 },
'wu-yi': { x: 32, y: 66 },
'yan-yan': { x: 35, y: 59 },
'li-yan': { x: 24, y: 49 },
'huang-quan': { x: 8, y: 54 },
'ma-chao': { x: 37, y: 56 },
'ma-dai': { x: 39, y: 61 },
'wang-ping': { x: 27, y: 52 }
};
export const fortyNinthBattleUnits: 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
].map((unit) => placeScenarioUnit(unit, fortyNinthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('menghuo-return-scout-a', '남만 재집결 척후', '강가 척후', 'bandit', 136, 586, 178, 43, 33),
createThirtyFirstEnemyUnit('menghuo-return-scout-b', '남만 재집결 척후', '늪지 척후', 'bandit', 136, 586, 178, 58, 31),
createThirtyFirstEnemyUnit('menghuo-return-scout-c', '남만 재집결 척후', '마을 척후', 'bandit', 137, 594, 180, 73, 48),
createThirtyFirstEnemyUnit('menghuo-return-scout-d', '남만 재집결 척후', '동쪽 척후', 'bandit', 137, 594, 180, 88, 62),
createThirtyFirstEnemyUnit('menghuo-return-vine-a', '등갑 재집결병', '습지 등갑병', 'yellowTurban', 137, 846, 214, 48, 41),
createThirtyFirstEnemyUnit('menghuo-return-vine-b', '등갑 재집결병', '산채 등갑병', 'yellowTurban', 138, 858, 216, 60, 44),
createThirtyFirstEnemyUnit('menghuo-return-vine-c', '등갑 재집결병', '마을 등갑병', 'yellowTurban', 138, 858, 216, 72, 54),
createThirtyFirstEnemyUnit('menghuo-return-vine-d', '등갑 재집결병', '본대 등갑병', 'yellowTurban', 139, 870, 218, 85, 47),
createThirtyFirstEnemyUnit('menghuo-return-vine-e', '등갑 재집결병', '후위 등갑병', 'yellowTurban', 139, 870, 218, 92, 38),
createThirtyFirstEnemyUnit('menghuo-return-archer-a', '남만 고지 궁병', '강변 궁병', 'archer', 136, 636, 188, 45, 25),
createThirtyFirstEnemyUnit('menghuo-return-archer-b', '남만 고지 궁병', '절벽 궁병', 'archer', 137, 644, 190, 59, 23),
createThirtyFirstEnemyUnit('menghuo-return-archer-c', '남만 고지 궁병', '마을 궁병', 'archer', 137, 644, 190, 70, 41),
createThirtyFirstEnemyUnit('menghuo-return-archer-d', '남만 고지 궁병', '늪지 궁병', 'archer', 138, 652, 192, 82, 58),
createThirtyFirstEnemyUnit('menghuo-return-archer-e', '남만 고지 궁병', '본영 궁병', 'archer', 138, 652, 192, 91, 51),
createThirtyFirstEnemyUnit('menghuo-return-beast-a', '맹수 재돌격대', '강가 맹수대', 'cavalry', 138, 862, 218, 53, 34),
createThirtyFirstEnemyUnit('menghuo-return-beast-b', '맹수 재돌격대', '늪지 맹수대', 'cavalry', 138, 862, 218, 66, 36),
createThirtyFirstEnemyUnit('menghuo-return-beast-c', '맹수 재돌격대', '산채 맹수대', 'cavalry', 139, 874, 220, 77, 49),
createThirtyFirstEnemyUnit('menghuo-return-beast-d', '맹수 재돌격대', '추격 맹수대', 'cavalry', 139, 874, 220, 89, 55),
createThirtyFirstEnemyUnit('menghuo-return-ambusher-a', '남만 재매복병', '덩굴 매복병', 'bandit', 137, 602, 182, 36, 47),
createThirtyFirstEnemyUnit('menghuo-return-ambusher-b', '남만 재매복병', '숲속 매복병', 'bandit', 138, 610, 184, 51, 50),
createThirtyFirstEnemyUnit('menghuo-return-ambusher-c', '남만 재매복병', '강가 매복병', 'bandit', 138, 610, 184, 64, 63),
createThirtyFirstEnemyUnit('menghuo-return-ambusher-d', '남만 재매복병', '후방 차단대', 'bandit', 139, 618, 186, 82, 70),
createThirtyFirstEnemyUnit('menghuo-return-ambusher-e', '남만 재매복병', '서쪽 차단대', 'bandit', 139, 618, 186, 31, 60),
createThirtyFirstEnemyUnit('menghuo-return-guard-a', '맹획 재집결 친위', '포로장 친위대', 'yellowTurban', 140, 894, 224, 69, 38),
createThirtyFirstEnemyUnit('menghuo-return-guard-b', '맹획 재집결 친위', '산채 친위대', 'yellowTurban', 140, 894, 224, 80, 44),
createThirtyFirstEnemyUnit('menghuo-return-guard-c', '맹획 재집결 친위', '본영 친위대', 'yellowTurban', 141, 906, 226, 90, 41),
createThirtyFirstEnemyUnit('menghuo-return-shaman-a', '남만 독안개 주술사', '늪지 주술사', 'strategist', 139, 676, 198, 61, 32),
createThirtyFirstEnemyUnit('menghuo-return-shaman-b', '남만 독안개 주술사', '강가 주술사', 'strategist', 140, 684, 200, 74, 45),
createThirtyFirstEnemyUnit('menghuo-return-shaman-c', '남만 독안개 주술사', '산채 주술사', 'strategist', 140, 684, 200, 84, 34),
createThirtyFirstEnemyUnit('menghuo-return-officer-meng-you', '맹우', '맹획의 아우', 'rebelLeader', 141, 922, 230, 73, 33),
createThirtyFirstEnemyUnit('menghuo-return-officer-duosi', '타사대왕', '독천 산채장', 'rebelLeader', 141, 922, 230, 79, 61),
createThirtyFirstEnemyUnit('menghuo-return-officer-mulu', '목록대왕', '맹수대장', 'rebelLeader', 142, 934, 232, 87, 50),
createThirtyFirstEnemyUnit('menghuo-second-leader', '맹획', '남만왕', 'rebelLeader', 144, 974, 240, 92, 39)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -16639,6 +16778,35 @@ export const mengHuoPacificationBonds: BattleBond[] = [
export const fortyEighthBattleBonds: BattleBond[] = [...fortySeventhBattleBonds, ...mengHuoPacificationBonds].map(
cloneBattleBondForScenario
);
export const mengHuoSecondCaptureBonds: BattleBond[] = [
{
id: 'zhuge-liang__huang-quan_second-capture',
unitIds: ['zhuge-liang', 'huang-quan'],
title: '회유 장부',
level: 45,
exp: 0,
description: '제갈량의 회유 방침과 황권의 포로 장부는 맹획을 다시 놓아 보내도 군율이 흔들리지 않게 만든다.'
},
{
id: 'ma-liang__wang-ping_second-capture',
unitIds: ['ma-liang', 'wang-ping'],
title: '마을 길잡이',
level: 41,
exp: 0,
description: '마량이 민심을 살피고 왕평이 산길을 끊어, 맹획이 돌아가는 길마다 촉한군의 질서를 보이게 한다.'
},
{
id: 'zhao-yun__huang-zhong_second-capture',
unitIds: ['zhao-yun', 'huang-zhong'],
title: '돌격과 고지',
level: 39,
exp: 0,
description: '조운은 맹수 돌격을 받쳐 세우고 황충은 고지 궁병을 억눌러, 생포망이 무너지지 않게 한다.'
}
];
export const fortyNinthBattleBonds: BattleBond[] = [...fortyEighthBattleBonds, ...mengHuoSecondCaptureBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -19088,6 +19256,68 @@ function createFortyEighthBattleTerrain(): TerrainType[][] {
);
}
function createFortyNinthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 80 }, (_, y) =>
Array.from({ length: 98 }, (_, x): TerrainType => {
if (
(x >= 0 && x <= 97 && y >= 72) ||
(x >= 56 && x <= 62 && y >= 24 && y <= 72) ||
(x >= 4 && x <= 25 && y >= 64 && y <= 79) ||
(x >= 73 && x <= 88 && y >= 54 && y <= 72)
) {
return 'river';
}
if (
(x >= 8 && x <= 91 && y >= 59 && y <= 65) ||
(x >= 25 && x <= 70 && y >= 39 && y <= 45) ||
(x >= 67 && x <= 92 && y >= 31 && y <= 36) ||
(x >= 34 && x <= 42 && y >= 36 && y <= 68)
) {
return 'road';
}
if (
(x >= 20 && x <= 68 && y >= 20 && y <= 70) ||
(x >= 52 && x <= 91 && y >= 25 && y <= 66) ||
(x >= 2 && x <= 34 && y >= 46 && y <= 70) ||
(x >= 74 && x <= 97 && y >= 38 && y <= 69)
) {
return 'forest';
}
if (
(x >= 0 && x <= 27 && y <= 35) ||
(x >= 31 && x <= 65 && y <= 25) ||
(x >= 68 && x <= 97 && y <= 33) ||
(x >= 84 && x <= 97 && y >= 31 && y <= 51)
) {
return 'hill';
}
if (
(x <= 2 && y <= 70) ||
(x >= 95 && y <= 66) ||
(x >= 28 && x <= 34 && y >= 0 && y <= 36) ||
(x >= 65 && x <= 71 && y >= 0 && y <= 30) ||
(x >= 40 && x <= 46 && y >= 60 && y <= 79)
) {
return 'cliff';
}
if (
(x >= 9 && x <= 23 && y >= 55 && y <= 67) ||
(x >= 36 && x <= 50 && y >= 40 && y <= 52) ||
(x >= 72 && x <= 87 && y >= 45 && y <= 57)
) {
return 'village';
}
if ((x >= 22 && x <= 35 && y >= 47 && y <= 58) || (x >= 60 && x <= 74 && y >= 52 && y <= 64)) {
return 'camp';
}
if ((x >= 46 && x <= 60 && y >= 28 && y <= 38) || (x >= 70 && x <= 88 && y >= 28 && y <= 41)) {
return 'fort';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -858,7 +858,14 @@ const unitTexture: Record<string, string> = {
'menghuo-officer-ahuinan': 'unit-rebel-leader',
'menghuo-officer-dongtuna': 'unit-rebel-leader',
'menghuo-officer-mangyachang': 'unit-rebel-leader',
'menghuo-leader': 'unit-rebel-leader'
'menghuo-leader': 'unit-rebel-leader',
'menghuo-return-shaman-a': 'unit-rebel-archer',
'menghuo-return-shaman-b': 'unit-rebel-archer',
'menghuo-return-shaman-c': 'unit-rebel-archer',
'menghuo-return-officer-meng-you': 'unit-rebel-leader',
'menghuo-return-officer-duosi': 'unit-rebel-leader',
'menghuo-return-officer-mulu': 'unit-rebel-leader',
'menghuo-second-leader': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -2290,7 +2297,40 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'menghuo-officer-ahuinan': 'guard',
'menghuo-officer-dongtuna': 'guard',
'menghuo-officer-mangyachang': 'aggressive',
'menghuo-leader': 'hold'
'menghuo-leader': 'hold',
'menghuo-return-scout-a': 'aggressive',
'menghuo-return-scout-b': 'aggressive',
'menghuo-return-scout-c': 'aggressive',
'menghuo-return-scout-d': 'aggressive',
'menghuo-return-vine-a': 'guard',
'menghuo-return-vine-b': 'guard',
'menghuo-return-vine-c': 'guard',
'menghuo-return-vine-d': 'guard',
'menghuo-return-vine-e': 'guard',
'menghuo-return-archer-a': 'hold',
'menghuo-return-archer-b': 'hold',
'menghuo-return-archer-c': 'hold',
'menghuo-return-archer-d': 'hold',
'menghuo-return-archer-e': 'hold',
'menghuo-return-beast-a': 'aggressive',
'menghuo-return-beast-b': 'aggressive',
'menghuo-return-beast-c': 'aggressive',
'menghuo-return-beast-d': 'aggressive',
'menghuo-return-ambusher-a': 'aggressive',
'menghuo-return-ambusher-b': 'aggressive',
'menghuo-return-ambusher-c': 'aggressive',
'menghuo-return-ambusher-d': 'aggressive',
'menghuo-return-ambusher-e': 'aggressive',
'menghuo-return-guard-a': 'guard',
'menghuo-return-guard-b': 'guard',
'menghuo-return-guard-c': 'guard',
'menghuo-return-shaman-a': 'hold',
'menghuo-return-shaman-b': 'hold',
'menghuo-return-shaman-c': 'hold',
'menghuo-return-officer-meng-you': 'aggressive',
'menghuo-return-officer-duosi': 'guard',
'menghuo-return-officer-mulu': 'aggressive',
'menghuo-second-leader': 'hold'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -12,6 +12,7 @@ import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battl
import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg';
import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg';
import fortyEighthBattleMapUrl from '../../assets/images/battle/forty-eighth-battle-map.svg';
import fortyNinthBattleMapUrl from '../../assets/images/battle/forty-ninth-battle-map.svg';
import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-battle-map.svg';
import fortySeventhBattleMapUrl from '../../assets/images/battle/forty-seventh-battle-map.svg';
import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
@@ -154,6 +155,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-forty-sixth', fortySixthBattleMapUrl);
this.load.image('battle-map-forty-seventh', fortySeventhBattleMapUrl);
this.load.image('battle-map-forty-eighth', fortyEighthBattleMapUrl);
this.load.image('battle-map-forty-ninth', fortyNinthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -42,6 +42,7 @@ import {
luoCastleRecruitUnits,
maichengIsolationBonds,
mengHuoPacificationBonds,
mengHuoSecondCaptureBonds,
nanzhongRecoveryBonds,
wulingRecruitBonds,
wulingRecruitUnits,
@@ -409,9 +410,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'forty-fifth-battle-yiling-vanguard',
'forty-sixth-battle-yiling-fire',
'forty-seventh-battle-nanzhong-stabilization',
'forty-eighth-battle-meng-huo-main-force'
'forty-eighth-battle-meng-huo-main-force',
'forty-ninth-battle-meng-huo-second-capture'
],
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유']
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금']
}
];
@@ -463,7 +465,8 @@ const campBattleIds = {
fortyFifth: 'forty-fifth-battle-yiling-vanguard',
fortySixth: 'forty-sixth-battle-yiling-fire',
fortySeventh: 'forty-seventh-battle-nanzhong-stabilization',
fortyEighth: 'forty-eighth-battle-meng-huo-main-force'
fortyEighth: 'forty-eighth-battle-meng-huo-main-force',
fortyNinth: 'forty-ninth-battle-meng-huo-second-capture'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1038,6 +1041,20 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'huang-zhong', reason: '고지 궁병과 주술사를 안정적으로 견제합니다.' }
],
note: '맹획 본대전은 제갈량이 필수이며, 맹획을 베기보다 생포해 돌려보낼 명분을 만드는 전투입니다. 회유, 포로 장부, 맹수 기병 대응을 균형 있게 고르십시오.'
},
'forty-ninth-battle-meng-huo-second-capture': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
recommended: [
{ unitId: 'zhuge-liang', reason: '두 번째 생포와 석방 명분을 유지하는 필수 지휘관입니다.' },
{ unitId: 'huang-quan', reason: '포로 안심 장부와 군율 유지로 회유의 신뢰를 쌓습니다.' },
{ unitId: 'wang-ping', reason: '늪지와 강가 우회로를 읽어 맹획의 재매복을 끊습니다.' },
{ unitId: 'ma-liang', reason: '마을 사람들에게 두 번째 석방의 뜻을 설명하는 연결 축입니다.' },
{ unitId: 'zhao-yun', reason: '맹수 재돌격을 받아 내고 생포망의 옆구리를 지킵니다.' },
{ unitId: 'huang-zhong', reason: '고지 궁병과 주술사를 억눌러 포위망이 흐트러지지 않게 합니다.' },
{ unitId: 'ma-chao', reason: '맹획의 측면 돌파와 재집결 기병을 강하게 묶어 둡니다.' }
],
note: '칠종칠금 2차전은 제갈량이 필수이며, 맹획을 다시 잡아도 마을과 포로장이 흔들리지 않게 지키는 회유전입니다. 산길 판단, 장부 관리, 고지 제압을 함께 고르십시오.'
}
};
@@ -5094,6 +5111,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 17
}
]
},
{
id: 'second-capture-zhuge-huangquan-ledger',
title: '두 번째 석방의 장부',
availableAfterBattleIds: [campBattleIds.fortyNinth],
unitIds: ['zhuge-liang', 'huang-quan'],
bondId: 'zhuge-liang__huang-quan_second-capture',
rewardExp: 60,
lines: [
'황권: 맹획을 두 번이나 놓아 보내면 군량을 쓰는 병사들이 먼저 의심할 수 있습니다.',
'제갈량: 그러니 장부가 필요합니다. 풀어 보내는 일이 방종이 아니라 남중 전체를 얻기 위한 군율임을 보여야 합니다.',
'황권: 포로 명부와 지급 군량을 함께 남기겠습니다. 다음에 그가 돌아와도 우리가 흔들리지 않았다는 증거가 될 것입니다.'
],
choices: [
{
id: 'record-second-release',
label: '석방 장부를 남긴다',
response: '황권은 맹획의 두 번째 석방과 포로 귀환 순서를 장부에 적었고, 제갈량은 장수들에게 그 뜻을 설명했습니다.',
rewardExp: 21
},
{
id: 'calm-officer-doubts',
label: '장수들의 의문을 달랜다',
response: '제갈량은 당장의 분노보다 오래 남을 질서를 택해야 한다고 말했고, 황권은 그 말을 군율 조항으로 정리했습니다.',
rewardExp: 20
}
]
},
{
id: 'second-capture-maliang-wangping-village-road',
title: '돌아가는 길의 말',
availableAfterBattleIds: [campBattleIds.fortyNinth],
unitIds: ['ma-liang', 'wang-ping'],
bondId: 'ma-liang__wang-ping_second-capture',
rewardExp: 56,
lines: [
'왕평: 맹획이 돌아가는 산길마다 우리가 지킨 마을이 있습니다. 그 길을 막지 말고 보게 해야 합니다.',
'마량: 마을 사람들이 직접 말하게 하자는 뜻이군요. 촉한군이 약탈하지 않았다는 사실은 글보다 빠르게 퍼질 것입니다.',
'왕평: 예. 다음 싸움의 길은 오늘 돌아가는 길에서 이미 열릴 수 있습니다.'
],
choices: [
{
id: 'escort-release-route',
label: '귀환 길을 보호한다',
response: '왕평은 맹획 일행이 지나갈 낮은 산길을 열었고, 마량은 마을마다 군령과 보호 표식을 남겼습니다.',
rewardExp: 19
},
{
id: 'let-villages-speak',
label: '마을의 말을 남긴다',
response: '마량은 촉한군이 값을 치르고 산 물자와 돌려보낸 포로의 이름을 마을 어른들이 직접 말하게 했습니다.',
rewardExp: 18
}
]
},
{
id: 'second-capture-zhaoyun-huangzhong-net',
title: '생포망을 조이는 법',
availableAfterBattleIds: [campBattleIds.fortyNinth],
unitIds: ['zhao-yun', 'huang-zhong'],
bondId: 'zhao-yun__huang-zhong_second-capture',
rewardExp: 56,
lines: [
'황충: 맹획의 군세는 더 거칠어졌지만, 고지 궁병을 눌러 두니 포위망이 오래 버텼소.',
'조운: 돌격을 받아도 끝까지 베지 않는 절제가 더 어려웠습니다.',
'황충: 다음에도 잡아야 한다면, 먼저 도망갈 길과 몰아넣을 길을 구분해야겠군.'
],
choices: [
{
id: 'tighten-capture-net',
label: '생포망을 다시 짠다',
response: '조운은 추격 금지선을 정했고, 황충은 적장이 빠져나갈 고지와 협곡을 표시했습니다.',
rewardExp: 19
},
{
id: 'pin-shaman-archers',
label: '주술사와 궁병을 묶는다',
response: '황충은 고지 궁병을 먼저 억누르는 순서를 정했고, 조운은 그 사이 기병이 파고들 길을 지켰습니다.',
rewardExp: 18
}
]
}
];
@@ -7164,6 +7262,65 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['맹획 회유 서신 1']
}
]
},
{
id: 'second-capture-release-ledger',
title: '석방 장부 확인',
location: '남중 포로장',
availableAfterBattleIds: [campBattleIds.fortyNinth],
bondId: 'zhuge-liang__huang-quan_second-capture',
description: '제갈량과 황권이 두 번째로 붙잡힌 맹획을 다시 놓아 보내기 전, 포로와 군량 장부를 공개해 군율을 확인합니다.',
lines: [
'황권: 이번에는 포로들이 직접 명부를 보게 하겠습니다. 숨기는 것이 없다는 사실이 돌아가는 길의 말이 됩니다.',
'제갈량: 좋습니다. 맹획도 보게 하십시오. 우리가 사람을 얻으려는지 땅을 빼앗으려는지, 그가 직접 판단하게 해야 합니다.',
'포로장 앞에는 석방 순서와 군량 지급량이 적힌 목패가 세워졌습니다.'
],
choices: [
{
id: 'open-captive-ledger',
label: '장부를 공개한다',
response: '황권은 포로 명부를 공개했고, 제갈량은 석방과 처벌의 기준이 군령에 따른 것임을 알렸습니다.',
bondExp: 46,
itemRewards: ['포로 안심 장부 1']
},
{
id: 'ration-returnees',
label: '귀환 군량을 나눈다',
response: '제갈량은 돌아가는 포로에게 최소한의 군량을 지급하게 했고, 황권은 지출을 빠짐없이 장부에 남겼습니다.',
bondExp: 44,
gold: 1480,
itemRewards: ['콩 14']
}
]
},
{
id: 'second-capture-village-route',
title: '귀환 산길 보호',
location: '남중 산마을 길목',
availableAfterBattleIds: [campBattleIds.fortyNinth],
bondId: 'ma-liang__wang-ping_second-capture',
description: '마량과 왕평이 맹획이 돌아갈 길목의 마을을 돌며 촉한군의 보호 표식과 우회로를 정리합니다.',
lines: [
'왕평: 이 길을 군대가 밟으면 밭이 무너집니다. 조금 돌아가더라도 낮은 둑길을 써야 합니다.',
'마량: 마을이 지켜졌다는 기억이 다음 회유의 시작입니다. 군령을 길목마다 붙이겠습니다.',
'마을 어른들은 촉한군이 남긴 보호 표식과 우회로 표식을 함께 확인했습니다.'
],
choices: [
{
id: 'protect-field-detour',
label: '밭길 우회로를 정한다',
response: '왕평은 병사들이 밟지 말아야 할 밭길을 표시했고, 마량은 우회 명령을 마을 사람 앞에서 읽었습니다.',
bondExp: 44,
itemRewards: ['남중 산길 지도 1']
},
{
id: 'post-amnesty-boards',
label: '회유 목패를 세운다',
response: '마량은 약탈 금지와 보호 약속을 적은 목패를 세웠고, 왕평은 맹획이 지나갈 길목을 비워 두었습니다.',
bondExp: 46,
itemRewards: ['칠종칠금 회유문 1']
}
]
}
];
@@ -7260,7 +7417,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fortyFifth ||
battleId === campBattleIds.fortySixth ||
battleId === campBattleIds.fortySeventh ||
battleId === campBattleIds.fortyEighth
battleId === campBattleIds.fortyEighth ||
battleId === campBattleIds.fortyNinth
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -7311,7 +7469,8 @@ export class CampScene extends Phaser.Scene {
...yilingVanguardBonds,
...yilingFireBonds,
...nanzhongRecoveryBonds,
...mengHuoPacificationBonds
...mengHuoPacificationBonds,
...mengHuoSecondCaptureBonds
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -7949,6 +8108,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'baidi-entrustment-camp') {
return '백제성 유탁 후 군영';
}
if (battleId === campBattleIds.fortyNinth) {
return '칠종칠금 2차전 후 군영';
}
if (battleId === campBattleIds.fortyEighth) {
return '맹획 본대전 후 군영';
}

View File

@@ -12,6 +12,7 @@ import {
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortyEighthBattleScenario,
fortyNinthBattleScenario,
fortySixthBattleScenario,
fortySeventhBattleScenario,
fifthBattleScenario,
@@ -396,6 +397,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'forty-sixth-camp' ||
campaign.step === 'forty-seventh-camp' ||
campaign.step === 'forty-eighth-camp' ||
campaign.step === 'forty-ninth-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp' ||
campaign.step === 'baidi-entrustment-camp'
@@ -644,6 +646,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'forty-ninth-battle') {
this.scene.start('BattleScene', { battleId: fortyNinthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -138,6 +138,8 @@ export type CampaignStep =
| 'forty-seventh-camp'
| 'forty-eighth-battle'
| 'forty-eighth-camp'
| 'forty-ninth-battle'
| 'forty-ninth-camp'
| 'hanzhong-king-camp'
| 'shu-han-foundation-camp'
| 'baidi-entrustment-camp';
@@ -294,7 +296,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'forty-fifth-battle-yiling-vanguard': { victory: 'forty-fifth-camp', retry: 'forty-fifth-battle' },
'forty-sixth-battle-yiling-fire': { victory: 'forty-sixth-camp', retry: 'forty-sixth-battle' },
'forty-seventh-battle-nanzhong-stabilization': { victory: 'forty-seventh-camp', retry: 'forty-seventh-battle' },
'forty-eighth-battle-meng-huo-main-force': { victory: 'forty-eighth-camp', retry: 'forty-eighth-battle' }
'forty-eighth-battle-meng-huo-main-force': { victory: 'forty-eighth-camp', retry: 'forty-eighth-battle' },
'forty-ninth-battle-meng-huo-second-capture': { victory: 'forty-ninth-camp', retry: 'forty-ninth-battle' }
};
export type CampaignSaveSlotSummary = {