Add Yiling fire attack chapter

This commit is contained in:
2026-06-23 19:46:28 +09:00
parent ef20c6460b
commit d88cc65972
11 changed files with 856 additions and 54 deletions

View File

@@ -44,6 +44,10 @@ import {
fortyFifthBattleMap,
fortyFifthBattleUnits,
fortyFifthBattleVictoryPages,
fortySixthBattleBonds,
fortySixthBattleMap,
fortySixthBattleUnits,
fortySixthBattleVictoryPages,
firstBattleMap,
firstBattleUnits,
firstBattleVictoryPages,
@@ -230,7 +234,8 @@ export type BattleScenarioId =
| 'forty-second-battle-jing-rear-crisis'
| 'forty-third-battle-gongan-collapse'
| 'forty-fourth-battle-maicheng-isolation'
| 'forty-fifth-battle-yiling-vanguard';
| 'forty-fifth-battle-yiling-vanguard'
| 'forty-sixth-battle-yiling-fire';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2922,6 +2927,66 @@ export const fortyFifthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const fortySixthBattleScenario: BattleScenarioDefinition = {
id: 'forty-sixth-battle-yiling-fire',
title: '이릉 화공전',
victoryConditionLabel: '오군 화공망 돌파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'육손은 긴 촉한 진영이 마른 숲과 강변 군막 사이로 늘어진 순간을 기다렸습니다. 오군의 화공이 시작되며 진영 곳곳이 끊어집니다.',
'불타는 군막을 모두 지키려 하기보다, 서쪽 협곡과 강변 물길을 열어 병사를 모아야 합니다. 마을과 군막 지점을 확보하면 퇴로가 안정됩니다.',
'황권과 제갈량은 화공의 번짐을 읽고, 조운과 왕평은 흩어진 길을 찾습니다. 누가 퇴로를 맡고 누가 불길을 뚫을지 출진 편성에서 결정하십시오.'
],
map: fortySixthBattleMap,
units: fortySixthBattleUnits,
bonds: fortySixthBattleBonds,
mapTextureKey: 'battle-map-forty-sixth',
leaderUnitId: 'yiling-fire-leader-lu-xun',
quickVictoryTurnLimit: 76,
baseVictoryGold: 9840,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '육손 화공 지휘선 돌파',
rewardGold: 6600,
unitId: 'yiling-fire-leader-lu-xun'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 1700,
unitId: 'liu-bei'
},
{
id: 'fire-camps',
kind: 'secure-terrain',
label: '불타는 군막 수습',
rewardGold: 3300,
terrain: 'camp'
},
{
id: 'retreat-road',
kind: 'secure-terrain',
label: '서쪽 퇴로 확보',
rewardGold: 2600,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '76턴 이내 돌파',
rewardGold: 2280,
maxTurn: 76
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 48', '상처약 38', '탁주 16', '화공 방화선 표식 1', '백제성 퇴로 지도 1'],
victoryPages: fortySixthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -2969,7 +3034,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario,
'forty-third-battle-gongan-collapse': fortyThirdBattleScenario,
'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario,
'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario
'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario,
'forty-sixth-battle-yiling-fire': fortySixthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -10,6 +10,7 @@ import {
fortyThirdBattleScenario,
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortySixthBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -66,6 +67,8 @@ import {
fortyFourthBattleVictoryPages,
fortyFifthBattleIntroPages,
fortyFifthBattleVictoryPages,
fortySixthBattleIntroPages,
fortySixthBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
fifteenthBattleIntroPages,
@@ -650,13 +653,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[fortyFifthBattleScenario.id]: {
afterBattleId: fortyFifthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '이릉 본전의 불씨',
eyebrow: '다음 장',
title: fortySixthBattleScenario.title,
description:
'이릉으로 향하는 첫 진영은 세웠지만 오군은 아직 본격적으로 맞붙지 않았습니다. 다음 장에서는 길게 늘어진 촉한 진영과 육손의 기다림, 화공의 위험을 본전으로 확장합니다.',
rewardHint: '다음 장: 이릉 본전과 화공 위기 준비 중',
pages: fortyFifthBattleVictoryPages,
unavailableNotice: '이릉 본전과 화공 위기는 다음 작업에서 이어집니다. 지금은 이릉 진격로 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.'
'이릉으로 향하는 첫 진영은 세웠지만 오군은 아직 본격적으로 맞붙지 않았습니다. 길게 늘어진 촉한 군막과 마른 숲길이 육손의 기다림 속에서 화공의 위기로 바뀝니다.',
rewardHint: `예상 보상: ${fortySixthBattleScenario.title} 개방 / 화공 방화선 표식`,
nextBattleId: fortySixthBattleScenario.id,
campaignStep: 'forty-sixth-battle',
pages: [...fortyFifthBattleVictoryPages, ...fortySixthBattleIntroPages]
},
[fortySixthBattleScenario.id]: {
afterBattleId: fortySixthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '백제성의 남은 뜻',
description:
'이릉의 불길을 뚫고 퇴로는 열었지만 복수전은 꺾였습니다. 다음 장에서는 백제성으로 물러난 유비가 남은 나라의 길과 후계를 정하는 이야기를 다룹니다.',
rewardHint: '다음 장: 백제성 유탁과 촉한 정비 준비 중',
pages: fortySixthBattleVictoryPages,
unavailableNotice: '백제성 유탁과 촉한 정비는 다음 작업에서 이어집니다. 지금은 이릉 화공전 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.'
}
};

View File

@@ -2969,6 +2969,70 @@ export const fortyFifthBattleVictoryPages: StoryPage[] = [
}
];
export const fortySixthBattleIntroPages: StoryPage[] = [
{
id: 'forty-sixth-camps-stretch',
bgm: 'story-dark',
chapter: '길어진 진영',
background: 'story-militia',
speaker: '황권',
text: '이릉의 첫 진영은 세웠지만, 촉한의 군막은 강변과 숲길을 따라 너무 길게 늘어졌습니다. 물길은 멀고, 마른 풀은 높으며, 오군은 여전히 큰 싸움을 피하고 있습니다.'
},
{
id: 'forty-sixth-lu-xun-fire',
bgm: 'story-dark',
chapter: '기다린 불씨',
background: 'story-three-heroes',
speaker: '제갈량',
text: '육손의 침묵은 물러남이 아니었습니다. 바람이 동쪽에서 불고 군막이 말랐을 때, 오군의 작은 횃불이 긴 진영 전체를 흔드는 칼이 되었습니다.'
},
{
id: 'forty-sixth-liu-bei-orders',
bgm: 'story-dark',
chapter: '불길 속의 명령',
background: 'story-resolve',
speaker: '유비',
portrait: 'liuBei',
text: '불길이 먼저 달려든다면, 사람의 길을 먼저 열어야 하오. 진영을 버려도 병사를 버릴 수는 없소. 서쪽 퇴로와 강변 물길을 붙들어 백제성으로 물러날 길을 만드시오.'
},
{
id: 'forty-sixth-sortie',
bgm: 'battle-prep',
chapter: '이릉 화공전',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '육손의 화공망이 진영을 갈라 놓았소. 불타는 군막 사이로 퇴로를 열고, 흩어진 병사를 모아 서쪽 협곡으로 빠져나가시오.'
}
];
export const fortySixthBattleVictoryPages: StoryPage[] = [
{
id: 'forty-sixth-victory-fire-broken',
bgm: 'story-dark',
chapter: '불길을 뚫은 길',
background: 'story-sortie',
text: '불타는 군막 사이로 좁은 퇴로가 열렸습니다. 승리라 부르기에는 잃은 것이 많았지만, 유비의 깃발은 완전히 꺼지지 않았습니다.'
},
{
id: 'forty-sixth-retreat-baidi',
bgm: 'story-dark',
chapter: '백제성으로',
background: 'story-militia',
speaker: '제갈량',
text: '이릉의 불길은 복수의 속도를 꺾었습니다. 남은 병사들은 백제성으로 물러나며, 촉한의 다음 길이 전장에서 조정과 후계로 옮겨가고 있음을 느꼈습니다.'
},
{
id: 'forty-sixth-next-baidi',
bgm: 'story-dark',
chapter: '남은 뜻',
background: 'story-three-heroes',
speaker: '유비',
portrait: 'liuBei',
text: '뜻을 세운 날은 멀어졌고, 손에 남은 것은 불탄 깃발뿐이오. 그러나 남은 이들이 있다면 뜻도 아직 끝나지 않았소. 다음 장에서는 백제성에서 남은 나라의 길을 정해야 하오.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -3384,6 +3448,12 @@ export const fortyFifthBattleMap: BattleMap = {
terrain: createFortyFifthBattleTerrain()
};
export const fortySixthBattleMap: BattleMap = {
width: 92,
height: 74,
terrain: createFortySixthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -15273,6 +15343,93 @@ export const fortyFifthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('yiling-leader-lu-xun', '육손', '오군 대도독', 'rebelLeader', 115, 768, 190, 84, 32)
];
const fortySixthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 18, y: 41 },
'guan-yu': { x: 13, y: 46 },
'zhang-fei': { x: 20, y: 44 },
'jian-yong': { x: 15, y: 51 },
'mi-zhu': { x: 13, y: 39 },
'sun-qian': { x: 16, y: 54 },
'zhao-yun': { x: 24, y: 38 },
'zhuge-liang': { x: 12, y: 43 },
'ma-liang': { x: 17, y: 35 },
'yi-ji': { x: 11, y: 56 },
'gong-zhi': { x: 21, y: 56 },
'huang-zhong': { x: 26, y: 42 },
'wei-yan': { x: 27, y: 47 },
'pang-tong': { x: 14, y: 32 },
'fa-zheng': { x: 19, y: 33 },
'wu-yi': { x: 28, y: 50 },
'yan-yan': { x: 30, y: 45 },
'li-yan': { x: 22, y: 30 },
'huang-quan': { x: 10, y: 37 },
'ma-chao': { x: 31, y: 39 },
'ma-dai': { x: 32, y: 43 },
'wang-ping': { x: 24, y: 34 }
};
export const fortySixthBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits,
...chengduSurrenderRecruitUnits,
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits
].map((unit) => placeScenarioUnit(unit, fortySixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('yiling-fire-scout-a', '오군 척후', '불길 척후', 'bandit', 114, 480, 150, 36, 26),
createThirtyFirstEnemyUnit('yiling-fire-scout-b', '오군 척후', '강변 척후', 'bandit', 114, 480, 150, 48, 57),
createThirtyFirstEnemyUnit('yiling-fire-scout-c', '오군 척후', '서쪽 척후', 'bandit', 115, 488, 152, 30, 34),
createThirtyFirstEnemyUnit('yiling-fire-scout-d', '오군 척후', '협곡 척후', 'bandit', 115, 488, 152, 64, 33),
createThirtyFirstEnemyUnit('yiling-fire-infantry-a', '오군 보병', '불길 보병', 'yellowTurban', 115, 690, 178, 42, 31),
createThirtyFirstEnemyUnit('yiling-fire-infantry-b', '오군 보병', '진영 보병', 'yellowTurban', 115, 690, 178, 51, 38),
createThirtyFirstEnemyUnit('yiling-fire-infantry-c', '오군 보병', '강안 보병', 'yellowTurban', 116, 702, 180, 58, 53),
createThirtyFirstEnemyUnit('yiling-fire-infantry-d', '오군 보병', '남안 보병', 'yellowTurban', 116, 702, 180, 70, 61),
createThirtyFirstEnemyUnit('yiling-fire-infantry-e', '오군 보병', '협곡 보병', 'yellowTurban', 117, 714, 182, 76, 37),
createThirtyFirstEnemyUnit('yiling-fire-infantry-f', '오군 보병', '후위 보병', 'yellowTurban', 117, 714, 182, 86, 48),
createThirtyFirstEnemyUnit('yiling-fire-archer-a', '오군 궁병', '화살 불씨', 'archer', 115, 530, 160, 39, 22),
createThirtyFirstEnemyUnit('yiling-fire-archer-b', '오군 궁병', '숲그늘 궁병', 'archer', 116, 538, 162, 54, 29),
createThirtyFirstEnemyUnit('yiling-fire-archer-c', '오군 궁병', '강안 궁병', 'archer', 116, 538, 162, 61, 48),
createThirtyFirstEnemyUnit('yiling-fire-archer-d', '오군 궁병', '남안 궁병', 'archer', 117, 546, 164, 75, 58),
createThirtyFirstEnemyUnit('yiling-fire-archer-e', '오군 궁병', '고지 궁병', 'archer', 117, 546, 164, 82, 30),
createThirtyFirstEnemyUnit('yiling-fire-archer-f', '오군 궁병', '퇴로 궁병', 'archer', 118, 554, 166, 88, 43),
createThirtyFirstEnemyUnit('yiling-fire-cavalry-a', '오군 기병', '불길 추격대', 'cavalry', 116, 704, 180, 44, 19),
createThirtyFirstEnemyUnit('yiling-fire-cavalry-b', '오군 기병', '숲끝 기병', 'cavalry', 116, 704, 180, 62, 24),
createThirtyFirstEnemyUnit('yiling-fire-cavalry-c', '오군 기병', '남로 기병', 'cavalry', 117, 716, 182, 73, 45),
createThirtyFirstEnemyUnit('yiling-fire-cavalry-d', '오군 기병', '퇴로 추격대', 'cavalry', 117, 716, 182, 88, 35),
createThirtyFirstEnemyUnit('yiling-fire-marine-a', '오군 수군', '강변 수군', 'yellowTurban', 116, 708, 180, 42, 64),
createThirtyFirstEnemyUnit('yiling-fire-marine-b', '오군 수군', '물길 차단대', 'yellowTurban', 117, 720, 182, 56, 67),
createThirtyFirstEnemyUnit('yiling-fire-marine-c', '오군 수군', '동쪽 수군', 'yellowTurban', 118, 732, 184, 72, 69),
createThirtyFirstEnemyUnit('yiling-fire-ambusher-a', '오군 복병', '불붙임 복병', 'bandit', 115, 500, 154, 35, 39),
createThirtyFirstEnemyUnit('yiling-fire-ambusher-b', '오군 복병', '군막 복병', 'bandit', 116, 508, 156, 47, 44),
createThirtyFirstEnemyUnit('yiling-fire-ambusher-c', '오군 복병', '강안 복병', 'bandit', 116, 508, 156, 59, 59),
createThirtyFirstEnemyUnit('yiling-fire-ambusher-d', '오군 복병', '후방 복병', 'bandit', 117, 516, 158, 80, 62),
createThirtyFirstEnemyUnit('yiling-fire-ambusher-e', '오군 복병', '서쪽 차단대', 'bandit', 117, 516, 158, 28, 50),
createThirtyFirstEnemyUnit('yiling-fire-guard-a', '오군 친위대', '화공 친위대', 'yellowTurban', 118, 742, 186, 66, 34),
createThirtyFirstEnemyUnit('yiling-fire-guard-b', '오군 친위대', '강안 친위대', 'yellowTurban', 118, 742, 186, 78, 51),
createThirtyFirstEnemyUnit('yiling-fire-guard-c', '오군 친위대', '대도독 친위대', 'yellowTurban', 119, 754, 188, 87, 56),
createThirtyFirstEnemyUnit('yiling-fire-strategist-a', '오군 책사', '화계 책사', 'strategist', 118, 570, 168, 63, 28),
createThirtyFirstEnemyUnit('yiling-fire-strategist-b', '오군 책사', '연소 책사', 'strategist', 119, 578, 170, 72, 47),
createThirtyFirstEnemyUnit('yiling-fire-strategist-c', '오군 책사', '수로 책사', 'strategist', 119, 578, 170, 57, 65),
createThirtyFirstEnemyUnit('yiling-fire-officer-zhu-ran', '주연', '화공 선봉장', 'rebelLeader', 120, 776, 192, 78, 40),
createThirtyFirstEnemyUnit('yiling-fire-officer-han-dang', '한당', '강안 노장', 'rebelLeader', 119, 762, 190, 66, 58),
createThirtyFirstEnemyUnit('yiling-fire-officer-pan-zhang', '반장', '추격 지휘관', 'rebelLeader', 119, 762, 190, 72, 24),
createThirtyFirstEnemyUnit('yiling-fire-officer-ma-zhong', '마충', '밀림 차단장', 'bandit', 119, 578, 170, 49, 50),
createThirtyFirstEnemyUnit('yiling-fire-leader-lu-xun', '육손', '화공 대도독', 'rebelLeader', 122, 812, 198, 86, 31)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -16063,6 +16220,35 @@ export const yilingVanguardBonds: BattleBond[] = [
export const fortyFifthBattleBonds: BattleBond[] = [...fortyFourthBattleBonds, ...yilingVanguardBonds].map(
cloneBattleBondForScenario
);
export const yilingFireBonds: BattleBond[] = [
{
id: 'liu-bei__huang-quan_yiling-fire',
unitIds: ['liu-bei', 'huang-quan'],
title: '퇴로의 장부',
level: 44,
exp: 0,
description: '유비의 결단과 황권의 신중함은 불타는 진영에서 퇴로와 보급을 지키는 힘이 된다.'
},
{
id: 'zhao-yun__wang-ping_yiling-fire',
unitIds: ['zhao-yun', 'wang-ping'],
title: '불길 속 길잡이',
level: 40,
exp: 0,
description: '조운의 기동과 왕평의 지형 판단은 불길이 번지는 숲길에서 흩어진 병사를 모은다.'
},
{
id: 'zhuge-liang__ma-liang_yiling-fire',
unitIds: ['zhuge-liang', 'ma-liang'],
title: '남은 뜻의 기록',
level: 38,
exp: 0,
description: '제갈량과 마량은 이릉의 패착을 기록하고 다음 국정으로 이어질 판단을 남긴다.'
}
];
export const fortySixthBattleBonds: BattleBond[] = [...fortyFifthBattleBonds, ...yilingFireBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -18327,6 +18513,67 @@ function createFortyFifthBattleTerrain(): TerrainType[][] {
);
}
function createFortySixthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 74 }, (_, y) =>
Array.from({ length: 92 }, (_, x): TerrainType => {
if (
(x >= 0 && x <= 91 && y >= 62) ||
(x >= 44 && x <= 58 && y >= 56 && y <= 73) ||
(x >= 66 && x <= 78 && y >= 64 && y <= 73)
) {
return 'river';
}
if (
(x >= 11 && x <= 22 && y >= 38 && y <= 50) ||
(x >= 42 && x <= 55 && y >= 35 && y <= 49) ||
(x >= 68 && x <= 82 && y >= 27 && y <= 42)
) {
return 'camp';
}
if (
(x >= 1 && x <= 84 && y >= 38 && y <= 43) ||
(x >= 15 && x <= 88 && y >= 50 && y <= 54) ||
(x >= 24 && x <= 37 && y >= 31 && y <= 50) ||
(x >= 57 && x <= 67 && y >= 23 && y <= 50)
) {
return 'road';
}
if (
(x >= 25 && x <= 60 && y >= 18 && y <= 55) ||
(x >= 48 && x <= 78 && y >= 25 && y <= 60) ||
(x >= 4 && x <= 30 && y >= 48 && y <= 61) ||
(x >= 72 && x <= 91 && y >= 42 && y <= 61)
) {
return 'forest';
}
if (
(x >= 0 && x <= 26 && y <= 29) ||
(x >= 34 && x <= 60 && y <= 22) ||
(x >= 66 && x <= 91 && y <= 28) ||
(x >= 80 && x <= 91 && y >= 29 && y <= 44)
) {
return 'hill';
}
if (
(x <= 2 && y <= 61) ||
(x >= 89 && y <= 57) ||
(x >= 30 && x <= 35 && y >= 0 && y <= 30) ||
(x >= 62 && x <= 66 && y >= 0 && y <= 25) ||
(x >= 35 && x <= 39 && y >= 52 && y <= 73)
) {
return 'cliff';
}
if ((x >= 16 && x <= 23 && y >= 31 && y <= 37) || (x >= 76 && x <= 87 && y >= 45 && y <= 57)) {
return 'fort';
}
if ((x >= 9 && x <= 18 && y >= 52 && y <= 58) || (x >= 56 && x <= 63 && y >= 53 && y <= 59)) {
return 'village';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -836,7 +836,15 @@ const unitTexture: Record<string, string> = {
'yiling-officer-zhu-ran': 'unit-rebel-leader',
'yiling-officer-pan-zhang': 'unit-rebel-leader',
'yiling-officer-ma-zhong': 'unit-rebel',
'yiling-leader-lu-xun': 'unit-rebel-leader'
'yiling-leader-lu-xun': 'unit-rebel-leader',
'yiling-fire-strategist-a': 'unit-rebel-archer',
'yiling-fire-strategist-b': 'unit-rebel-archer',
'yiling-fire-strategist-c': 'unit-rebel-archer',
'yiling-fire-officer-zhu-ran': 'unit-rebel-leader',
'yiling-fire-officer-han-dang': 'unit-rebel-leader',
'yiling-fire-officer-pan-zhang': 'unit-rebel-leader',
'yiling-fire-officer-ma-zhong': 'unit-rebel',
'yiling-fire-leader-lu-xun': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -2161,7 +2169,46 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'yiling-officer-zhu-ran': 'guard',
'yiling-officer-pan-zhang': 'aggressive',
'yiling-officer-ma-zhong': 'aggressive',
'yiling-leader-lu-xun': 'hold'
'yiling-leader-lu-xun': 'hold',
'yiling-fire-scout-a': 'aggressive',
'yiling-fire-scout-b': 'aggressive',
'yiling-fire-scout-c': 'aggressive',
'yiling-fire-scout-d': 'aggressive',
'yiling-fire-infantry-a': 'guard',
'yiling-fire-infantry-b': 'guard',
'yiling-fire-infantry-c': 'guard',
'yiling-fire-infantry-d': 'guard',
'yiling-fire-infantry-e': 'guard',
'yiling-fire-infantry-f': 'guard',
'yiling-fire-archer-a': 'hold',
'yiling-fire-archer-b': 'hold',
'yiling-fire-archer-c': 'hold',
'yiling-fire-archer-d': 'hold',
'yiling-fire-archer-e': 'hold',
'yiling-fire-archer-f': 'hold',
'yiling-fire-cavalry-a': 'aggressive',
'yiling-fire-cavalry-b': 'aggressive',
'yiling-fire-cavalry-c': 'aggressive',
'yiling-fire-cavalry-d': 'aggressive',
'yiling-fire-marine-a': 'aggressive',
'yiling-fire-marine-b': 'guard',
'yiling-fire-marine-c': 'guard',
'yiling-fire-ambusher-a': 'aggressive',
'yiling-fire-ambusher-b': 'aggressive',
'yiling-fire-ambusher-c': 'aggressive',
'yiling-fire-ambusher-d': 'aggressive',
'yiling-fire-ambusher-e': 'aggressive',
'yiling-fire-guard-a': 'guard',
'yiling-fire-guard-b': 'guard',
'yiling-fire-guard-c': 'guard',
'yiling-fire-strategist-a': 'hold',
'yiling-fire-strategist-b': 'hold',
'yiling-fire-strategist-c': 'hold',
'yiling-fire-officer-zhu-ran': 'aggressive',
'yiling-fire-officer-han-dang': 'guard',
'yiling-fire-officer-pan-zhang': 'aggressive',
'yiling-fire-officer-ma-zhong': 'aggressive',
'yiling-fire-leader-lu-xun': 'hold'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -11,6 +11,7 @@ import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-bat
import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battle-map.svg';
import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg';
import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg';
import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-battle-map.svg';
import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
@@ -148,6 +149,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-forty-third', fortyThirdBattleMapUrl);
this.load.image('battle-map-forty-fourth', fortyFourthBattleMapUrl);
this.load.image('battle-map-forty-fifth', fortyFifthBattleMapUrl);
this.load.image('battle-map-forty-sixth', fortySixthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -47,6 +47,7 @@ import {
xuzhouRecruitUnits,
yizhouRecruitBonds,
yizhouRecruitUnits,
yilingFireBonds,
yilingVanguardBonds,
zhugeRecruitBonds,
zhugeRecruitUnits,
@@ -400,9 +401,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'forty-second-battle-jing-rear-crisis',
'forty-third-battle-gongan-collapse',
'forty-fourth-battle-maicheng-isolation',
'forty-fifth-battle-yiling-vanguard'
'forty-fifth-battle-yiling-vanguard',
'forty-sixth-battle-yiling-fire'
],
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 복수전']
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공']
}
];
@@ -451,7 +453,8 @@ const campBattleIds = {
fortySecond: 'forty-second-battle-jing-rear-crisis',
fortyThird: 'forty-third-battle-gongan-collapse',
fortyFourth: 'forty-fourth-battle-maicheng-isolation',
fortyFifth: 'forty-fifth-battle-yiling-vanguard'
fortyFifth: 'forty-fifth-battle-yiling-vanguard',
fortySixth: 'forty-sixth-battle-yiling-fire'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -984,6 +987,20 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-liang', reason: '형주 상실 뒤 흩어진 잔병과 현지 민심을 다독이는 연결 축입니다.' }
],
note: '이릉 진격로는 스물두 명 중 일곱 명만 출전합니다. 유비는 필수이며, 복수의 선봉과 보급·화공 대비를 균형 있게 고르는 전투입니다.'
},
'forty-sixth-battle-yiling-fire': {
maxUnits: 7,
requiredUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'liu-bei', reason: '이릉 화공 속에서 병사를 버리지 않고 퇴로를 열어야 하는 주장입니다.' },
{ unitId: 'huang-quan', reason: '군막 간격, 보급 장부, 서쪽 퇴로를 신중하게 계산하는 핵심 참모입니다.' },
{ unitId: 'zhuge-liang', reason: '육손의 화공 의도와 다음 백제성 수습까지 읽는 큰 책략 축입니다.' },
{ unitId: 'ma-liang', reason: '형주 잔병과 현지 민심을 모아 불길 속 사기를 붙잡습니다.' },
{ unitId: 'zhao-yun', reason: '끊어진 군막 사이를 빠르게 오가며 흩어진 아군을 회수합니다.' },
{ unitId: 'wang-ping', reason: '숲길과 강변 퇴로의 지형 판단으로 포위망을 빠져나갈 길을 찾습니다.' },
{ unitId: 'ma-chao', reason: '오군 추격 기병과 불길 바깥 차단선을 강하게 밀어낼 수 있습니다.' }
],
note: '이릉 화공전은 스물두 명 중 일곱 명만 출전합니다. 유비는 필수이며, 불길 수습, 퇴로 확보, 추격 차단 중 무엇을 두껍게 할지 선택하는 전투입니다.'
}
};
@@ -4713,6 +4730,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 16
}
]
},
{
id: 'liu-huangquan-after-yiling-fire',
title: '퇴로의 장부',
availableAfterBattleIds: [campBattleIds.fortySixth],
unitIds: ['liu-bei', 'huang-quan'],
bondId: 'liu-bei__huang-quan_yiling-fire',
rewardExp: 52,
lines: [
'황권: 폐하, 불길은 군막을 태웠지만 장부까지 태우면 남은 병사를 다시 세울 수 없습니다.',
'유비: 내가 서두른 탓이 크오. 그렇기에 남은 자들의 이름을 더 잊지 말아야 하오.',
'황권: 퇴로를 기록하고 흩어진 부대를 셈하겠습니다. 다음 싸움보다 먼저, 남은 군의 숨을 이어야 합니다.'
],
choices: [
{
id: 'count-retreat-columns',
label: '퇴각 부대를 다시 센다',
response: '유비는 황권에게 퇴각 부대의 이름과 위치를 모두 다시 세게 했고, 남은 병사들의 동요가 조금 잦아들었습니다.',
rewardExp: 18
},
{
id: 'protect-supply-ledgers',
label: '보급 장부를 지킨다',
response: '황권은 불탄 군막에서 건진 보급 장부를 정리했고, 유비는 다음 길을 급히 정하지 않겠다고 고개를 끄덕였습니다.',
rewardExp: 17
}
]
},
{
id: 'zhaoyun-wangping-after-yiling-fire',
title: '불길 속 길잡이',
availableAfterBattleIds: [campBattleIds.fortySixth],
unitIds: ['zhao-yun', 'wang-ping'],
bondId: 'zhao-yun__wang-ping_yiling-fire',
rewardExp: 50,
lines: [
'왕평: 불길이 지나간 숲은 길처럼 보이지만, 재가 깊어 말과 병사가 함께 빠집니다.',
'조운: 길이 사라졌다면 다시 만들면 되오. 다만 뒤처진 병사가 그 길을 알아볼 수 있어야 하오.',
'왕평: 강변의 낮은 돌과 그을린 나무를 표식으로 삼겠습니다. 불길이 만든 길을 퇴로로 바꾸겠습니다.'
],
choices: [
{
id: 'mark-ash-road',
label: '그을린 길을 표시한다',
response: '조운은 그을린 숲길에 작은 깃발을 남겼고, 왕평은 퇴로 표식을 병사들에게 나누어 익혔습니다.',
rewardExp: 17
},
{
id: 'cover-stragglers',
label: '뒤처진 병사를 엄호한다',
response: '왕평은 늦은 병사들이 빠지는 낮은 땅을 짚었고, 조운은 추격대를 끊는 짧은 기동로를 잡았습니다.',
rewardExp: 16
}
]
},
{
id: 'zhuge-maliang-after-yiling-fire',
title: '남은 뜻의 기록',
availableAfterBattleIds: [campBattleIds.fortySixth],
unitIds: ['zhuge-liang', 'ma-liang'],
bondId: 'zhuge-liang__ma-liang_yiling-fire',
rewardExp: 50,
lines: [
'마량: 이릉의 패착을 숨기면 병사들은 같은 불길을 다시 만날 것입니다.',
'제갈량: 아픈 기록일수록 남겨야 합니다. 나라가 오래 버티려면 승리보다 실패의 모양을 알아야 하지요.',
'마량: 백제성에 닿기 전에, 잃은 길과 남은 길을 나누어 적겠습니다.'
],
choices: [
{
id: 'write-fire-lesson',
label: '화공의 원인을 기록한다',
response: '제갈량과 마량은 늘어진 진영, 마른 숲, 바람의 방향을 차례로 적어 다음 작전의 금기로 삼았습니다.',
rewardExp: 18
},
{
id: 'prepare-baidi-council',
label: '백제성 논의를 준비한다',
response: '마량은 백제성에 모일 문서를 정리했고, 제갈량은 전장이 아닌 조정에서 이어질 책임을 조용히 받아들였습니다.',
rewardExp: 17
}
]
}
];
@@ -6542,6 +6640,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['탁주 4']
}
]
},
{
id: 'yiling-firebreak-lines',
title: '화공 방화선 수습',
location: '이릉 불탄 군막',
availableAfterBattleIds: [campBattleIds.fortySixth],
bondId: 'zhuge-liang__ma-liang_yiling-fire',
description: '제갈량과 마량이 불탄 군막 사이에 남은 방화선을 정리하고, 다음 전투에서 반복하지 말아야 할 배치를 기록합니다.',
lines: [
'마량: 여기서 군막과 마른 풀이 너무 가까웠습니다. 물동이는 있었지만 불길을 끊을 빈 땅이 부족했습니다.',
'제갈량: 이 기록은 패배의 부끄러움이 아니라 다음 병사를 살리는 약입니다.',
'병사들은 재가 된 군막 터에 낮은 말뚝을 세우고, 다시는 같은 간격으로 진을 치지 않도록 표시했습니다.'
],
choices: [
{
id: 'mark-firebreak-spacing',
label: '방화선 간격을 새긴다',
response: '방화선 간격 표식이 정리되어, 다음 군영 배치에서 화공 대비가 더 분명해졌습니다.',
bondExp: 40,
itemRewards: ['화공 방화선 표식 1']
},
{
id: 'recover-camp-records',
label: '군막 기록을 수습한다',
response: '마량은 타다 남은 배치 기록을 건졌고, 제갈량은 불길이 번진 순서를 다시 맞추었습니다.',
bondExp: 38,
gold: 1220,
itemRewards: ['상처약 6']
}
]
},
{
id: 'yiling-fire-baidi-retreat-route',
title: '백제성 퇴로 정찰',
location: '서쪽 협곡 물길',
availableAfterBattleIds: [campBattleIds.fortySixth],
bondId: 'zhao-yun__wang-ping_yiling-fire',
description: '조운과 왕평이 백제성으로 향하는 협곡 물길을 다시 훑고, 흩어진 병사들이 모일 표식을 남깁니다.',
lines: [
'왕평: 강가의 낮은 돌을 따라가면 밤에도 길을 잃지 않습니다. 다만 추격 기병이 들어오면 먼저 막히는 곳입니다.',
'조운: 내가 뒤를 끊고, 그대는 앞의 표식을 맡으시오. 병사들이 길을 보면 다시 걸을 수 있소.',
'정찰대는 협곡 입구마다 작은 천 조각을 묶고, 백제성으로 향하는 물길을 낮게 표시했습니다.'
],
choices: [
{
id: 'place-baidi-route-markers',
label: '백제성 표식을 남긴다',
response: '백제성으로 향하는 물길에 표식이 남아, 흩어진 병사들이 서쪽으로 모일 가능성이 높아졌습니다.',
bondExp: 39,
itemRewards: ['백제성 퇴로 지도 1']
},
{
id: 'delay-wu-pursuit',
label: '오군 추격로를 늦춘다',
response: '조운은 좁은 길목에 작은 장애물을 놓았고, 왕평은 우회로를 지워 추격대의 속도를 늦추었습니다.',
bondExp: 37,
gold: 1240,
itemRewards: ['탁주 4']
}
]
}
];
@@ -6635,7 +6793,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fortySecond ||
battleId === campBattleIds.fortyThird ||
battleId === campBattleIds.fortyFourth ||
battleId === campBattleIds.fortyFifth
battleId === campBattleIds.fortyFifth ||
battleId === campBattleIds.fortySixth
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -6683,7 +6842,8 @@ export class CampScene extends Phaser.Scene {
...jingRearCrisisBonds,
...jingCollapseBonds,
...maichengIsolationBonds,
...yilingVanguardBonds
...yilingVanguardBonds,
...yilingFireBonds
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -7318,6 +7478,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'hanzhong-king-camp') {
return '한중왕 즉위 준비 군영';
}
if (battleId === campBattleIds.fortySixth) {
return '이릉 화공전 후 군영';
}
if (battleId === campBattleIds.fortyFifth) {
return '이릉 진격로 후 군영';
}

