Add Xiaopei supply road battle

This commit is contained in:
2026-06-23 01:26:45 +09:00
parent 6b863882aa
commit 1c501057e3
11 changed files with 693 additions and 25 deletions

View File

@@ -1,5 +1,9 @@
import {
firstBattleBonds,
eighthBattleBonds,
eighthBattleMap,
eighthBattleUnits,
eighthBattleVictoryPages,
firstBattleMap,
firstBattleUnits,
firstBattleVictoryPages,
@@ -40,7 +44,8 @@ export type BattleScenarioId =
| 'fourth-battle-guangzong-camp'
| 'fifth-battle-sishui-vanguard'
| 'sixth-battle-jieqiao-relief'
| 'seventh-battle-xuzhou-rescue';
| 'seventh-battle-xuzhou-rescue'
| 'eighth-battle-xiaopei-supply-road';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -450,6 +455,59 @@ export const seventhBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const eighthBattleScenario: BattleScenarioDefinition = {
id: 'eighth-battle-xiaopei-supply-road',
title: '소패 보급로 방위전',
victoryConditionLabel: '고순 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'여포군의 척후와 습격병이 소패 보급로를 흔들고 있습니다. 고순을 격파하면 서주의 첫 방위선을 지킬 수 있습니다.',
'기병은 길을 타고 빠르게 파고들고, 궁병은 강 건너와 언덕에서 사거리를 잡습니다. 마을과 창고 지형을 이용해 버티십시오.',
'18턴 이내 승리하면 서주 보급망과 새 합류 무장의 명성이 크게 오릅니다.'
],
map: eighthBattleMap,
units: eighthBattleUnits,
bonds: eighthBattleBonds,
mapTextureKey: 'battle-map-eighth',
leaderUnitId: 'xiaopei-leader-gao-shun',
quickVictoryTurnLimit: 18,
baseVictoryGold: 1080,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '고순 격파',
rewardGold: 640,
unitId: 'xiaopei-leader-gao-shun'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 250,
unitId: 'liu-bei'
},
{
id: 'village',
kind: 'secure-terrain',
label: '소패 보급촌 확보',
rewardGold: 360,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '18턴 이내 승리',
rewardGold: 300,
maxTurn: 18
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 3', '상처약 2', '탁주 1', '서주 보급망 +1'],
victoryPages: eighthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -459,7 +517,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'fourth-battle-guangzong-camp': fourthBattleScenario,
'fifth-battle-sishui-vanguard': fifthBattleScenario,
'sixth-battle-jieqiao-relief': sixthBattleScenario,
'seventh-battle-xuzhou-rescue': seventhBattleScenario
'seventh-battle-xuzhou-rescue': seventhBattleScenario,
'eighth-battle-xiaopei-supply-road': eighthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -1,6 +1,7 @@
import type { CampaignStep } from '../state/campaignState';
import {
defaultBattleScenario,
eighthBattleScenario,
fifthBattleScenario,
fourthBattleScenario,
secondBattleScenario,
@@ -10,6 +11,8 @@ import {
type BattleScenarioId
} from './battles';
import {
eighthBattleIntroPages,
eighthBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
firstBattleVictoryPages,
@@ -101,12 +104,22 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[seventhBattleScenario.id]: {
afterBattleId: seventhBattleScenario.id,
eyebrow: '다음 전장',
title: eighthBattleScenario.title,
description: '도겸에게 서주를 맡은 뒤 첫 과제는 소패 보급로를 지키는 일입니다. 여포군의 그림자가 드리우기 전에 새로 합류한 장수들과 방위선을 세워야 합니다.',
rewardHint: `예상 보상: ${eighthBattleScenario.title} 개방`,
nextBattleId: eighthBattleScenario.id,
campaignStep: 'eighth-battle',
pages: [...seventhBattleVictoryPages, ...eighthBattleIntroPages]
},
[eighthBattleScenario.id]: {
afterBattleId: eighthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '도겸의 서주',
description: '서주 구원전 뒤 도겸은 유비에게 서주의 장래를 맡아 달라 청합니다. 이제 서주 인수와 여포의 그림자가 다음 줄기를 만듭니다.',
rewardHint: '다음 장: 도겸에게 서주 인수 준비 중',
pages: seventhBattleVictoryPages,
unavailableNotice: '도겸의 서주 인수 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태와 출전 구성을 정비하세요.'
title: '여포의 의탁',
description: '소패 보급로를 지켜냈지만 여포가 몸을 의탁하겠다는 전갈이 도착합니다. 받아들이는 선택은 곧 서주 상실의 갈등으로 이어질 것입니다.',
rewardHint: '다음 장: 여포 의탁과 서주 내부 불안 준비 중',
pages: eighthBattleVictoryPages,
unavailableNotice: '여포 의탁 이후의 서주 상실 흐름은 다음 작업에서 이어집니다. 군영에서 성장 출전 구성을 정비하십시오.'
}
};

View File

@@ -540,6 +540,60 @@ export const seventhBattleVictoryPages: StoryPage[] = [
}
];
export const eighthBattleIntroPages: StoryPage[] = [
{
id: 'eighth-xuzhou-handover',
bgm: 'militia-theme',
chapter: '서주의 새 군영',
background: 'story-militia',
text: '도겸은 병든 몸을 일으켜 서주의 장부와 군량 창고를 유비에게 넘겼다. 간옹과 미축은 백성의 청원과 보급로를 정리하며 새 군영의 기초를 세웠다.'
},
{
id: 'eighth-lu-bu-shadow',
bgm: 'battle-prep',
chapter: '여포의 그림자',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '여포가 떠돌고 있다는 소식은 가볍지 않다. 그 이름 하나만으로도 여러 군세가 흔들린다. 그러나 지금은 서주의 길과 백성을 먼저 지켜야 한다.'
},
{
id: 'eighth-xiaopei-road',
bgm: 'battle-prep',
chapter: '소패 보급로',
background: 'story-sortie',
speaker: '미축',
text: '소패로 이어지는 창고 길목이 습격받고 있습니다. 군량이 끊기면 서주를 안정시킬 수 없습니다. 보급로와 마을을 동시에 지켜야 합니다.'
},
{
id: 'eighth-sortie-choice',
bgm: 'battle-prep',
chapter: '첫 방위전',
background: 'story-three-heroes',
speaker: '간옹',
text: '현덕, 이제 함께 갈 장수를 고르는 일도 전술이 되었네. 칼이 필요한 곳과 말이 필요한 곳, 그리고 말보다 장부가 필요한 곳을 구분해야 하네.'
}
];
export const eighthBattleVictoryPages: StoryPage[] = [
{
id: 'eighth-victory-supply-road',
bgm: 'militia-theme',
chapter: '지켜낸 보급로',
background: 'story-militia',
text: '소패의 보급로가 다시 열리자 서주의 창고와 마을은 겨우 숨을 돌렸다. 새로 합류한 장수들은 각자의 방식으로 유비군의 빈틈을 메웠다.'
},
{
id: 'eighth-lu-bu-arrival',
bgm: 'battle-prep',
chapter: '뜻밖의 손님',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '전투가 끝난 뒤, 여포가 몸을 의탁할 곳을 구한다는 전갈이 왔다. 그를 받아들이는 일은 의로울 수 있으나, 서주에 또 다른 불씨를 들이는 일이기도 했다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -727,6 +781,12 @@ export const seventhBattleMap: BattleMap = {
]
};
export const eighthBattleMap: BattleMap = {
width: 26,
height: 22,
terrain: createEighthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -2611,6 +2671,251 @@ export const xuzhouRecruitUnits: UnitData[] = [
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
'zhang-fei': { x: 2, y: 18 },
'jian-yong': { x: 3, y: 18 },
'mi-zhu': { x: 1, y: 18 }
};
export const eighthBattleUnits: UnitData[] = [
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
placeScenarioUnit(unit, eighthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
),
{
id: 'xiaopei-raider-a',
name: '여포군 척후',
faction: 'enemy',
className: '기동 척후',
classKey: 'cavalry',
level: 8,
exp: 52,
hp: 43,
maxHp: 43,
attack: 15,
move: 5,
stats: { might: 82, intelligence: 50, leadership: 62, agility: 86, luck: 55 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 28 },
armor: { itemId: 'rebel-vest', level: 2, exp: 14 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 17,
y: 9
},
{
id: 'xiaopei-raider-b',
name: '여포군 척후',
faction: 'enemy',
className: '기동 척후',
classKey: 'cavalry',
level: 8,
exp: 54,
hp: 44,
maxHp: 44,
attack: 15,
move: 5,
stats: { might: 83, intelligence: 50, leadership: 62, agility: 86, luck: 55 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 30 },
armor: { itemId: 'rebel-vest', level: 2, exp: 14 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 19,
y: 10
},
{
id: 'xiaopei-infantry-a',
name: '소패 침투병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 8,
exp: 50,
hp: 42,
maxHp: 42,
attack: 13,
move: 3,
stats: { might: 78, intelligence: 50, leadership: 66, agility: 61, luck: 53 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 26 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 12 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 11,
y: 12
},
{
id: 'xiaopei-infantry-b',
name: '소패 침투병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 8,
exp: 52,
hp: 43,
maxHp: 43,
attack: 13,
move: 3,
stats: { might: 79, intelligence: 50, leadership: 67, agility: 62, luck: 53 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 28 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 12 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 12,
y: 14
},
{
id: 'xiaopei-archer-a',
name: '소패 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 8,
exp: 52,
hp: 34,
maxHp: 34,
attack: 14,
move: 3,
stats: { might: 70, intelligence: 62, leadership: 60, agility: 71, luck: 54 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 28 },
armor: { itemId: 'cloth-armor', level: 2, exp: 12 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 18,
y: 7
},
{
id: 'xiaopei-archer-b',
name: '소패 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 9,
exp: 54,
hp: 35,
maxHp: 35,
attack: 15,
move: 3,
stats: { might: 72, intelligence: 63, leadership: 61, agility: 72, luck: 55 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 30 },
armor: { itemId: 'cloth-armor', level: 2, exp: 12 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 21,
y: 14
},
{
id: 'xiaopei-guard-a',
name: '창고 습격병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'bandit',
level: 8,
exp: 50,
hp: 38,
maxHp: 38,
attack: 14,
move: 4,
stats: { might: 79, intelligence: 48, leadership: 58, agility: 72, luck: 52 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 26 },
armor: { itemId: 'rebel-vest', level: 2, exp: 12 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 8,
y: 11
},
{
id: 'xiaopei-guard-b',
name: '창고 습격병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'bandit',
level: 8,
exp: 52,
hp: 39,
maxHp: 39,
attack: 14,
move: 4,
stats: { might: 80, intelligence: 48, leadership: 58, agility: 73, luck: 52 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 28 },
armor: { itemId: 'rebel-vest', level: 2, exp: 12 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 9,
y: 14
},
{
id: 'xiaopei-cavalry-a',
name: '여포군 기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 9,
exp: 56,
hp: 46,
maxHp: 46,
attack: 16,
move: 5,
stats: { might: 86, intelligence: 50, leadership: 66, agility: 88, luck: 56 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 32 },
armor: { itemId: 'rebel-vest', level: 2, exp: 16 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 23,
y: 6
},
{
id: 'xiaopei-cavalry-b',
name: '여포군 기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 9,
exp: 58,
hp: 47,
maxHp: 47,
attack: 16,
move: 5,
stats: { might: 87, intelligence: 50, leadership: 66, agility: 89, luck: 56 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 34 },
armor: { itemId: 'rebel-vest', level: 2, exp: 16 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 24,
y: 15
},
{
id: 'xiaopei-leader-gao-shun',
name: '고순',
faction: 'enemy',
className: '여포군 선봉장',
classKey: 'rebelLeader',
level: 10,
exp: 62,
hp: 62,
maxHp: 62,
attack: 17,
move: 4,
stats: { might: 88, intelligence: 66, leadership: 86, agility: 68, luck: 58 },
equipment: {
weapon: { itemId: 'leader-axe', level: 2, exp: 34 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 18 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 22,
y: 3
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -2663,6 +2968,39 @@ export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleB
export const fifthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const sixthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const seventhBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const eighthBattleBonds: BattleBond[] = [...firstBattleBonds, ...xuzhouRecruitBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
Array.from({ length: 26 }, (_, x): TerrainType => {
if (x <= 1 && y >= 16 && y <= 20) {
return 'camp';
}
if ((x === 2 && y >= 15 && y <= 20) || (x === 3 && y >= 18 && y <= 20)) {
return 'camp';
}
if ((x >= 20 && x <= 23 && y >= 2 && y <= 5) || (x >= 21 && x <= 24 && y >= 13 && y <= 16)) {
return 'fort';
}
if ((x >= 17 && x <= 18 && y >= 7 && y <= 9) || (x >= 8 && x <= 9 && y >= 12 && y <= 14)) {
return 'village';
}
if ((x === 14 || x === 15) && y <= 18 && y >= 0) {
return 'river';
}
if (x === y - 12 || x === y - 13 || (y >= 7 && y <= 10 && x >= 2 && x <= 18) || (x >= 4 && x <= 22 && y === 10)) {
return 'road';
}
if ((x <= 6 && y <= 4) || (x >= 5 && x <= 10 && y >= 3 && y <= 7) || (x >= 2 && x <= 6 && y >= 8 && y <= 12) || (x >= 18 && x <= 25 && y >= 17)) {
return 'forest';
}
if ((x >= 18 && y <= 2) || (x >= 22 && y >= 7 && y <= 12) || (x >= 10 && x <= 13 && y >= 15 && y <= 20) || (x <= 4 && y >= 4 && y <= 7)) {
return 'hill';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {