Add Fan Castle vanguard sortie chapter

This commit is contained in:
2026-06-23 15:46:28 +09:00
parent 8585fec6b5
commit fe1f897609
11 changed files with 935 additions and 55 deletions

View File

@@ -51,6 +51,10 @@ import {
thirtyEighthBattleMap,
thirtyEighthBattleUnits,
thirtyEighthBattleVictoryPages,
thirtyNinthBattleBonds,
thirtyNinthBattleMap,
thirtyNinthBattleUnits,
thirtyNinthBattleVictoryPages,
thirtyFourthBattleBonds,
thirtyFourthBattleMap,
thirtyFourthBattleUnits,
@@ -195,7 +199,8 @@ export type BattleScenarioId =
| 'thirty-fifth-battle-yangping-scout'
| 'thirty-sixth-battle-dingjun-vanguard'
| 'thirty-seventh-battle-hanzhong-decisive'
| 'thirty-eighth-battle-jing-defense';
| 'thirty-eighth-battle-jing-defense'
| 'thirty-ninth-battle-fan-castle-vanguard';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2449,6 +2454,69 @@ export const thirtyEighthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const thirtyNinthBattleScenario: BattleScenarioDefinition = {
id: 'thirty-ninth-battle-fan-castle-vanguard',
title: '번성 외곽 압박전',
victoryConditionLabel: '방덕 선봉 격파',
defeatConditionLabel: '유비 또는 관우 퇴각',
openingObjectiveLines: [
'형주 방위가 안정된 사이 관우는 번성 외곽으로 북상했습니다. 조조군의 보루와 나루를 먼저 장악해야 다음 결전을 열 수 있습니다.',
'이번 전투는 관우가 핵심입니다. 관우가 무너지면 번성 전선의 사기가 흔들리니, 유비와 관우를 함께 지키며 전선을 밀어 올리십시오.',
'성채 안쪽의 조인, 수로를 보는 우금, 기병을 이끄는 방덕이 서로 다른 방식으로 버틸 것입니다. 기동대와 책사를 골고루 출전시키면 공명과 성장 기회가 커집니다.'
],
map: thirtyNinthBattleMap,
units: thirtyNinthBattleUnits,
bonds: thirtyNinthBattleBonds,
mapTextureKey: 'battle-map-thirty-ninth',
leaderUnitId: 'fan-castle-leader-pang-de',
quickVictoryTurnLimit: 62,
baseVictoryGold: 7600,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '방덕 선봉 격파',
rewardGold: 4800,
unitId: 'fan-castle-leader-pang-de'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 1200,
unitId: 'liu-bei'
},
{
id: 'guan-yu',
kind: 'keep-unit-alive',
label: '관우 생존',
rewardGold: 1900,
unitId: 'guan-yu'
},
{
id: 'outer-fort',
kind: 'secure-terrain',
label: '번성 외곽 보루 확보',
rewardGold: 2320,
terrain: 'fort'
},
{
id: 'quick',
kind: 'quick-victory',
label: '62턴 이내 승리',
rewardGold: 1720,
maxTurn: 62
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'guan-yu' }
],
itemRewards: ['콩 34', '상처약 25', '탁주 10', '번성 외곽도 1', '한수 수공 준비'],
victoryPages: thirtyNinthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -2489,7 +2557,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario,
'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario,
'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario,
'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario
'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario,
'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -21,6 +21,7 @@ import {
thirtyFourthBattleScenario,
thirtyEighthBattleScenario,
thirtyFirstBattleScenario,
thirtyNinthBattleScenario,
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
thirtySixthBattleScenario,
@@ -84,6 +85,8 @@ import {
thirtyEighthBattleVictoryPages,
thirtyFirstBattleIntroPages,
thirtyFirstBattleVictoryPages,
thirtyNinthBattleIntroPages,
thirtyNinthBattleVictoryPages,
thirtySecondBattleIntroPages,
thirtySecondBattleVictoryPages,
thirtyThirdBattleIntroPages,
@@ -552,13 +555,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[thirtyEighthBattleScenario.id]: {
afterBattleId: thirtyEighthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '번성 전선 준비',
eyebrow: '다음 장',
title: thirtyNinthBattleScenario.title,
description:
'형주 봉화대와 나루를 안정시켰지만, 북쪽 번성의 문과 오군의 시선은 더 큰 압박으로 남았습니다. 다음은 관우의 형주 전선이 본격적으로 시험받는 흐름입니다.',
rewardHint: '다음 장: 번성 전선 준비 중',
pages: thirtyEighthBattleVictoryPages,
unavailableNotice: '번성 전선과 관우의 북상은 다음 작업에서 이어집니다. 형주 방위, 강변 민심, 오·위 양면 압박을 준비하십시오.'
'형주 봉화대와 나루를 안정시켰지만 번성은 아직 닫혀 있습니다. 다음 출진에서는 관우를 축으로 번성 외곽의 보루와 수로를 눌러, 한수 수공과 성문 공략의 발판을 마련합니다.',
rewardHint: `예상 보상: ${thirtyNinthBattleScenario.title} 개방 / 번성 외곽도`,
nextBattleId: thirtyNinthBattleScenario.id,
campaignStep: 'thirty-ninth-battle',
pages: [...thirtyEighthBattleVictoryPages, ...thirtyNinthBattleIntroPages]
},
[thirtyNinthBattleScenario.id]: {
afterBattleId: thirtyNinthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '한수 수공 준비',
description:
'번성 외곽의 보루를 눌렀지만 조인과 우금의 본대는 아직 성 안과 수로를 지키고 있습니다. 다음 장에서는 한수의 물길, 둑, 성벽을 함께 다루는 더 큰 전장이 이어집니다.',
rewardHint: '다음 장: 한수 수공과 칠군 침수 준비 중',
pages: thirtyNinthBattleVictoryPages,
unavailableNotice: '한수 수공과 칠군 침수 전장은 다음 작업에서 이어집니다. 지금은 군영에서 무장 성장, 공명도, 보급을 정비할 수 있습니다.'
}
};