View File

@@ -11,6 +11,7 @@ import {
fortyThirdBattleScenario,
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortySixthBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -390,6 +391,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'forty-third-camp' ||
campaign.step === 'forty-fourth-camp' ||
campaign.step === 'forty-fifth-camp' ||
campaign.step === 'forty-sixth-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp'
) {
@@ -622,6 +624,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'forty-sixth-battle') {
this.scene.start('BattleScene', { battleId: fortySixthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -132,6 +132,8 @@ export type CampaignStep =
| 'forty-fourth-camp'
| 'forty-fifth-battle'
| 'forty-fifth-camp'
| 'forty-sixth-battle'
| 'forty-sixth-camp'
| 'hanzhong-king-camp'
| 'shu-han-foundation-camp';
@@ -284,7 +286,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'forty-second-battle-jing-rear-crisis': { victory: 'forty-second-camp', retry: 'forty-second-battle' },
'forty-third-battle-gongan-collapse': { victory: 'forty-third-camp', retry: 'forty-third-battle' },
'forty-fourth-battle-maicheng-isolation': { victory: 'forty-fourth-camp', retry: 'forty-fourth-battle' },
'forty-fifth-battle-yiling-vanguard': { victory: 'forty-fifth-camp', retry: 'forty-fifth-battle' }
'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' }
};
export type CampaignSaveSlotSummary = {