Add Dingjun Mountain vanguard chapter

This commit is contained in:
2026-06-23 13:38:49 +09:00
parent c724d9d48f
commit 24b7db0b14
12 changed files with 894 additions and 39 deletions

View File

@@ -194,6 +194,15 @@ export const itemCatalog: Record<string, ItemDefinition> = {
strategyBonus: 2,
effects: ['공명 연계 판정 보정']
},
'mountain-guide-scroll': {
id: 'mountain-guide-scroll',
name: '산악 지형도',
slot: 'accessory',
rank: 'treasure',
description: '한중 산길과 능선의 보급로를 적은 지형도.',
strategyBonus: 2,
effects: ['산지와 숲 지형 판단에 도움을 준다']
},
'bravery-token': {
id: 'bravery-token',
name: '호담패',

View File

@@ -63,6 +63,10 @@ import {
thirtyThirdBattleMap,
thirtyThirdBattleUnits,
thirtyThirdBattleVictoryPages,
thirtySixthBattleBonds,
thirtySixthBattleMap,
thirtySixthBattleUnits,
thirtySixthBattleVictoryPages,
thirteenthBattleBonds,
thirteenthBattleMap,
thirteenthBattleUnits,
@@ -180,7 +184,8 @@ export type BattleScenarioId =
| 'thirty-second-battle-mianzhu-gate'
| 'thirty-third-battle-chengdu-surrender'
| 'thirty-fourth-battle-jiameng-pass'
| 'thirty-fifth-battle-yangping-scout';
| 'thirty-fifth-battle-yangping-scout'
| 'thirty-sixth-battle-dingjun-vanguard';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2245,6 +2250,69 @@ export const thirtyFifthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const thirtySixthBattleScenario: BattleScenarioDefinition = {
id: 'thirty-sixth-battle-dingjun-vanguard',
title: '정군산 전초전',
victoryConditionLabel: '하후연 선봉 격파',
defeatConditionLabel: '유비 또는 황충 퇴각',
openingObjectiveLines: [
'정군산은 한중 전선의 고지입니다. 능선을 장악하면 한중 본전의 주도권을 유비군이 가져올 수 있습니다.',
'황충과 법정의 계책이 이번 전투의 중심입니다. 황충이 무너지면 정군산의 기세도 꺾입니다.',
'마초와 마대는 서쪽 능선을 흔들고, 왕평을 설득할 실마리를 찾아 한중 산길의 마음을 유비군 쪽으로 돌리십시오.'
],
map: thirtySixthBattleMap,
units: thirtySixthBattleUnits,
bonds: thirtySixthBattleBonds,
mapTextureKey: 'battle-map-thirty-sixth',
leaderUnitId: 'dingjun-leader-xiahou-yuan',
quickVictoryTurnLimit: 54,
baseVictoryGold: 6240,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '하후연 선봉 격파',
rewardGold: 3920,
unitId: 'dingjun-leader-xiahou-yuan'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 1000,
unitId: 'liu-bei'
},
{
id: 'huang-zhong',
kind: 'keep-unit-alive',
label: '황충 생존',
rewardGold: 1640,
unitId: 'huang-zhong'
},
{
id: 'dingjun-ridge-storehouse',
kind: 'secure-terrain',
label: '정군산 보급 창고 확보',
rewardGold: 1860,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '54턴 이내 승리',
rewardGold: 1440,
maxTurn: 54
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'huang-zhong' }
],
itemRewards: ['콩 26', '상처약 20', '탁주 9', '산악 지형도 +1', '왕평 합류'],
victoryPages: thirtySixthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -2282,7 +2350,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario,
'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario,
'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario,
'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario
'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario,
'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -22,6 +22,7 @@ import {
thirtyFirstBattleScenario,
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
thirtySixthBattleScenario,
thirtiethBattleScenario,
twentyEighthBattleScenario,
twentyNinthBattleScenario,
@@ -81,6 +82,8 @@ import {
thirtySecondBattleVictoryPages,
thirtyThirdBattleIntroPages,
thirtyThirdBattleVictoryPages,
thirtySixthBattleIntroPages,
thirtySixthBattleVictoryPages,
thirtiethBattleIntroPages,
thirtiethBattleVictoryPages,
twentyEighthBattleIntroPages,
@@ -487,13 +490,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[thirtyFifthBattleScenario.id]: {
afterBattleId: thirtyFifthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '한중 본전 준비',
eyebrow: '다음 장',
title: thirtySixthBattleScenario.title,
description:
'양평관 정찰로 한중의 길목과 장로군의 배치를 확인했습니다. 마대까지 합류했으니 다음은 한중을 둘러싼 본격적인 전선을 준비해야 합니다.',
rewardHint: '다음 장: 한중 본전 준비 중',
pages: thirtyFifthBattleVictoryPages,
unavailableNotice: '한중 본전과 촉한 건국 선포는 다음 작업에서 이어집니다. 마초와 마대의 기병 운용, 황권의 지형 판단, 제갈량의 전선 설계를 준비하십시오.'
'양평관 정찰로 한중의 길목과 장로군의 배치를 확인했습니다. 이제 정군산으로 올라 하후연의 선봉을 흔들고, 한중 산길을 아는 왕평을 유비군 쪽으로 끌어들일 차례입니다.',
rewardHint: `예상 보상: ${thirtySixthBattleScenario.title} 개방 / 왕평 합류`,
nextBattleId: thirtySixthBattleScenario.id,
campaignStep: 'thirty-sixth-battle',
pages: [...thirtyFifthBattleVictoryPages, ...thirtySixthBattleIntroPages]
},
[thirtySixthBattleScenario.id]: {
afterBattleId: thirtySixthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '한중 결전 준비',
description:
'정군산 전초전으로 한중의 고지를 확보했고 왕평이 합류했습니다. 다음은 한중 결전과 촉한 건국 선포로 이어질 큰 흐름을 준비해야 합니다.',
rewardHint: '다음 장: 한중 결전 준비 중',
pages: thirtySixthBattleVictoryPages,
unavailableNotice: '한중 결전과 촉한 건국 선포는 다음 작업에서 이어집니다. 황충, 법정, 왕평의 산악 전선과 마초·마대의 기병 축을 준비하십시오.'
}
};

View File

@@ -2262,6 +2262,68 @@ export const thirtyFifthBattleVictoryPages: StoryPage[] = [
}
];
export const thirtySixthBattleIntroPages: StoryPage[] = [
{
id: 'thirty-sixth-dingjun-mountain',
bgm: 'story-dark',
chapter: '정군산으로',
background: 'story-three-heroes',
speaker: '제갈량',
text: '양평관의 길을 살핀 뒤 한중의 핵심은 정군산으로 모였습니다. 산을 잡으면 한중을 굽어보고, 산을 잃으면 북문 전체가 흔들립니다.'
},
{
id: 'thirty-sixth-huang-zhong-plan',
bgm: 'battle-prep',
chapter: '노장의 결심',
background: 'story-liu-bei',
speaker: '황충',
text: '나이가 전열을 막지는 못합니다. 법정의 계책으로 능선을 잡아 주신다면, 이 늙은 활과 칼이 하후연의 기세를 꺾어 보이겠습니다.'
},
{
id: 'thirty-sixth-western-cavalry',
bgm: 'battle-prep',
chapter: '서량 기병의 우회로',
background: 'story-militia',
speaker: '마초',
text: '정군산 서쪽 능선은 말이 달리기 어렵지만, 마대와 함께 길을 잡으면 적의 눈을 돌릴 수 있습니다. 황충 장군이 치는 순간을 만들겠습니다.'
},
{
id: 'thirty-sixth-sortie',
bgm: 'battle-prep',
chapter: '정군산 전초전',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '정군산을 얻으면 한중의 이름이 가까워질 것이오. 황충과 법정은 산을 보고, 마초와 마대는 길을 흔들며, 모두 유비군의 깃발을 산 위에 세우시오.'
}
];
export const thirtySixthBattleVictoryPages: StoryPage[] = [
{
id: 'thirty-sixth-victory-ridge-held',
bgm: 'militia-theme',
chapter: '정군산 능선을 잡다',
background: 'story-sortie',
text: '정군산의 첫 능선이 유비군 손에 들어오자 한중 전선의 무게가 바뀌었습니다. 하후연의 선봉은 물러났고, 산길의 병사들은 새 주인을 가늠하기 시작했습니다.'
},
{
id: 'thirty-sixth-wang-ping-joins',
bgm: 'battle-prep',
chapter: '왕평의 투항',
background: 'story-liu-bei',
speaker: '왕평',
text: '저는 한중의 산길과 병사들의 마음을 압니다. 조조군의 군율은 엄하나 이 땅을 품지는 못했습니다. 유비공께서 한중을 안정시킨다면 제 길도 그곳에 있겠습니다.'
},
{
id: 'thirty-sixth-next-foundation',
bgm: 'story-dark',
chapter: '한중의 깃발',
background: 'story-three-heroes',
speaker: '제갈량',
text: '정군산의 승리와 왕평의 합류로 한중의 산길은 조금씩 유비군 편으로 기울었습니다. 이제 한중 본전과 촉한의 이름이 더 가까워집니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2617,6 +2679,12 @@ export const thirtyFifthBattleMap: BattleMap = {
terrain: createThirtyFifthBattleTerrain()
};
export const thirtySixthBattleMap: BattleMap = {
width: 72,
height: 54,
terrain: createThirtySixthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4882,6 +4950,30 @@ export const hanzhongScoutRecruitUnits: UnitData[] = [
}
];
export const hanzhongMainRecruitUnits: UnitData[] = [
{
id: 'wang-ping',
name: '왕평',
faction: 'ally',
className: '한중 보병',
classKey: 'spearman',
level: 25,
exp: 0,
hp: 54,
maxHp: 54,
attack: 17,
move: 4,
stats: { might: 82, intelligence: 70, leadership: 78, agility: 72, luck: 66 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 22 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 18 },
accessory: { itemId: 'mountain-guide-scroll', level: 2, exp: 10 }
},
x: 15,
y: 36
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -13667,6 +13759,77 @@ export const thirtyFifthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('yangping-leader-zhang-wei', '장위', '양평관 선봉장', 'rebelLeader', 73, 382, 108, 66, 24)
];
const thirtySixthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 4, y: 48 },
'guan-yu': { x: 5, y: 46 },
'zhang-fei': { x: 4, y: 50 },
'jian-yong': { x: 3, y: 51 },
'mi-zhu': { x: 5, y: 52 },
'sun-qian': { x: 7, y: 51 },
'zhao-yun': { x: 8, y: 47 },
'zhuge-liang': { x: 6, y: 46 },
'ma-liang': { x: 8, y: 52 },
'yi-ji': { x: 9, y: 49 },
'gong-zhi': { x: 10, y: 52 },
'huang-zhong': { x: 7, y: 48 },
'wei-yan': { x: 10, y: 46 },
'pang-tong': { x: 9, y: 47 },
'fa-zheng': { x: 11, y: 49 },
'wu-yi': { x: 12, y: 47 },
'yan-yan': { x: 11, y: 45 },
'li-yan': { x: 13, y: 46 },
'huang-quan': { x: 14, y: 48 },
'ma-chao': { x: 13, y: 44 },
'ma-dai': { x: 15, y: 45 }
};
export const thirtySixthBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits,
...chengduSurrenderRecruitUnits,
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits
].map((unit) => placeScenarioUnit(unit, thirtySixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('dingjun-scout-a', '정군산 척후', '서령 척후', 'bandit', 72, 238, 82, 20, 47),
createThirtyFirstEnemyUnit('dingjun-scout-b', '정군산 척후', '산길 척후', 'bandit', 72, 238, 82, 29, 45),
createThirtyFirstEnemyUnit('dingjun-scout-c', '정군산 척후', '강변 척후', 'bandit', 73, 246, 84, 38, 43),
createThirtyFirstEnemyUnit('dingjun-infantry-a', '조조 보병', '서령 보병', 'yellowTurban', 73, 348, 100, 36, 40),
createThirtyFirstEnemyUnit('dingjun-infantry-b', '조조 보병', '중턱 보병', 'yellowTurban', 73, 348, 100, 42, 39),
createThirtyFirstEnemyUnit('dingjun-infantry-c', '하후연 보병', '능선 보병', 'yellowTurban', 74, 360, 104, 49, 35),
createThirtyFirstEnemyUnit('dingjun-infantry-d', '하후연 보병', '본진 보병', 'yellowTurban', 74, 360, 104, 53, 33),
createThirtyFirstEnemyUnit('dingjun-infantry-e', '정군산 보병', '창고 보병', 'yellowTurban', 75, 372, 106, 63, 30),
createThirtyFirstEnemyUnit('dingjun-archer-a', '조조 궁병', '서벽 궁병', 'archer', 73, 246, 84, 39, 37),
createThirtyFirstEnemyUnit('dingjun-archer-b', '조조 궁병', '능선 궁병', 'archer', 74, 254, 86, 51, 29),
createThirtyFirstEnemyUnit('dingjun-archer-c', '조조 궁병', '고지 궁병', 'archer', 74, 254, 86, 60, 24),
createThirtyFirstEnemyUnit('dingjun-archer-d', '조조 궁병', '창고 궁병', 'archer', 75, 262, 88, 66, 32),
createThirtyFirstEnemyUnit('dingjun-cavalry-a', '조조 기병', '좌익 기병', 'cavalry', 74, 364, 104, 32, 49),
createThirtyFirstEnemyUnit('dingjun-cavalry-b', '조조 기병', '우익 기병', 'cavalry', 74, 364, 104, 47, 44),
createThirtyFirstEnemyUnit('dingjun-cavalry-c', '하후연 기병', '돌격 기병', 'cavalry', 75, 376, 106, 59, 38),
createThirtyFirstEnemyUnit('dingjun-cavalry-d', '하후연 기병', '친위 기병', 'cavalry', 75, 376, 106, 67, 33),
createThirtyFirstEnemyUnit('dingjun-guard-a', '하후연 친위대', '능선 친위대', 'yellowTurban', 75, 388, 108, 56, 29),
createThirtyFirstEnemyUnit('dingjun-guard-b', '하후연 친위대', '본진 친위대', 'yellowTurban', 75, 388, 108, 64, 28),
createThirtyFirstEnemyUnit('dingjun-strategist-a', '조조 책사', '고지 책사', 'strategist', 74, 254, 88, 54, 25),
createThirtyFirstEnemyUnit('dingjun-strategist-b', '조조 책사', '본진 책사', 'strategist', 75, 262, 90, 66, 22),
createThirtyFirstEnemyUnit('dingjun-ambusher-a', '조조 복병', '남령 복병', 'bandit', 73, 248, 86, 43, 47),
createThirtyFirstEnemyUnit('dingjun-ambusher-b', '조조 복병', '동령 복병', 'bandit', 73, 248, 86, 56, 41),
createThirtyFirstEnemyUnit('dingjun-officer-zhang-he', '장합', '조조 선봉장', 'cavalry', 76, 392, 112, 61, 27),
createThirtyFirstEnemyUnit('dingjun-officer-wang-ping', '왕평', '한중 길잡이', 'spearman', 75, 330, 100, 58, 31),
createThirtyFirstEnemyUnit('dingjun-leader-xiahou-yuan', '하후연', '정군산 주장', 'rebelLeader', 77, 418, 116, 68, 25)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -14126,6 +14289,33 @@ export const hanzhongScoutRecruitBonds: BattleBond[] = [
}
];
export const hanzhongMainRecruitBonds: BattleBond[] = [
{
id: 'huang-zhong__wang-ping',
unitIds: ['huang-zhong', 'wang-ping'],
title: '노장과 산길',
level: 28,
exp: 0,
description: '황충의 과감한 고지 돌파와 왕평의 산길 판단이 맞물려 정군산 전투의 흐름을 안정시킵니다.'
},
{
id: 'zhuge-liang__wang-ping',
unitIds: ['zhuge-liang', 'wang-ping'],
title: '한중 지형책',
level: 26,
exp: 0,
description: '제갈량의 큰 전선 설계와 왕평의 실제 산길 지식이 만나 한중 공략의 정확도를 높입니다.'
},
{
id: 'ma-dai__wang-ping',
unitIds: ['ma-dai', 'wang-ping'],
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);
@@ -14174,6 +14364,9 @@ export const thirtyFourthBattleBonds: BattleBond[] = [...thirtyThirdBattleBonds,
export const thirtyFifthBattleBonds: BattleBond[] = [...thirtyFourthBattleBonds, ...hanzhongOpeningRecruitBonds].map(
cloneBattleBondForScenario
);
export const thirtySixthBattleBonds: BattleBond[] = [...thirtyFifthBattleBonds, ...hanzhongScoutRecruitBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -15709,6 +15902,77 @@ function createThirtyFifthBattleTerrain(): TerrainType[][] {
);
}
function createThirtySixthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 54 }, (_, y) =>
Array.from({ length: 72 }, (_, x): TerrainType => {
if (x <= 4 && y >= 47) {
return 'camp';
}
if (
(x >= 54 && x <= 70 && y >= 12 && y <= 31) ||
(x >= 50 && x <= 62 && y >= 8 && y <= 20) ||
(x >= 60 && x <= 71 && y >= 23 && y <= 38) ||
(x >= 57 && x <= 65 && y >= 34 && y <= 41)
) {
return 'fort';
}
if (
(x >= 19 && x <= 23 && y >= 45 && y <= 47) ||
(x >= 39 && x <= 42 && y >= 39 && y <= 41) ||
(x >= 50 && x <= 52 && y >= 34 && y <= 36) ||
(x >= 62 && x <= 65 && y >= 37 && y <= 40)
) {
return 'village';
}
if (
(y >= 49 && x >= 4 && x <= 24) ||
(x >= 23 && x <= 36 && y >= 45 && y <= 49) ||
(x >= 35 && x <= 50 && y >= 40 && y <= 45) ||
(x >= 49 && x <= 61 && y >= 34 && y <= 40) ||
(x >= 60 && x <= 69 && y >= 28 && y <= 34) ||
(x >= 64 && x <= 71 && y >= 24 && y <= 28)
) {
return 'road';
}
if ((x === 17 || x === 18) && y >= 0 && y <= 43) {
return 'river';
}
if ((x === 40 || x === 41) && y >= 3 && y <= 38) {
return 'river';
}
if ((x === 52 || x === 53) && y >= 39 && y <= 53) {
return 'river';
}
if (
(x >= 4 && x <= 25 && y >= 8 && y <= 38) ||
(x >= 26 && x <= 39 && y >= 15 && y <= 39) ||
(x >= 43 && x <= 57 && y >= 41 && y <= 53) ||
(x >= 55 && x <= 71 && y >= 37 && y <= 52)
) {
return 'forest';
}
if (
(x >= 0 && x <= 16 && y <= 22) ||
(x >= 26 && x <= 39 && y <= 14) ||
(x >= 40 && x <= 58 && y >= 1 && y <= 17) ||
(x >= 60 && x <= 71 && y <= 15) ||
(x >= 30 && x <= 44 && y >= 46 && y <= 53)
) {
return 'hill';
}
if (
(x <= 2 && y <= 46) ||
(x >= 69 && y >= 33) ||
(x >= 54 && x <= 57 && y <= 8) ||
(x >= 25 && x <= 29 && y >= 24 && y <= 36)
) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),