Add Han River flood chapter

This commit is contained in:
2026-06-23 16:12:24 +09:00
parent fe1f897609
commit 628e73ba58
11 changed files with 910 additions and 45 deletions

View File

@@ -20,6 +20,10 @@ import {
eleventhBattleMap,
eleventhBattleUnits,
eleventhBattleVictoryPages,
fortiethBattleBonds,
fortiethBattleMap,
fortiethBattleUnits,
fortiethBattleVictoryPages,
firstBattleMap,
firstBattleUnits,
firstBattleVictoryPages,
@@ -200,7 +204,8 @@ export type BattleScenarioId =
| 'thirty-sixth-battle-dingjun-vanguard'
| 'thirty-seventh-battle-hanzhong-decisive'
| 'thirty-eighth-battle-jing-defense'
| 'thirty-ninth-battle-fan-castle-vanguard';
| 'thirty-ninth-battle-fan-castle-vanguard'
| 'fortieth-battle-han-river-flood';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2517,6 +2522,69 @@ export const thirtyNinthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const fortiethBattleScenario: BattleScenarioDefinition = {
id: 'fortieth-battle-han-river-flood',
title: '한수 수공',
victoryConditionLabel: '우금 본대 격파',
defeatConditionLabel: '유비 또는 관우 퇴각',
openingObjectiveLines: [
'한수의 수위가 오르며 번성 외곽이 진흙과 물길로 갈라졌습니다. 우금의 본대를 강과 둑 사이에 묶어 칠군을 흔드십시오.',
'관우는 이번 전투의 중심입니다. 관우가 전선을 잃으면 수공의 명분과 병사의 사기가 함께 무너집니다.',
'우금은 본진을 지키고, 방덕은 기병으로 돌파를 시도합니다. 법정, 마량, 황권 같은 참모와 기동대를 함께 운용하면 물길을 장악하기 쉽습니다.'
],
map: fortiethBattleMap,
units: fortiethBattleUnits,
bonds: fortiethBattleBonds,
mapTextureKey: 'battle-map-fortieth',
leaderUnitId: 'han-river-leader-yu-jin',
quickVictoryTurnLimit: 64,
baseVictoryGold: 7920,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '우금 본대 격파',
rewardGold: 5060,
unitId: 'han-river-leader-yu-jin'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 1240,
unitId: 'liu-bei'
},
{
id: 'guan-yu',
kind: 'keep-unit-alive',
label: '관우 생존',
rewardGold: 1960,
unitId: 'guan-yu'
},
{
id: 'river-bank',
kind: 'secure-terrain',
label: '한수 둑길 확보',
rewardGold: 2440,
terrain: 'road'
},
{
id: 'quick',
kind: 'quick-victory',
label: '64턴 이내 승리',
rewardGold: 1800,
maxTurn: 64
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'guan-yu' }
],
itemRewards: ['콩 36', '상처약 26', '탁주 11', '한수 수위표 1', '칠군 항복문 1'],
victoryPages: fortiethBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -2558,7 +2626,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario,
'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario,
'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario,
'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario
'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario,
'fortieth-battle-han-river-flood': fortiethBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -4,6 +4,7 @@ import {
eighthBattleScenario,
eighteenthBattleScenario,
eleventhBattleScenario,
fortiethBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -48,6 +49,8 @@ import {
eighteenthBattleVictoryPages,
eleventhBattleIntroPages,
eleventhBattleVictoryPages,
fortiethBattleIntroPages,
fortiethBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
fifteenthBattleIntroPages,
@@ -566,13 +569,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[thirtyNinthBattleScenario.id]: {
afterBattleId: thirtyNinthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '한수 수공 준비',
eyebrow: '다음 장',
title: fortiethBattleScenario.title,
description:
'번성 외곽의 보루를 눌렀지만 조인과 우금의 본대는 아직 성 안과 수로를 지키고 있습니다. 다음 에서는 한수의 물길, 둑, 성벽을 함께 다루는 더 큰 전장이 이어집니다.',
rewardHint: '다음 장: 한수 수공과 칠군 침수 준비 중',
pages: thirtyNinthBattleVictoryPages,
unavailableNotice: '한수 수공과 칠군 침수 전장은 다음 작업에서 이어집니다. 지금은 군영에서 무장 성장, 공명도, 보급을 정비할 수 있습니다.'
'번성 외곽의 보루를 눌렀지만 조인과 우금의 본대는 아직 성 안과 수로를 지키고 있습니다. 다음 출진에서는 한수의 물길을 장악해 칠군을 흔들고 번성 포위의 결정적 발판을 마련합니다.',
rewardHint: `예상 보상: ${fortiethBattleScenario.title} 개방 / 칠군 항복문`,
nextBattleId: fortiethBattleScenario.id,
campaignStep: 'fortieth-battle',
pages: [...thirtyNinthBattleVictoryPages, ...fortiethBattleIntroPages]
},
[fortiethBattleScenario.id]: {
afterBattleId: fortiethBattleScenario.id,
eyebrow: '다음 장 준비',
title: '번성 포위 준비',
description:
'한수의 물길이 조조군을 무너뜨렸지만 번성의 성문은 아직 열리지 않았습니다. 다음 장에서는 수공 이후의 포위전, 방덕의 결말, 오와 위의 시선이 더 날카롭게 이어집니다.',
rewardHint: '다음 장: 번성 포위와 형주 위기 준비 중',
pages: fortiethBattleVictoryPages,
unavailableNotice: '번성 포위전과 형주 위기는 다음 작업에서 이어집니다. 지금은 군영에서 물길 전투의 성장, 공명도, 보급을 정비할 수 있습니다.'
}
};

View File

@@ -2589,6 +2589,70 @@ export const thirtyNinthBattleVictoryPages: StoryPage[] = [
}
];
export const fortiethBattleIntroPages: StoryPage[] = [
{
id: 'fortieth-han-river-rain',
bgm: 'story-dark',
chapter: '불어난 한수',
background: 'story-militia',
speaker: '제갈량',
text: '비가 이어지며 한수의 물길이 불어났습니다. 번성 밖의 평지는 진흙으로 변했고, 우금의 본대와 방덕의 선봉은 강과 둑 사이에 길게 늘어섰습니다.'
},
{
id: 'fortieth-water-plan',
bgm: 'battle-prep',
chapter: '수공의 군령',
background: 'story-liu-bei',
speaker: '법정',
text: '둑을 무너뜨리는 것만이 수공은 아닙니다. 물이 차오를 때 적이 믿는 퇴로를 먼저 끊고, 궁병과 기병을 높은 둑 위로 몰아야 합니다.'
},
{
id: 'fortieth-guan-yu-command',
bgm: 'battle-prep',
chapter: '관우의 결단',
background: 'story-three-heroes',
speaker: '관우',
portrait: 'guanYu',
text: '물길이 우리 편에 선다면 적의 칼도 무뎌질 것이오. 우금의 본대를 눌러 번성의 숨통을 끊고, 방덕의 돌격은 둑 위에서 맞받아치겠소.'
},
{
id: 'fortieth-sortie',
bgm: 'battle-prep',
chapter: '한수 수공',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '관우가 앞을 열고, 법정과 마량이 물길을 살피며, 기동대는 둑과 나루를 끊어라. 이번 싸움은 성벽보다 먼저 강을 얻는 싸움이다.'
}
];
export const fortiethBattleVictoryPages: StoryPage[] = [
{
id: 'fortieth-victory-seven-armies',
bgm: 'militia-theme',
chapter: '칠군이 물에 잠기다',
background: 'story-sortie',
text: '한수의 물길이 전장을 덮치자 조조군의 진형은 무너졌습니다. 우금의 본대는 퇴로를 잃었고, 번성 외곽에는 관우군의 깃발이 더욱 가까워졌습니다.'
},
{
id: 'fortieth-yu-jin-surrenders',
bgm: 'battle-prep',
chapter: '우금의 항복',
background: 'story-three-heroes',
speaker: '제갈량',
text: '우금은 병사들을 살리기 위해 무기를 내려놓았습니다. 그러나 방덕은 끝까지 물러서지 않았고, 번성 안의 조인은 더 깊은 수성전을 준비하기 시작했습니다.'
},
{
id: 'fortieth-next-siege',
bgm: 'story-dark',
chapter: '번성 포위',
background: 'story-militia',
speaker: '관우',
portrait: 'guanYu',
text: '강은 길을 열었으나 성문은 아직 남아 있소. 병사들에게 쉬게 하되 경계를 늦추지 마시오. 다음은 번성의 성벽을 직접 마주할 차례요.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2968,6 +3032,12 @@ export const thirtyNinthBattleMap: BattleMap = {
terrain: createThirtyNinthBattleTerrain()
};
export const fortiethBattleMap: BattleMap = {
width: 80,
height: 62,
terrain: createFortiethBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -14347,6 +14417,84 @@ export const thirtyNinthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('fan-castle-leader-pang-de', '방덕', '번성 선봉장', 'cavalry', 92, 566, 150, 71, 31)
];
const fortiethBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 5, y: 56 },
'guan-yu': { x: 11, y: 51 },
'zhang-fei': { x: 6, y: 58 },
'jian-yong': { x: 4, y: 59 },
'mi-zhu': { x: 8, y: 57 },
'sun-qian': { x: 9, y: 60 },
'zhao-yun': { x: 14, y: 54 },
'zhuge-liang': { x: 9, y: 53 },
'ma-liang': { x: 13, y: 56 },
'yi-ji': { x: 15, y: 59 },
'gong-zhi': { x: 16, y: 57 },
'huang-zhong': { x: 17, y: 52 },
'wei-yan': { x: 15, y: 50 },
'pang-tong': { x: 12, y: 54 },
'fa-zheng': { x: 18, y: 56 },
'wu-yi': { x: 19, y: 53 },
'yan-yan': { x: 20, y: 55 },
'li-yan': { x: 18, y: 50 },
'huang-quan': { x: 21, y: 57 },
'ma-chao': { x: 22, y: 51 },
'ma-dai': { x: 23, y: 53 },
'wang-ping': { x: 21, y: 49 }
};
export const fortiethBattleUnits: 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, fortiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('han-river-scout-a', '우금군 척후', '둑길 척후', 'bandit', 88, 326, 108, 30, 53),
createThirtyFirstEnemyUnit('han-river-scout-b', '우금군 척후', '수문 척후', 'bandit', 88, 326, 108, 38, 50),
createThirtyFirstEnemyUnit('han-river-scout-c', '우금군 척후', '상류 척후', 'bandit', 89, 334, 110, 55, 44),
createThirtyFirstEnemyUnit('han-river-infantry-a', '우금군 보병', '하류 보병', 'yellowTurban', 89, 492, 138, 42, 46),
createThirtyFirstEnemyUnit('han-river-infantry-b', '우금군 보병', '둑길 보병', 'yellowTurban', 89, 492, 138, 50, 43),
createThirtyFirstEnemyUnit('han-river-infantry-c', '우금군 보병', '중앙 보병', 'yellowTurban', 90, 504, 140, 59, 37),
createThirtyFirstEnemyUnit('han-river-infantry-d', '우금군 보병', '수로 보병', 'yellowTurban', 90, 504, 140, 66, 33),
createThirtyFirstEnemyUnit('han-river-infantry-e', '우금군 보병', '상류 보병', 'yellowTurban', 91, 516, 142, 72, 27),
createThirtyFirstEnemyUnit('han-river-archer-a', '우금군 궁병', '제방 궁병', 'archer', 89, 346, 116, 45, 48),
createThirtyFirstEnemyUnit('han-river-archer-b', '우금군 궁병', '나루 궁병', 'archer', 90, 354, 118, 55, 40),
createThirtyFirstEnemyUnit('han-river-archer-c', '우금군 궁병', '중앙 궁병', 'archer', 90, 354, 118, 64, 35),
createThirtyFirstEnemyUnit('han-river-archer-d', '우금군 궁병', '성벽 궁병', 'archer', 91, 362, 120, 73, 29),
createThirtyFirstEnemyUnit('han-river-archer-e', '우금군 궁병', '상류 궁병', 'archer', 91, 362, 120, 75, 21),
createThirtyFirstEnemyUnit('han-river-cavalry-a', '방덕 기병', '좌익 기병', 'cavalry', 90, 504, 142, 34, 54),
createThirtyFirstEnemyUnit('han-river-cavalry-b', '방덕 기병', '둑길 기병', 'cavalry', 90, 504, 142, 49, 49),
createThirtyFirstEnemyUnit('han-river-cavalry-c', '방덕 기병', '중앙 기병', 'cavalry', 91, 516, 144, 60, 43),
createThirtyFirstEnemyUnit('han-river-cavalry-d', '방덕 기병', '상류 기병', 'cavalry', 91, 516, 144, 70, 37),
createThirtyFirstEnemyUnit('han-river-guard-a', '우금군 친위대', '수문 친위대', 'yellowTurban', 92, 548, 148, 61, 34),
createThirtyFirstEnemyUnit('han-river-guard-b', '우금군 친위대', '본진 친위대', 'yellowTurban', 92, 548, 148, 68, 30),
createThirtyFirstEnemyUnit('han-river-guard-c', '우금군 친위대', '우금 친위대', 'yellowTurban', 93, 560, 150, 72, 24),
createThirtyFirstEnemyUnit('han-river-guard-d', '우금군 친위대', '성문 친위대', 'yellowTurban', 93, 560, 150, 76, 18),
createThirtyFirstEnemyUnit('han-river-strategist-a', '조조군 책사', '수위 책사', 'strategist', 90, 362, 122, 57, 41),
createThirtyFirstEnemyUnit('han-river-strategist-b', '조조군 책사', '본진 책사', 'strategist', 91, 370, 124, 68, 32),
createThirtyFirstEnemyUnit('han-river-strategist-c', '조조군 책사', '상류 책사', 'strategist', 92, 378, 126, 74, 23),
createThirtyFirstEnemyUnit('han-river-ambusher-a', '둑길 복병', '진흙 복병', 'bandit', 89, 336, 112, 40, 55),
createThirtyFirstEnemyUnit('han-river-ambusher-b', '둑길 복병', '제방 복병', 'bandit', 90, 344, 114, 54, 47),
createThirtyFirstEnemyUnit('han-river-ambusher-c', '둑길 복병', '나루 복병', 'bandit', 90, 344, 114, 66, 42),
createThirtyFirstEnemyUnit('han-river-officer-pang-de', '방덕', '조조군 선봉장', 'cavalry', 93, 584, 154, 72, 34),
createThirtyFirstEnemyUnit('han-river-officer-cao-ren', '조인', '번성 수비장', 'rebelLeader', 94, 588, 154, 76, 19),
createThirtyFirstEnemyUnit('han-river-leader-yu-jin', '우금', '칠군 총대장', 'rebelLeader', 95, 612, 158, 73, 26)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -14906,6 +15054,25 @@ export const fanCastleBonds: BattleBond[] = [
}
];
export const hanRiverFloodBonds: BattleBond[] = [
{
id: 'guan-yu__huang-quan',
unitIds: ['guan-yu', 'huang-quan'],
title: '한수 물길 판단',
level: 25,
exp: 0,
description: '관우의 결단과 황권의 신중한 물길 판단이 맞물리면, 강과 둑을 활용한 포위 공격의 안정성이 높아집니다.'
},
{
id: 'guan-yu__li-yan',
unitIds: ['guan-yu', 'li-yan'],
title: '제방 보급선',
level: 24,
exp: 0,
description: '관우의 전선 압박과 이엄의 보급 관리가 이어지면, 진흙과 물길 사이에서도 병력 회복과 장기전 준비가 단단해집니다.'
}
];
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -14968,6 +15135,9 @@ export const thirtyEighthBattleBonds: BattleBond[] = [...thirtySeventhBattleBond
export const thirtyNinthBattleBonds: BattleBond[] = [...thirtyEighthBattleBonds, ...fanCastleBonds].map(
cloneBattleBondForScenario
);
export const fortiethBattleBonds: BattleBond[] = [...thirtyNinthBattleBonds, ...hanRiverFloodBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -16789,6 +16959,79 @@ function createThirtyNinthBattleTerrain(): TerrainType[][] {
);
}
function createFortiethBattleTerrain(): TerrainType[][] {
return Array.from({ length: 62 }, (_, y) =>
Array.from({ length: 80 }, (_, x): TerrainType => {
if (x <= 6 && y >= 55) {
return 'camp';
}
if (
(x >= 62 && x <= 79 && y >= 6 && y <= 22) ||
(x >= 55 && x <= 68 && y >= 10 && y <= 24) ||
(x >= 68 && x <= 79 && y >= 22 && y <= 39) ||
(x >= 70 && x <= 76 && y >= 36 && y <= 45)
) {
return 'fort';
}
if (
(x >= 22 && x <= 26 && y >= 52 && y <= 56) ||
(x >= 44 && x <= 48 && y >= 45 && y <= 48) ||
(x >= 58 && x <= 62 && y >= 38 && y <= 42) ||
(x >= 72 && x <= 75 && y >= 42 && y <= 46)
) {
return 'village';
}
if (
(x >= 19 && x <= 23 && y >= 0 && y <= 53) ||
(x >= 34 && x <= 37 && y >= 13 && y <= 58) ||
(x >= 50 && x <= 53 && y >= 4 && y <= 47) ||
(x >= 60 && x <= 62 && y >= 41 && y <= 61) ||
(y >= 30 && y <= 33 && x >= 22 && x <= 62)
) {
return 'river';
}
if (
(y >= 57 && x >= 6 && x <= 29) ||
(x >= 28 && x <= 44 && y >= 52 && y <= 57) ||
(x >= 43 && x <= 58 && y >= 46 && y <= 52) ||
(x >= 57 && x <= 70 && y >= 40 && y <= 46) ||
(x >= 69 && x <= 78 && y >= 32 && y <= 40) ||
(x >= 72 && x <= 79 && y >= 18 && y <= 32) ||
(x >= 58 && x <= 77 && y >= 16 && y <= 20) ||
(y >= 26 && y <= 29 && x >= 22 && x <= 72)
) {
return 'road';
}
if (
(x >= 4 && x <= 29 && y >= 11 && y <= 43) ||
(x >= 30 && x <= 46 && y >= 19 && y <= 48) ||
(x >= 52 && x <= 66 && y >= 46 && y <= 61) ||
(x >= 63 && x <= 79 && y >= 42 && y <= 60)
) {
return 'forest';
}
if (
(x >= 0 && x <= 18 && y <= 28) ||
(x >= 28 && x <= 48 && y <= 18) ||
(x >= 49 && x <= 67 && y >= 1 && y <= 20) ||
(x >= 68 && x <= 79 && y <= 15) ||
(x >= 33 && x <= 52 && y >= 54 && y <= 61)
) {
return 'hill';
}
if (
(x <= 2 && y <= 54) ||
(x >= 77 && y >= 37) ||
(x >= 60 && x <= 64 && y <= 8) ||
(x >= 27 && x <= 33 && y >= 28 && y <= 43)
) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -645,7 +645,37 @@ const unitTexture: Record<string, string> = {
'fan-castle-ambusher-c': 'unit-rebel',
'fan-castle-officer-yu-jin': 'unit-rebel-leader',
'fan-castle-officer-cao-ren': 'unit-rebel-leader',
'fan-castle-leader-pang-de': 'unit-rebel-cavalry'
'fan-castle-leader-pang-de': 'unit-rebel-cavalry',
'han-river-scout-a': 'unit-rebel',
'han-river-scout-b': 'unit-rebel',
'han-river-scout-c': 'unit-rebel',
'han-river-infantry-a': 'unit-rebel',
'han-river-infantry-b': 'unit-rebel',
'han-river-infantry-c': 'unit-rebel',
'han-river-infantry-d': 'unit-rebel',
'han-river-infantry-e': 'unit-rebel',
'han-river-archer-a': 'unit-rebel-archer',
'han-river-archer-b': 'unit-rebel-archer',
'han-river-archer-c': 'unit-rebel-archer',
'han-river-archer-d': 'unit-rebel-archer',
'han-river-archer-e': 'unit-rebel-archer',
'han-river-cavalry-a': 'unit-rebel-cavalry',
'han-river-cavalry-b': 'unit-rebel-cavalry',
'han-river-cavalry-c': 'unit-rebel-cavalry',
'han-river-cavalry-d': 'unit-rebel-cavalry',
'han-river-guard-a': 'unit-rebel',
'han-river-guard-b': 'unit-rebel',
'han-river-guard-c': 'unit-rebel',
'han-river-guard-d': 'unit-rebel',
'han-river-strategist-a': 'unit-rebel-archer',
'han-river-strategist-b': 'unit-rebel-archer',
'han-river-strategist-c': 'unit-rebel-archer',
'han-river-ambusher-a': 'unit-rebel',
'han-river-ambusher-b': 'unit-rebel',
'han-river-ambusher-c': 'unit-rebel',
'han-river-officer-pang-de': 'unit-rebel-cavalry',
'han-river-officer-cao-ren': 'unit-rebel-leader',
'han-river-leader-yu-jin': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -1748,7 +1778,37 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'fan-castle-ambusher-c': 'aggressive',
'fan-castle-officer-yu-jin': 'guard',
'fan-castle-officer-cao-ren': 'guard',
'fan-castle-leader-pang-de': 'aggressive'
'fan-castle-leader-pang-de': 'aggressive',
'han-river-scout-a': 'aggressive',
'han-river-scout-b': 'aggressive',
'han-river-scout-c': 'aggressive',
'han-river-infantry-a': 'guard',
'han-river-infantry-b': 'guard',
'han-river-infantry-c': 'guard',
'han-river-infantry-d': 'guard',
'han-river-infantry-e': 'guard',
'han-river-archer-a': 'hold',
'han-river-archer-b': 'hold',
'han-river-archer-c': 'hold',
'han-river-archer-d': 'hold',
'han-river-archer-e': 'hold',
'han-river-cavalry-a': 'aggressive',
'han-river-cavalry-b': 'aggressive',
'han-river-cavalry-c': 'aggressive',
'han-river-cavalry-d': 'aggressive',
'han-river-guard-a': 'guard',
'han-river-guard-b': 'guard',
'han-river-guard-c': 'guard',
'han-river-guard-d': 'guard',
'han-river-strategist-a': 'hold',
'han-river-strategist-b': 'hold',
'han-river-strategist-c': 'hold',
'han-river-ambusher-a': 'aggressive',
'han-river-ambusher-b': 'aggressive',
'han-river-ambusher-c': 'aggressive',
'han-river-officer-pang-de': 'aggressive',
'han-river-officer-cao-ren': 'guard',
'han-river-leader-yu-jin': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -5,6 +5,7 @@ import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
import fifteenthBattleMapUrl from '../../assets/images/battle/fifteenth-battle-map.svg';
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-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';
@@ -136,6 +137,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-thirty-seventh', thirtySeventhBattleMapUrl);
this.load.image('battle-map-thirty-eighth', thirtyEighthBattleMapUrl);
this.load.image('battle-map-thirty-ninth', thirtyNinthBattleMapUrl);
this.load.image('battle-map-fortieth', fortiethBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -20,6 +20,7 @@ import {
guiyangRecruitBonds,
guiyangRecruitUnits,
hanzhongDecisiveBonds,
hanRiverFloodBonds,
hanzhongOpeningRecruitBonds,
hanzhongOpeningRecruitUnits,
hanzhongMainRecruitBonds,
@@ -353,7 +354,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'thirty-sixth-battle-dingjun-vanguard',
'thirty-seventh-battle-hanzhong-decisive',
'thirty-eighth-battle-jing-defense',
'thirty-ninth-battle-fan-castle-vanguard'
'thirty-ninth-battle-fan-castle-vanguard',
'fortieth-battle-han-river-flood'
],
nextHints: ['왕업 선언', '오호대장군', '북벌 준비']
}
@@ -398,7 +400,8 @@ const campBattleIds = {
thirtySixth: 'thirty-sixth-battle-dingjun-vanguard',
thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive',
thirtyEighth: 'thirty-eighth-battle-jing-defense',
thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard'
thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard',
fortieth: 'fortieth-battle-han-river-flood'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -847,6 +850,20 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-liang', reason: '형주 민심과 보급선을 안정시키며 관우 전선을 보조합니다.' }
],
note: '번성 외곽 압박전은 스물두 명 중 일곱 명만 출전합니다. 유비와 관우는 필수 출전하며, 나머지 다섯 자리에 기동, 원거리, 책략, 보급을 어떻게 배치할지 고르는 전투입니다.'
},
'fortieth-battle-han-river-flood': {
maxUnits: 7,
requiredUnitIds: ['liu-bei', 'guan-yu'],
recommended: [
{ unitId: 'liu-bei', reason: '칠군을 흔드는 대의명분과 패배 조건의 중심입니다.' },
{ unitId: 'guan-yu', reason: '한수 수공의 전선 지휘관이며 생존 목표입니다.' },
{ unitId: 'fa-zheng', reason: '수위와 둑길을 읽어 수공의 타이밍을 잡는 핵심 참모입니다.' },
{ unitId: 'ma-liang', reason: '형주 현지의 제방과 민심을 함께 살피는 보조 책사입니다.' },
{ unitId: 'huang-quan', reason: '물길과 퇴로를 신중하게 판정해 포위 안정성을 높입니다.' },
{ unitId: 'zhao-yun', reason: '제방과 나루 사이를 빠르게 보강하는 기동 축입니다.' },
{ unitId: 'ma-chao', reason: '방덕 기병의 돌파를 맞받아치는 기병 대응 카드입니다.' }
],
note: '한수 수공은 스물두 명 중 일곱 명만 출전합니다. 유비와 관우는 필수이며, 나머지 다섯 자리에 물길 참모, 기동대, 원거리 압박을 어떻게 섞을지 고르는 전투입니다.'
}
};
@@ -4090,6 +4107,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 13
}
]
},
{
id: 'guan-huangquan-after-han-river',
title: '물길의 뒷정리',
availableAfterBattleIds: [campBattleIds.fortieth],
unitIds: ['guan-yu', 'huang-quan'],
bondId: 'guan-yu__huang-quan',
rewardExp: 42,
lines: [
'황권: 물이 이긴 뒤가 더 어렵습니다. 젖은 병사와 흔들린 백성을 함께 다독여야 합니다.',
'관우: 적을 무너뜨린 물길이 백성의 삶까지 무너뜨려서는 안 되오. 그대의 신중함이 필요하오.',
'황권: 둑의 약한 곳과 마을의 낮은 길을 먼저 적겠습니다. 다음 포위전은 뒤가 단단해야 오래 갑니다.'
],
choices: [
{
id: 'inspect-flooded-villages',
label: '잠긴 마을을 살핀다',
response: '황권은 낮은 마을의 길을 정리했고, 관우군은 백성을 해치지 않는 수공 군령을 다시 세웠습니다.',
rewardExp: 16
},
{
id: 'write-bank-repair-orders',
label: '제방 보수 군령을 쓴다',
response: '관우와 황권이 제방 보수 순서를 정리하며 번성 포위 뒤의 민심 관리가 조금 더 단단해졌습니다.',
rewardExp: 15
}
]
},
{
id: 'guan-li-after-han-river',
title: '젖은 보급선',
availableAfterBattleIds: [campBattleIds.fortieth],
unitIds: ['guan-yu', 'li-yan'],
bondId: 'guan-yu__li-yan',
rewardExp: 40,
lines: [
'이엄: 수공은 이겼지만 곡식도 젖었습니다. 병사들이 물을 이기려면 창고가 먼저 말라야 합니다.',
'관우: 앞에서 칼이 이겨도 뒤의 곡식이 상하면 오래 버티지 못하오. 창고를 맡아 주시오.',
'이엄: 젖은 포대를 버릴 것과 살릴 것을 나누겠습니다. 다음 포위전에서 병사들의 밥이 끊기지 않게 하겠습니다.'
],
choices: [
{
id: 'dry-flooded-grain',
label: '젖은 곡식을 말린다',
response: '이엄이 곡식을 나누어 말리게 하자 병사들의 보급 불안이 줄었습니다.',
rewardExp: 15
},
{
id: 'split-siege-rations',
label: '포위전 군량을 나눈다',
response: '관우군의 군량이 여러 창고에 나뉘어 다음 장기전 준비가 더 안정되었습니다.',
rewardExp: 14
}
]
},
{
id: 'guan-fa-after-han-river',
title: '수공의 대가',
availableAfterBattleIds: [campBattleIds.fortieth],
unitIds: ['guan-yu', 'fa-zheng'],
bondId: 'guan-yu__fa-zheng',
rewardExp: 38,
lines: [
'법정: 우금의 본대는 무너졌지만, 물이 만든 승리는 오래 설명해야 합니다. 두려움도 함께 남기 때문입니다.',
'관우: 군령이 칼보다 무거운 날이었소. 다음에는 성문 앞에서 그 무게를 다시 견뎌야 하오.',
'법정: 방덕과 조인의 반응을 보면 아직 끝난 싸움은 아닙니다. 승리 뒤의 긴장을 놓치지 않겠습니다.'
],
choices: [
{
id: 'explain-water-orders',
label: '수공 군령을 설명한다',
response: '법정은 병사들에게 물길을 쓴 이유와 다음 포위전의 순서를 설명했고, 관우군의 동요가 줄었습니다.',
rewardExp: 14
},
{
id: 'prepare-siege-after-flood',
label: '포위전 순서를 정한다',
response: '관우와 법정은 수공 뒤의 진격 순서를 다시 짰고, 번성 성문을 향한 압박이 더 선명해졌습니다.',
rewardExp: 13
}
]
}
];
@@ -5559,6 +5657,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['콩 12']
}
]
},
{
id: 'han-river-dike-repair',
title: '한수 제방 보수',
location: '한수 남쪽 둑',
availableAfterBattleIds: [campBattleIds.fortieth],
bondId: 'guan-yu__huang-quan',
description: '관우와 황권이 수공 뒤 약해진 제방을 살피며, 민심과 다음 포위전의 길을 동시에 다집니다.',
lines: [
'황권: 승리한 물길도 그대로 두면 아군의 뒤를 칩니다. 둑의 약한 곳을 먼저 막아야 합니다.',
'관우: 백성의 밭이 물에 잠기면 승리의 깃발도 가벼워지오. 제방을 보수하시오.',
'병사들은 젖은 흙을 걷어 내고 새 말뚝을 박으며, 다음 전장의 퇴로와 보급로를 다시 표시했습니다.'
],
choices: [
{
id: 'repair-weak-bank',
label: '약한 둑을 먼저 막는다',
response: '제방의 약한 곳이 보수되어 강변 마을과 보급로가 조금 더 안정되었습니다.',
bondExp: 34,
itemRewards: ['제방 보수도 1']
},
{
id: 'mark-dry-routes',
label: '마른길을 표시한다',
response: '황권은 물이 빠지는 길을 표시했고, 관우군은 다음 포위전의 이동 순서를 다시 익혔습니다.',
bondExp: 32,
gold: 760,
itemRewards: ['상처약 5']
}
]
},
{
id: 'han-river-prisoners',
title: '칠군 포로 정리',
location: '한수 임시 포로진',
availableAfterBattleIds: [campBattleIds.fortieth],
bondId: 'guan-yu__li-yan',
description: '관우와 이엄이 물에 젖은 포로와 노획 물자를 정리하며, 번성 포위 전의 후방 부담을 줄입니다.',
lines: [
'이엄: 포로를 굶기면 후방이 흔들리고, 너무 풀어 주면 적의 귀가 됩니다. 순서가 필요합니다.',
'관우: 항복한 병사를 함부로 대하면 의로움이 상하오. 다만 군령은 엄정해야 하오.',
'포로진에는 이름표와 식량 표식이 붙었고, 노획 물자는 공성 준비에 쓸 것과 백성에게 돌릴 것으로 나뉘었습니다.'
],
choices: [
{
id: 'separate-prisoner-units',
label: '포로 부대를 나눈다',
response: '포로들이 소대별로 정리되어 후방 혼란이 줄고, 이엄의 보급 관리가 더 쉬워졌습니다.',
bondExp: 32,
itemRewards: ['칠군 항복문 1', '콩 10']
},
{
id: 'return-flooded-tools',
label: '노획 농기구를 돌려준다',
response: '젖은 농기구가 강변 백성에게 돌아가자 수공 뒤의 민심이 조금 누그러졌습니다.',
bondExp: 30,
gold: 860,
itemRewards: ['탁주 3']
}
]
}
];
@@ -5645,6 +5803,55 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.fortieth) {
this.campaign = ensureCampaignRosterUnits(
[
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits,
...chengduSurrenderRecruitUnits,
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits
],
[
...xuzhouRecruitBonds,
...caoBreakRecruitBonds,
...liuBiaoRecruitBonds,
...zhugeRecruitBonds,
...jingzhouRecruitBonds,
...guiyangRecruitBonds,
...wulingRecruitBonds,
...changshaRecruitBonds,
...yizhouRecruitBonds,
...fuPassRecruitBonds,
...luoCastleRecruitBonds,
...luoCastleProperRecruitBonds,
...chengduPressureRecruitBonds,
...chengduSurrenderRecruitBonds,
...hanzhongOpeningRecruitBonds,
...hanzhongScoutRecruitBonds,
...hanzhongMainRecruitBonds,
...hanzhongDecisiveBonds,
...jingDefenseBonds,
...fanCastleBonds,
...hanRiverFloodBonds
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (battleId === campBattleIds.thirtyNinth) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -6273,6 +6480,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'hanzhong-king-camp') {
return '한중왕 즉위 준비 군영';
}
if (battleId === campBattleIds.fortieth) {
return '한수 수공 후 군영';
}
if (battleId === campBattleIds.thirtyNinth) {
return '번성 외곽 압박전 후 군영';
}

View File

@@ -5,6 +5,7 @@ import {
eighthBattleScenario,
eighteenthBattleScenario,
eleventhBattleScenario,
fortiethBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -378,6 +379,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'thirty-seventh-camp' ||
campaign.step === 'thirty-eighth-camp' ||
campaign.step === 'thirty-ninth-camp' ||
campaign.step === 'fortieth-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp'
) {
@@ -580,6 +582,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'fortieth-battle') {
this.scene.start('BattleScene', { battleId: fortiethBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -120,6 +120,8 @@ export type CampaignStep =
| 'thirty-eighth-camp'
| 'thirty-ninth-battle'
| 'thirty-ninth-camp'
| 'fortieth-battle'
| 'fortieth-camp'
| 'hanzhong-king-camp'
| 'shu-han-foundation-camp';
@@ -222,7 +224,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'thirty-sixth-battle-dingjun-vanguard': { victory: 'thirty-sixth-camp', retry: 'thirty-sixth-battle' },
'thirty-seventh-battle-hanzhong-decisive': { victory: 'thirty-seventh-camp', retry: 'thirty-seventh-battle' },
'thirty-eighth-battle-jing-defense': { victory: 'thirty-eighth-camp', retry: 'thirty-eighth-battle' },
'thirty-ninth-battle-fan-castle-vanguard': { victory: 'thirty-ninth-camp', retry: 'thirty-ninth-battle' }
'thirty-ninth-battle-fan-castle-vanguard': { victory: 'thirty-ninth-camp', retry: 'thirty-ninth-battle' },
'fortieth-battle-han-river-flood': { victory: 'fortieth-camp', retry: 'fortieth-battle' }
};
export type CampaignSaveSlotSummary = {