Add Xiapi outer siege chapter

This commit is contained in:
2026-06-23 02:55:33 +09:00
parent 83b2bb6361
commit 450842bd28
11 changed files with 778 additions and 17 deletions

View File

@@ -19,6 +19,10 @@ import {
tenthBattleMap,
tenthBattleUnits,
tenthBattleVictoryPages,
twelfthBattleBonds,
twelfthBattleMap,
twelfthBattleUnits,
twelfthBattleVictoryPages,
fifthBattleBonds,
fifthBattleMap,
fifthBattleUnits,
@@ -60,7 +64,8 @@ export type BattleScenarioId =
| 'eighth-battle-xiaopei-supply-road'
| 'ninth-battle-xuzhou-gate-night-raid'
| 'tenth-battle-xuzhou-breakout'
| 'eleventh-battle-xudu-refuge-road';
| 'eleventh-battle-xudu-refuge-road'
| 'twelfth-battle-xiapi-outer-siege';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -682,6 +687,59 @@ export const eleventhBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const twelfthBattleScenario: BattleScenarioDefinition = {
id: 'twelfth-battle-xiapi-outer-siege',
title: '하비 외곽전',
victoryConditionLabel: '장료 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'조조는 하비 포위망의 남쪽 둑길을 유비군에게 맡깁니다. 장료를 격파하면 여포군 선봉은 성 안으로 물러납니다.',
'강줄기와 둑길이 전장을 나눕니다. 여포군 기병은 길을 타고 돌파하고, 성루 궁병과 진궁은 후방에서 압박합니다.',
'둑길 진영을 확보하고 20턴 안에 승리하면 조조 휘하에서의 공적과 유비군의 독자 보급을 함께 지킬 수 있습니다.'
],
map: twelfthBattleMap,
units: twelfthBattleUnits,
bonds: twelfthBattleBonds,
mapTextureKey: 'battle-map-twelfth',
leaderUnitId: 'xiapi-leader-zhang-liao',
quickVictoryTurnLimit: 20,
baseVictoryGold: 1440,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '장료 격파',
rewardGold: 900,
unitId: 'xiapi-leader-zhang-liao'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 340,
unitId: 'liu-bei'
},
{
id: 'camp',
kind: 'secure-terrain',
label: '하비 둑길 진영 확보',
rewardGold: 500,
terrain: 'camp'
},
{
id: 'quick',
kind: 'quick-victory',
label: '20턴 이내 승리',
rewardGold: 390,
maxTurn: 20
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 4', '상처약 3', '탁주 2', '하비 포위 공적 +1'],
victoryPages: twelfthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -695,7 +753,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'eighth-battle-xiaopei-supply-road': eighthBattleScenario,
'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario,
'tenth-battle-xuzhou-breakout': tenthBattleScenario,
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario,
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -10,6 +10,7 @@ import {
seventhBattleScenario,
sixthBattleScenario,
tenthBattleScenario,
twelfthBattleScenario,
thirdBattleScenario,
type BattleScenarioId
} from './battles';
@@ -33,6 +34,8 @@ import {
sixthBattleVictoryPages,
tenthBattleIntroPages,
tenthBattleVictoryPages,
twelfthBattleIntroPages,
twelfthBattleVictoryPages,
thirdBattleIntroPages,
thirdBattleVictoryPages,
type StoryPage
@@ -153,12 +156,22 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[eleventhBattleScenario.id]: {
afterBattleId: eleventhBattleScenario.id,
eyebrow: '다음 전장',
title: twelfthBattleScenario.title,
description: '조조는 유비군을 하비 포위망에 투입합니다. 여포를 다시 마주하는 이 싸움에서 유비군은 조조의 명을 따르되, 서주 백성을 위한 뜻도 지켜야 합니다.',
rewardHint: `예상 보상: ${twelfthBattleScenario.title} 개방`,
nextBattleId: twelfthBattleScenario.id,
campaignStep: 'twelfth-battle',
pages: [...eleventhBattleVictoryPages, ...twelfthBattleIntroPages]
},
[twelfthBattleScenario.id]: {
afterBattleId: twelfthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '조조의 그늘 아래',
description: '허도 입성로를 열어 조조의 장막에 들어섰지만, 유비군은 보호와 감시를 동시에 받게 됩니다. 다음 흐름은 조조 밑에서 기회를 기다리는 장으로 이어집니다.',
rewardHint: '다음 장: 조조 휘하의 긴장 준비 중',
pages: eleventhBattleVictoryPages,
unavailableNotice: '조조 휘하 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
title: '하비 포위의 끝',
description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 다음 흐름은 여포 토벌의 결말과 조조 밑에서 깊어지장으로 이어집니다.',
rewardHint: '다음 장: 하비 결전과 조조 이탈 준비 중',
pages: twelfthBattleVictoryPages,
unavailableNotice: '하비 결전과 조조 이탈 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
}
};

View File

@@ -759,6 +759,60 @@ export const eleventhBattleVictoryPages: StoryPage[] = [
}
];
export const twelfthBattleIntroPages: StoryPage[] = [
{
id: 'twelfth-cao-cao-order',
bgm: 'story-dark',
chapter: '조조의 명',
background: 'story-liu-bei',
text: '허도에 머문 지 오래지 않아 조조는 유비군을 하비 포위전에 부른다. 여포를 치는 명령은 서주를 잃은 한을 풀 기회이자, 조조의 칼끝이 어디를 향하는지 가늠하는 시험이었다.'
},
{
id: 'twelfth-xiapi-shadow',
bgm: 'militia-theme',
chapter: '하비의 물길',
background: 'story-militia',
speaker: '미축',
text: '하비 외곽은 강과 진흙이 얽혀 있습니다. 보급로를 끊고 둑길을 잡지 못하면, 여포군 기병이 포위망의 빈틈을 찢고 나올 것입니다.'
},
{
id: 'twelfth-brothers-before-lubu',
bgm: 'story-dark',
chapter: '다시 마주한 여포',
background: 'story-three-heroes',
speaker: '장비',
portrait: 'zhangFei',
text: '형님, 이번엔 여포의 이름만 들어도 창끝이 뜨겁습니다. 허나 서두르지 않겠습니다. 먼저 포위망을 조이고, 놈의 날뛰는 길을 끊겠습니다.'
},
{
id: 'twelfth-xiapi-sortie',
bgm: 'battle-prep',
chapter: '하비 외곽전',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '여포를 치는 싸움이 조조의 명이라 해도, 서주 백성이 다시 짓밟히지 않게 하는 일은 우리의 뜻이다. 물길을 잡고 포위망을 열어라.'
}
];
export const twelfthBattleVictoryPages: StoryPage[] = [
{
id: 'twelfth-victory-outer-camp',
bgm: 'militia-theme',
chapter: '무너진 외곽 진영',
background: 'story-militia',
text: '유비군은 하비 외곽의 둑길과 보급 진영을 장악했다. 여포군 선봉은 성 안으로 물러났고, 조조군은 포위망을 한층 더 좁혔다.'
},
{
id: 'twelfth-cao-cao-watches',
bgm: 'story-dark',
chapter: '칭찬과 감시',
background: 'story-liu-bei',
speaker: '간옹',
text: '현덕, 조조는 오늘의 공을 칭찬하겠지만 동시에 더 깊이 살필 걸세. 여포를 치는 동안에도, 우리가 조조의 손안에만 있지 않다는 것을 잊지 말아야 하네.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -970,6 +1024,12 @@ export const eleventhBattleMap: BattleMap = {
terrain: createEleventhBattleTerrain()
};
export const twelfthBattleMap: BattleMap = {
width: 30,
height: 24,
terrain: createTwelfthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -3834,6 +3894,293 @@ export const eleventhBattleUnits: UnitData[] = [
}
];
const twelfthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 3, y: 18 },
'guan-yu': { x: 4, y: 17 },
'zhang-fei': { x: 4, y: 19 },
'jian-yong': { x: 5, y: 20 },
'mi-zhu': { x: 3, y: 21 }
};
export const twelfthBattleUnits: UnitData[] = [
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
placeScenarioUnit(unit, twelfthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
),
{
id: 'xiapi-cavalry-a',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 13,
exp: 76,
hp: 58,
maxHp: 58,
attack: 20,
move: 5,
stats: { might: 95, intelligence: 54, leadership: 74, agility: 96, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 54 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 20,
y: 5
},
{
id: 'xiapi-cavalry-b',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 13,
exp: 78,
hp: 59,
maxHp: 59,
attack: 20,
move: 5,
stats: { might: 96, intelligence: 54, leadership: 74, agility: 97, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 23,
y: 15
},
{
id: 'xiapi-cavalry-c',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 13,
exp: 78,
hp: 59,
maxHp: 59,
attack: 20,
move: 5,
stats: { might: 96, intelligence: 55, leadership: 75, agility: 97, luck: 61 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 25,
y: 10
},
{
id: 'xiapi-dike-guard-a',
name: '하비 둑길 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 12,
exp: 74,
hp: 52,
maxHp: 52,
attack: 17,
move: 3,
stats: { might: 86, intelligence: 56, leadership: 72, agility: 65, luck: 57 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 50 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 12,
y: 14
},
{
id: 'xiapi-dike-guard-b',
name: '하비 둑길 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 12,
exp: 76,
hp: 53,
maxHp: 53,
attack: 17,
move: 3,
stats: { might: 87, intelligence: 56, leadership: 73, agility: 66, luck: 57 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 14,
y: 18
},
{
id: 'xiapi-dike-guard-c',
name: '하비 둑길 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 12,
exp: 76,
hp: 53,
maxHp: 53,
attack: 17,
move: 3,
stats: { might: 87, intelligence: 57, leadership: 73, agility: 66, luck: 57 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 17,
y: 9
},
{
id: 'xiapi-archer-a',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 12,
exp: 74,
hp: 43,
maxHp: 43,
attack: 18,
move: 3,
stats: { might: 78, intelligence: 68, leadership: 66, agility: 78, luck: 58 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 50 },
armor: { itemId: 'cloth-armor', level: 2, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 22,
y: 7
},
{
id: 'xiapi-archer-b',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 12,
exp: 76,
hp: 44,
maxHp: 44,
attack: 18,
move: 3,
stats: { might: 79, intelligence: 69, leadership: 67, agility: 79, luck: 58 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 52 },
armor: { itemId: 'cloth-armor', level: 2, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 24,
y: 18
},
{
id: 'xiapi-archer-c',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 12,
exp: 76,
hp: 44,
maxHp: 44,
attack: 18,
move: 3,
stats: { might: 79, intelligence: 69, leadership: 67, agility: 79, luck: 58 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 52 },
armor: { itemId: 'cloth-armor', level: 2, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 19,
y: 3
},
{
id: 'xiapi-camp-raider-a',
name: '여포군 기습병',
faction: 'enemy',
className: '여포군 기습병',
classKey: 'bandit',
level: 12,
exp: 74,
hp: 48,
maxHp: 48,
attack: 18,
move: 4,
stats: { might: 88, intelligence: 55, leadership: 64, agility: 81, luck: 58 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 50 },
armor: { itemId: 'rebel-vest', level: 2, exp: 22 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 9,
y: 8
},
{
id: 'xiapi-camp-raider-b',
name: '여포군 기습병',
faction: 'enemy',
className: '여포군 기습병',
classKey: 'bandit',
level: 12,
exp: 76,
hp: 49,
maxHp: 49,
attack: 18,
move: 4,
stats: { might: 89, intelligence: 55, leadership: 64, agility: 82, luck: 58 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 },
armor: { itemId: 'rebel-vest', level: 2, exp: 22 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 11,
y: 21
},
{
id: 'xiapi-strategist-chen-gong',
name: '진궁',
faction: 'enemy',
className: '여포군 책사',
classKey: 'strategist',
level: 13,
exp: 82,
hp: 52,
maxHp: 52,
attack: 18,
move: 3,
stats: { might: 62, intelligence: 96, leadership: 84, agility: 70, luck: 64 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 54 },
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 22,
y: 12
},
{
id: 'xiapi-leader-zhang-liao',
name: '장료',
faction: 'enemy',
className: '여포군 선봉장',
classKey: 'rebelLeader',
level: 14,
exp: 86,
hp: 78,
maxHp: 78,
attack: 21,
move: 5,
stats: { might: 95, intelligence: 78, leadership: 92, agility: 86, luck: 63 },
equipment: {
weapon: { itemId: 'leader-axe', level: 2, exp: 58 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 26 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 24,
y: 11
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -3890,6 +4237,7 @@ export const eighthBattleBonds: BattleBond[] = [...firstBattleBonds, ...xuzhouRe
export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleBondForScenario);
export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario);
export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario);
export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -4046,6 +4394,44 @@ function createEleventhBattleTerrain(): TerrainType[][] {
);
}
function createTwelfthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 24 }, (_, y) =>
Array.from({ length: 30 }, (_, x): TerrainType => {
if (x <= 2 && y >= 17 && y <= 22) {
return 'camp';
}
if ((x >= 23 && x <= 27 && y >= 8 && y <= 13) || (x >= 20 && x <= 24 && y >= 2 && y <= 4)) {
return 'fort';
}
if ((x >= 10 && x <= 12 && y >= 16 && y <= 18) || (x >= 16 && x <= 18 && y >= 7 && y <= 9)) {
return 'village';
}
if ((x === 14 || x === 15) && y >= 0 && y <= 22) {
return 'river';
}
if ((x === 20 || x === 21) && y >= 4 && y <= 20) {
return 'river';
}
if ((x >= 12 && x <= 22 && y === 12) || (x >= 4 && x <= 13 && y === 18) || (x >= 6 && x <= 24 && y === 10)) {
return 'road';
}
if ((x >= 2 && x <= 26 && y === x - 2) || (x >= 3 && x <= 15 && y === 21 - x)) {
return 'road';
}
if ((x <= 8 && y <= 8) || (x >= 4 && x <= 11 && y >= 9 && y <= 13) || (x >= 22 && y >= 16 && y <= 22)) {
return 'forest';
}
if ((x >= 6 && x <= 10 && y >= 2 && y <= 5) || (x >= 24 && x <= 28 && y >= 4 && y <= 8) || (x >= 9 && x <= 13 && y >= 20)) {
return 'hill';
}
if ((x >= 27 && y <= 4) || (x >= 27 && y >= 15) || (x <= 1 && y <= 4)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),