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),