Add Xiapi final sortie chapter

This commit is contained in:
2026-06-23 03:43:53 +09:00
parent 450842bd28
commit 3cdbbe8d5a
12 changed files with 905 additions and 23 deletions

View File

@@ -67,6 +67,15 @@ export const itemCatalog: Record<string, ItemDefinition> = {
attackBonus: 5,
effects: ['연속 공격 판정 보정']
},
'sky-piercer-halberd': {
id: 'sky-piercer-halberd',
name: '방천화극',
slot: 'weapon',
rank: 'treasure',
description: '여포의 상징으로 두려움을 산 긴 화극. 한 번 휘두르면 전열이 크게 흔들린다.',
attackBonus: 6,
effects: ['단독으로 돌파할 때 피해 보너스']
},
'yellow-turban-saber': {
id: 'yellow-turban-saber',
name: '황건도',

View File

@@ -19,6 +19,10 @@ import {
tenthBattleMap,
tenthBattleUnits,
tenthBattleVictoryPages,
thirteenthBattleBonds,
thirteenthBattleMap,
thirteenthBattleUnits,
thirteenthBattleVictoryPages,
twelfthBattleBonds,
twelfthBattleMap,
twelfthBattleUnits,
@@ -65,7 +69,8 @@ export type BattleScenarioId =
| 'ninth-battle-xuzhou-gate-night-raid'
| 'tenth-battle-xuzhou-breakout'
| 'eleventh-battle-xudu-refuge-road'
| 'twelfth-battle-xiapi-outer-siege';
| 'twelfth-battle-xiapi-outer-siege'
| 'thirteenth-battle-xiapi-final';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -740,6 +745,59 @@ export const twelfthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const thirteenthBattleScenario: BattleScenarioDefinition = {
id: 'thirteenth-battle-xiapi-final',
title: '하비 결전',
victoryConditionLabel: '여포 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'여포가 하비 성 안에서 마지막 돌파를 준비합니다. 여포를 격파하면 서주를 뒤흔든 난전도 끝을 맞습니다.',
'진궁은 성루 궁병과 수문 수비를 이용해 접근로를 끊으려 합니다. 기병 돌파대와 성문 수비대를 나누어 상대하십시오.',
'성문 요새를 확보하고 22턴 안에 승리하면 조조 휘하에서도 유비군의 전공과 독자 보급 명분을 모두 남길 수 있습니다.'
],
map: thirteenthBattleMap,
units: thirteenthBattleUnits,
bonds: thirteenthBattleBonds,
mapTextureKey: 'battle-map-thirteenth',
leaderUnitId: 'xiapi-final-leader-lu-bu',
quickVictoryTurnLimit: 22,
baseVictoryGold: 1560,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '여포 격파',
rewardGold: 980,
unitId: 'xiapi-final-leader-lu-bu'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 360,
unitId: 'liu-bei'
},
{
id: 'gate',
kind: 'secure-terrain',
label: '하비 성문 확보',
rewardGold: 560,
terrain: 'fort'
},
{
id: 'quick',
kind: 'quick-victory',
label: '22턴 이내 승리',
rewardGold: 420,
maxTurn: 22
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 5', '상처약 3', '탁주 2', '하비 결전 공적 +1'],
victoryPages: thirteenthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -754,7 +812,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario,
'tenth-battle-xuzhou-breakout': tenthBattleScenario,
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario,
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario,
'thirteenth-battle-xiapi-final': thirteenthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -10,6 +10,7 @@ import {
seventhBattleScenario,
sixthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
twelfthBattleScenario,
thirdBattleScenario,
type BattleScenarioId
@@ -34,6 +35,8 @@ import {
sixthBattleVictoryPages,
tenthBattleIntroPages,
tenthBattleVictoryPages,
thirteenthBattleIntroPages,
thirteenthBattleVictoryPages,
twelfthBattleIntroPages,
twelfthBattleVictoryPages,
thirdBattleIntroPages,
@@ -166,12 +169,22 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[twelfthBattleScenario.id]: {
afterBattleId: twelfthBattleScenario.id,
eyebrow: '다음 전장',
title: thirteenthBattleScenario.title,
description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 성문과 수문을 뚫고 여포를 격파하면 조조 휘하에서 깊어지는 긴장이 다음 흐름으로 이어집니다.',
rewardHint: `예상 보상: ${thirteenthBattleScenario.title} 개방`,
nextBattleId: thirteenthBattleScenario.id,
campaignStep: 'thirteenth-battle',
pages: [...twelfthBattleVictoryPages, ...thirteenthBattleIntroPages]
},
[thirteenthBattleScenario.id]: {
afterBattleId: thirteenthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '하비 포위의 끝',
description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 다음 흐름은 여포 토벌의 결말과 조조 밑에서 깊어지장으로 이어집니다.',
rewardHint: '다음 장: 하비 결전과 조조 이탈 준비 중',
pages: twelfthBattleVictoryPages,
unavailableNotice: '하비 결전과 조조 이탈 장은 다음 작업에서 이어집니다. 군영에서 성장 출전 구성을 정비하십시오.'
title: '조조 이탈의 불씨',
description: '여포가 무너지고 하비 결전은 끝났지만, 조조의 칭찬과 경계는 동시에 깊어졌습니다. 다음 흐름은 조조 휘하를 벗어날 명분과 때를 준비하는 장으로 이어집니다.',
rewardHint: '다음 장: 조조 이탈과 원소 의탁 준비 중',
pages: thirteenthBattleVictoryPages,
unavailableNotice: '조조 이탈과 원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.'
}
};

View File

@@ -813,6 +813,70 @@ export const twelfthBattleVictoryPages: StoryPage[] = [
}
];
export const thirteenthBattleIntroPages: StoryPage[] = [
{
id: 'thirteenth-cao-cao-final-order',
bgm: 'story-dark',
chapter: '하비 결전의 명',
background: 'story-liu-bei',
text: '하비 외곽이 무너지자 조조는 마지막 총공세를 명했다. 성 안의 여포는 아직 강했고, 진궁은 남은 성루와 수문을 엮어 마지막 반격을 준비하고 있었다.'
},
{
id: 'thirteenth-brothers-before-final',
bgm: 'militia-theme',
chapter: '여포를 앞두고',
background: 'story-three-heroes',
speaker: '관우',
portrait: 'guanYu',
text: '형님, 여포는 용맹하나 믿음이 얕습니다. 다만 그 칼끝이 어지러운 세상의 그림자라면, 오늘 싸움 뒤에 우리 길도 더 뚜렷해질 것입니다.'
},
{
id: 'thirteenth-liu-bei-resolve',
bgm: 'story-dark',
chapter: '조조의 그늘',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '조조의 명으로 싸우되, 백성의 원망을 덜고 난세의 사슬을 끊기 위해 싸운다. 여포의 끝을 보더라도 우리의 뜻까지 맡기지는 말자.'
},
{
id: 'thirteenth-xiapi-final-sortie',
bgm: 'battle-prep',
chapter: '하비 결전',
background: 'story-sortie',
speaker: '장비',
portrait: 'zhangFei',
text: '좋습니다, 형님. 이번엔 성문을 열고 여포의 깃발까지 밀고 들어가겠습니다. 누가 함께 나설지 정해 주십시오. 대기한 자도 다음 싸움을 위해 힘을 아껴야 합니다.'
}
];
export const thirteenthBattleVictoryPages: StoryPage[] = [
{
id: 'thirteenth-victory-lubu-falls',
bgm: 'militia-theme',
chapter: '무너진 여포',
background: 'story-militia',
text: '하비 성내의 마지막 저항이 꺾이고 여포의 깃발이 내려갔다. 뛰어난 무용도 믿음을 잃으면 성 하나를 지키지 못한다는 소문이 병사들 사이에 퍼졌다.'
},
{
id: 'thirteenth-cao-cao-weighs-liu-bei',
bgm: 'story-dark',
chapter: '칭찬과 경계',
background: 'story-liu-bei',
speaker: '간옹',
text: '조조는 형님의 말을 들은 뒤 오래 웃지 않았네. 여포의 운명은 끝났지만, 이제부터는 조조가 우리를 어디까지 품고 어디서부터 경계할지 살펴야 하네.'
},
{
id: 'thirteenth-road-to-cao-break',
bgm: 'story-dark',
chapter: '떠날 때를 헤아리다',
background: 'story-three-heroes',
speaker: '유비',
portrait: 'liuBei',
text: '오늘 싸움으로 빚도 공도 쌓였다. 그러나 의로 시작한 길을 남의 장부 속 숫자로 끝낼 수는 없다. 떠날 때가 오면 망설이지 않도록 마음을 단단히 하자.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1030,6 +1094,12 @@ export const twelfthBattleMap: BattleMap = {
terrain: createTwelfthBattleTerrain()
};
export const thirteenthBattleMap: BattleMap = {
width: 32,
height: 24,
terrain: createThirteenthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4181,6 +4251,335 @@ export const twelfthBattleUnits: UnitData[] = [
}
];
const thirteenthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 3, y: 19 },
'guan-yu': { x: 4, y: 18 },
'zhang-fei': { x: 4, y: 20 },
'jian-yong': { x: 5, y: 19 },
'mi-zhu': { x: 3, y: 21 }
};
export const thirteenthBattleUnits: UnitData[] = [
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
placeScenarioUnit(unit, thirteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
),
{
id: 'xiapi-final-cavalry-a',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 14,
exp: 84,
hp: 62,
maxHp: 62,
attack: 21,
move: 5,
stats: { might: 98, intelligence: 55, leadership: 76, agility: 99, luck: 62 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 60 },
armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 15,
y: 15
},
{
id: 'xiapi-final-cavalry-b',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 14,
exp: 84,
hp: 62,
maxHp: 62,
attack: 21,
move: 5,
stats: { might: 98, intelligence: 55, leadership: 76, agility: 99, luck: 62 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 60 },
armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 20,
y: 7
},
{
id: 'xiapi-final-cavalry-c',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 14,
exp: 86,
hp: 63,
maxHp: 63,
attack: 21,
move: 5,
stats: { might: 99, intelligence: 56, leadership: 77, agility: 100, luck: 63 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 },
armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 25,
y: 17
},
{
id: 'xiapi-final-cavalry-d',
name: '여포군 친위기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 15,
exp: 88,
hp: 66,
maxHp: 66,
attack: 22,
move: 5,
stats: { might: 101, intelligence: 58, leadership: 80, agility: 101, luck: 64 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 },
armor: { itemId: 'rebel-vest', level: 2, exp: 28 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 27,
y: 12
},
{
id: 'xiapi-final-gate-guard-a',
name: '하비 성문 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 13,
exp: 82,
hp: 56,
maxHp: 56,
attack: 18,
move: 3,
stats: { might: 89, intelligence: 58, leadership: 75, agility: 68, luck: 59 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 18,
y: 13
},
{
id: 'xiapi-final-gate-guard-b',
name: '하비 성문 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 13,
exp: 82,
hp: 56,
maxHp: 56,
attack: 18,
move: 3,
stats: { might: 89, intelligence: 58, leadership: 75, agility: 68, luck: 59 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 19,
y: 14
},
{
id: 'xiapi-final-gate-guard-c',
name: '하비 성문 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 13,
exp: 84,
hp: 57,
maxHp: 57,
attack: 18,
move: 3,
stats: { might: 90, intelligence: 58, leadership: 76, agility: 69, luck: 59 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 23,
y: 10
},
{
id: 'xiapi-final-gate-guard-d',
name: '하비 성문 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 13,
exp: 84,
hp: 57,
maxHp: 57,
attack: 18,
move: 3,
stats: { might: 90, intelligence: 58, leadership: 76, agility: 69, luck: 59 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 24,
y: 11
},
{
id: 'xiapi-final-archer-a',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 13,
exp: 82,
hp: 46,
maxHp: 46,
attack: 19,
move: 3,
stats: { might: 80, intelligence: 70, leadership: 68, agility: 82, luck: 60 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 56 },
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 22,
y: 7
},
{
id: 'xiapi-final-archer-b',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 13,
exp: 84,
hp: 47,
maxHp: 47,
attack: 19,
move: 3,
stats: { might: 81, intelligence: 71, leadership: 69, agility: 83, luck: 60 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 58 },
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 25,
y: 8
},
{
id: 'xiapi-final-archer-c',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 13,
exp: 84,
hp: 47,
maxHp: 47,
attack: 19,
move: 3,
stats: { might: 81, intelligence: 71, leadership: 69, agility: 83, luck: 60 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 58 },
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 27,
y: 15
},
{
id: 'xiapi-final-raider-a',
name: '여포군 성내 기습병',
faction: 'enemy',
className: '여포군 기습병',
classKey: 'bandit',
level: 13,
exp: 82,
hp: 52,
maxHp: 52,
attack: 19,
move: 4,
stats: { might: 91, intelligence: 57, leadership: 67, agility: 85, 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: 11,
y: 10
},
{
id: 'xiapi-final-raider-b',
name: '여포군 성내 기습병',
faction: 'enemy',
className: '여포군 기습병',
classKey: 'bandit',
level: 13,
exp: 84,
hp: 53,
maxHp: 53,
attack: 19,
move: 4,
stats: { might: 92, intelligence: 57, leadership: 67, agility: 86, luck: 61 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 12,
y: 21
},
{
id: 'xiapi-final-strategist-chen-gong',
name: '진궁',
faction: 'enemy',
className: '여포군 책사',
classKey: 'strategist',
level: 14,
exp: 90,
hp: 56,
maxHp: 56,
attack: 19,
move: 3,
stats: { might: 64, intelligence: 98, leadership: 88, agility: 72, luck: 66 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 60 },
armor: { itemId: 'cloth-armor', level: 2, exp: 24 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 25,
y: 11
},
{
id: 'xiapi-final-leader-lu-bu',
name: '여포',
faction: 'enemy',
className: '비장군',
classKey: 'rebelLeader',
level: 16,
exp: 96,
hp: 94,
maxHp: 94,
attack: 25,
move: 5,
stats: { might: 108, intelligence: 46, leadership: 88, agility: 102, luck: 65 },
equipment: {
weapon: { itemId: 'sky-piercer-halberd', level: 3, exp: 20 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 28,
y: 11
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -4238,6 +4637,7 @@ export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleB
export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario);
export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario);
export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario);
export const thirteenthBattleBonds: BattleBond[] = twelfthBattleBonds.map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -4432,6 +4832,62 @@ function createTwelfthBattleTerrain(): TerrainType[][] {
);
}
function createThirteenthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 24 }, (_, y) =>
Array.from({ length: 32 }, (_, x): TerrainType => {
if (x <= 2 && y >= 18 && y <= 22) {
return 'camp';
}
if (
(x >= 20 && x <= 29 && y >= 8 && y <= 15) ||
(x >= 23 && x <= 28 && y >= 5 && y <= 7) ||
(x >= 25 && x <= 30 && y >= 16 && y <= 18)
) {
return 'fort';
}
if ((x >= 10 && x <= 12 && y >= 17 && y <= 19) || (x >= 17 && x <= 19 && y >= 13 && y <= 15)) {
return 'village';
}
if ((x === 14 || x === 15) && y >= 0 && y <= 22) {
return 'river';
}
if ((x === 18 || x === 19) && y >= 2 && y <= 20) {
return 'river';
}
if (
(y === 19 && x >= 2 && x <= 19) ||
(y === 14 && x >= 15 && x <= 24) ||
(y === 11 && x >= 19 && x <= 29) ||
(x === 24 && y >= 7 && y <= 18)
) {
return 'road';
}
if ((x >= 4 && x <= 16 && y === 23 - x) || (x >= 10 && x <= 22 && y === x - 3)) {
return 'road';
}
if (
(x <= 8 && y <= 8) ||
(x >= 4 && x <= 12 && y >= 9 && y <= 13) ||
(x >= 2 && x <= 9 && y >= 14 && y <= 17) ||
(x >= 28 && y >= 19 && y <= 22)
) {
return 'forest';
}
if (
(x >= 6 && x <= 10 && y >= 2 && y <= 5) ||
(x >= 27 && x <= 31 && y >= 1 && y <= 5) ||
(x >= 9 && x <= 13 && y >= 20)
) {
return 'hill';
}
if ((x >= 30 && y <= 8) || (x >= 30 && y >= 14) || (x <= 1 && y <= 4)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),