View File

@@ -2525,6 +2525,70 @@ export const thirtyEighthBattleVictoryPages: StoryPage[] = [
}
];
export const thirtyNinthBattleIntroPages: StoryPage[] = [
{
id: 'thirty-ninth-fan-castle-shadow',
bgm: 'story-dark',
chapter: '번성의 그림자',
background: 'story-three-heroes',
speaker: '제갈량',
text: '형주의 봉화는 다시 올랐으나, 번성의 문은 더욱 굳게 닫혔습니다. 조조의 북방 병력은 수로와 성채를 엮어 시간을 벌려 하고, 오의 시선도 강 건너에서 조용히 움직입니다.'
},
{
id: 'thirty-ninth-guan-yu-north',
bgm: 'battle-prep',
chapter: '관우의 북상',
background: 'story-liu-bei',
speaker: '관우',
portrait: 'guanYu',
text: '형주를 지키는 일은 곧 한실을 일으키는 일입니다. 번성 외곽의 보루와 나루를 눌러야 적의 숨통을 조이고, 뒤이어 큰 결전을 열 수 있습니다.'
},
{
id: 'thirty-ninth-flooded-fields',
bgm: 'battle-prep',
chapter: '물길을 보는 눈',
background: 'story-militia',
speaker: '법정',
text: '한수의 물길은 평소에는 느려 보이나, 비와 둑의 높이에 따라 전장의 형세를 통째로 바꿉니다. 이번 전투에서는 성 밖 보급선과 제방 감시초를 먼저 잡아야 합니다.'
},
{
id: 'thirty-ninth-sortie',
bgm: 'battle-prep',
chapter: '번성 외곽 압박전',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '관우가 선봉을 잡되 모두가 그 뒤를 받쳐라. 번성 외곽을 열면 다음 물길과 성문을 함께 도모할 수 있다. 오늘의 선택이 다음 전장의 폭을 넓힐 것이다.'
}
];
export const thirtyNinthBattleVictoryPages: StoryPage[] = [
{
id: 'thirty-ninth-victory-outer-wall',
bgm: 'militia-theme',
chapter: '외곽 보루 함락',
background: 'story-sortie',
text: '번성 외곽의 보루와 나루가 촉한군의 손에 들어왔습니다. 조조군은 성 안쪽으로 물러났고, 방덕의 선봉은 다시 방어진을 고쳐 세우며 다음 격돌을 예고했습니다.'
},
{
id: 'thirty-ninth-pang-de-resolve',
bgm: 'battle-prep',
chapter: '방덕의 결의',
background: 'story-three-heroes',
speaker: '제갈량',
text: '방덕은 물러났으나 기세를 잃지 않았습니다. 조인과 우금도 성 안의 병력을 다시 묶고 있으니, 다음 전투에서는 물길과 성벽이 함께 움직일 것입니다.'
},
{
id: 'thirty-ninth-next-water-lines',
bgm: 'story-dark',
chapter: '한수 수공 준비',
background: 'story-militia',
speaker: '관우',
portrait: 'guanYu',
text: '성은 아직 멀리 있고, 강은 우리 곁에 있소. 병사들에게 둑을 살피게 하고 배와 밧줄을 준비하라. 다음에는 물길마저 우리 편으로 삼아야 하오.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2898,6 +2962,12 @@ export const thirtyEighthBattleMap: BattleMap = {
terrain: createThirtyEighthBattleTerrain()
};
export const thirtyNinthBattleMap: BattleMap = {
width: 78,
height: 60,
terrain: createThirtyNinthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -14199,6 +14269,84 @@ export const thirtyEighthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('jing-defense-leader-lu-meng', '여몽', '오군 도독', 'rebelLeader', 88, 520, 140, 72, 35)
];
const thirtyNinthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 5, y: 54 },
'guan-yu': { x: 10, y: 49 },
'zhang-fei': { x: 6, y: 56 },
'jian-yong': { x: 4, y: 57 },
'mi-zhu': { x: 8, y: 55 },
'sun-qian': { x: 9, y: 58 },
'zhao-yun': { x: 13, y: 52 },
'zhuge-liang': { x: 9, y: 51 },
'ma-liang': { x: 12, y: 54 },
'yi-ji': { x: 14, y: 57 },
'gong-zhi': { x: 15, y: 55 },
'huang-zhong': { x: 16, y: 50 },
'wei-yan': { x: 14, y: 48 },
'pang-tong': { x: 11, y: 52 },
'fa-zheng': { x: 17, y: 54 },
'wu-yi': { x: 18, y: 51 },
'yan-yan': { x: 19, y: 53 },
'li-yan': { x: 17, y: 48 },
'huang-quan': { x: 20, y: 55 },
'ma-chao': { x: 21, y: 49 },
'ma-dai': { x: 22, y: 51 },
'wang-ping': { x: 20, y: 47 }
};
export const thirtyNinthBattleUnits: 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, thirtyNinthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('fan-castle-scout-a', '조조군 척후', '강변 척후', 'bandit', 85, 306, 102, 28, 51),
createThirtyFirstEnemyUnit('fan-castle-scout-b', '조조군 척후', '제방 척후', 'bandit', 85, 306, 102, 35, 48),
createThirtyFirstEnemyUnit('fan-castle-scout-c', '조조군 척후', '북문 척후', 'bandit', 86, 314, 104, 50, 42),
createThirtyFirstEnemyUnit('fan-castle-infantry-a', '번성 보병', '하류 보병', 'yellowTurban', 86, 462, 130, 40, 44),
createThirtyFirstEnemyUnit('fan-castle-infantry-b', '번성 보병', '중로 보병', 'yellowTurban', 86, 462, 130, 48, 41),
createThirtyFirstEnemyUnit('fan-castle-infantry-c', '번성 보병', '성문 보병', 'yellowTurban', 87, 474, 132, 57, 35),
createThirtyFirstEnemyUnit('fan-castle-infantry-d', '번성 보병', '보루 보병', 'yellowTurban', 87, 474, 132, 63, 31),
createThirtyFirstEnemyUnit('fan-castle-infantry-e', '번성 보병', '상류 보병', 'yellowTurban', 88, 486, 134, 69, 25),
createThirtyFirstEnemyUnit('fan-castle-archer-a', '번성 궁병', '제방 궁병', 'archer', 86, 322, 108, 43, 46),
createThirtyFirstEnemyUnit('fan-castle-archer-b', '번성 궁병', '강안 궁병', 'archer', 87, 330, 110, 53, 38),
createThirtyFirstEnemyUnit('fan-castle-archer-c', '번성 궁병', '북문 궁병', 'archer', 87, 330, 110, 62, 33),
createThirtyFirstEnemyUnit('fan-castle-archer-d', '번성 궁병', '보루 궁병', 'archer', 88, 338, 112, 71, 27),
createThirtyFirstEnemyUnit('fan-castle-archer-e', '번성 궁병', '성벽 궁병', 'archer', 88, 338, 112, 73, 19),
createThirtyFirstEnemyUnit('fan-castle-cavalry-a', '방덕 기병', '좌익 기병', 'cavalry', 87, 474, 134, 32, 52),
createThirtyFirstEnemyUnit('fan-castle-cavalry-b', '방덕 기병', '중앙 기병', 'cavalry', 87, 474, 134, 47, 47),
createThirtyFirstEnemyUnit('fan-castle-cavalry-c', '방덕 기병', '우익 기병', 'cavalry', 88, 486, 136, 58, 41),
createThirtyFirstEnemyUnit('fan-castle-cavalry-d', '방덕 기병', '북로 기병', 'cavalry', 88, 486, 136, 68, 35),
createThirtyFirstEnemyUnit('fan-castle-guard-a', '번성 친위대', '외곽 친위대', 'yellowTurban', 89, 512, 140, 59, 32),
createThirtyFirstEnemyUnit('fan-castle-guard-b', '번성 친위대', '성문 친위대', 'yellowTurban', 89, 512, 140, 66, 28),
createThirtyFirstEnemyUnit('fan-castle-guard-c', '번성 친위대', '수문 친위대', 'yellowTurban', 90, 524, 142, 70, 22),
createThirtyFirstEnemyUnit('fan-castle-guard-d', '번성 친위대', '내성 친위대', 'yellowTurban', 90, 524, 142, 74, 17),
createThirtyFirstEnemyUnit('fan-castle-strategist-a', '조조군 책사', '수로 책사', 'strategist', 87, 338, 114, 55, 39),
createThirtyFirstEnemyUnit('fan-castle-strategist-b', '조조군 책사', '성문 책사', 'strategist', 88, 346, 116, 66, 30),
createThirtyFirstEnemyUnit('fan-castle-strategist-c', '조조군 책사', '상류 책사', 'strategist', 89, 354, 118, 72, 21),
createThirtyFirstEnemyUnit('fan-castle-ambusher-a', '번성 복병', '숲길 복병', 'bandit', 86, 316, 106, 38, 53),
createThirtyFirstEnemyUnit('fan-castle-ambusher-b', '번성 복병', '둑길 복병', 'bandit', 87, 324, 108, 52, 45),
createThirtyFirstEnemyUnit('fan-castle-ambusher-c', '번성 복병', '나루 복병', 'bandit', 87, 324, 108, 64, 40),
createThirtyFirstEnemyUnit('fan-castle-officer-yu-jin', '우금', '조조군 도독', 'rebelLeader', 90, 540, 144, 69, 24),
createThirtyFirstEnemyUnit('fan-castle-officer-cao-ren', '조인', '번성 수비장', 'rebelLeader', 91, 552, 146, 73, 18),
createThirtyFirstEnemyUnit('fan-castle-leader-pang-de', '방덕', '번성 선봉장', 'cavalry', 92, 566, 150, 71, 31)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -14739,6 +14887,25 @@ export const jingDefenseBonds: BattleBond[] = [
}
];
export const fanCastleBonds: BattleBond[] = [
{
id: 'guan-yu__fa-zheng',
unitIds: ['guan-yu', 'fa-zheng'],
title: '번성 책략선',
level: 27,
exp: 0,
description: '관우의 정면 압박과 법정의 물길 판단이 맞물리면, 번성 외곽의 보루와 나루를 동시에 흔드는 공명 효과가 강해집니다.'
},
{
id: 'guan-yu__ma-chao',
unitIds: ['guan-yu', 'ma-chao'],
title: '청룡과 서량 기병',
level: 26,
exp: 0,
description: '관우의 묵직한 전선 유지와 마초의 거친 기동력이 이어지면, 방덕 기병대의 돌파를 맞받아치는 연계 공격 확률이 높아집니다.'
}
];
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -14798,6 +14965,9 @@ export const thirtySeventhBattleBonds: BattleBond[] = [
export const thirtyEighthBattleBonds: BattleBond[] = [...thirtySeventhBattleBonds, ...jingDefenseBonds].map(
cloneBattleBondForScenario
);
export const thirtyNinthBattleBonds: BattleBond[] = [...thirtyEighthBattleBonds, ...fanCastleBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -16547,6 +16717,78 @@ function createThirtyEighthBattleTerrain(): TerrainType[][] {
);
}
function createThirtyNinthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 60 }, (_, y) =>
Array.from({ length: 78 }, (_, x): TerrainType => {
if (x <= 6 && y >= 53) {
return 'camp';
}
if (
(x >= 60 && x <= 77 && y >= 6 && y <= 24) ||
(x >= 53 && x <= 65 && y >= 9 && y <= 22) ||
(x >= 66 && x <= 77 && y >= 20 && y <= 38) ||
(x >= 68 && x <= 74 && y >= 34 && y <= 44)
) {
return 'fort';
}
if (
(x >= 21 && x <= 25 && y >= 50 && y <= 54) ||
(x >= 42 && x <= 46 && y >= 43 && y <= 46) ||
(x >= 56 && x <= 60 && y >= 36 && y <= 40) ||
(x >= 70 && x <= 73 && y >= 40 && y <= 44)
) {
return 'village';
}
if (
(y >= 55 && x >= 6 && x <= 27) ||
(x >= 26 && x <= 42 && y >= 50 && y <= 55) ||
(x >= 41 && x <= 56 && y >= 44 && y <= 50) ||
(x >= 55 && x <= 68 && y >= 38 && y <= 44) ||
(x >= 67 && x <= 76 && y >= 30 && y <= 38) ||
(x >= 70 && x <= 77 && y >= 18 && y <= 30) ||
(x >= 58 && x <= 75 && y >= 16 && y <= 20)
) {
return 'road';
}
if ((x === 19 || x === 20 || x === 21) && y >= 0 && y <= 51) {
return 'river';
}
if ((x === 48 || x === 49) && y >= 6 && y <= 44) {
return 'river';
}
if ((x === 59 || x === 60) && y >= 40 && y <= 59) {
return 'river';
}
if (
(x >= 4 && x <= 28 && y >= 10 && y <= 42) ||
(x >= 29 && x <= 44 && y >= 18 && y <= 46) ||
(x >= 49 && x <= 64 && y >= 44 && y <= 59) ||
(x >= 61 && x <= 77 && y >= 41 && y <= 58)
) {
return 'forest';
}
if (
(x >= 0 && x <= 18 && y <= 26) ||
(x >= 28 && x <= 46 && y <= 17) ||
(x >= 47 && x <= 65 && y >= 1 && y <= 19) ||
(x >= 66 && x <= 77 && y <= 14) ||
(x >= 32 && x <= 50 && y >= 52 && y <= 59)
) {
return 'hill';
}
if (
(x <= 2 && y <= 52) ||
(x >= 75 && y >= 36) ||
(x >= 58 && x <= 62 && y <= 8) ||
(x >= 27 && x <= 32 && y >= 27 && y <= 41)
) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),