Add Jieqiao relief campaign battle

This commit is contained in:
2026-06-23 00:24:03 +09:00
parent 8e9bddcacf
commit 84a5606a73
10 changed files with 737 additions and 16 deletions

View File

@@ -11,6 +11,10 @@ import {
secondBattleMap,
secondBattleUnits,
secondBattleVictoryPages,
sixthBattleBonds,
sixthBattleMap,
sixthBattleUnits,
sixthBattleVictoryPages,
thirdBattleBonds,
fourthBattleBonds,
fourthBattleMap,
@@ -30,7 +34,8 @@ export type BattleScenarioId =
| 'second-battle-yellow-turban-pursuit'
| 'third-battle-guangzong-road'
| 'fourth-battle-guangzong-camp'
| 'fifth-battle-sishui-vanguard';
| 'fifth-battle-sishui-vanguard'
| 'sixth-battle-jieqiao-relief';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -334,6 +339,59 @@ export const fifthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const sixthBattleScenario: BattleScenarioDefinition = {
id: 'sixth-battle-jieqiao-relief',
title: '계교 원군로',
victoryConditionLabel: '곡의 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'공손찬의 보급로를 원소군 교위 곡의가 끊으려 합니다. 곡의를 격파하면 계교로 향하는 길을 다시 열 수 있습니다.',
'강가의 궁병과 측면 기병이 동시에 압박합니다. 숲길 척후를 먼저 밀어내고 중앙 길을 확보하십시오.',
'15턴 이내에 승리하면 공손찬 진영에서 세 형제의 신뢰가 커집니다.'
],
map: sixthBattleMap,
units: sixthBattleUnits,
bonds: sixthBattleBonds,
mapTextureKey: 'battle-map-sixth',
leaderUnitId: 'jieqiao-leader-qu-yi',
quickVictoryTurnLimit: 15,
baseVictoryGold: 860,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '곡의 격파',
rewardGold: 520,
unitId: 'jieqiao-leader-qu-yi'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 210,
unitId: 'liu-bei'
},
{
id: 'village',
kind: 'secure-terrain',
label: '계교 보급촌 확보',
rewardGold: 300,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '15턴 이내 승리',
rewardGold: 250,
maxTurn: 15
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 2', '상처약 2', '탁주 1', '공손찬 신뢰 +1'],
victoryPages: sixthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -341,7 +399,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'second-battle-yellow-turban-pursuit': secondBattleScenario,
'third-battle-guangzong-road': thirdBattleScenario,
'fourth-battle-guangzong-camp': fourthBattleScenario,
'fifth-battle-sishui-vanguard': fifthBattleScenario
'fifth-battle-sishui-vanguard': fifthBattleScenario,
'sixth-battle-jieqiao-relief': sixthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -4,6 +4,7 @@ import {
fifthBattleScenario,
fourthBattleScenario,
secondBattleScenario,
sixthBattleScenario,
thirdBattleScenario,
type BattleScenarioId
} from './battles';
@@ -15,6 +16,8 @@ import {
fourthBattleVictoryPages,
secondBattleIntroPages,
secondBattleVictoryPages,
sixthBattleIntroPages,
sixthBattleVictoryPages,
thirdBattleIntroPages,
thirdBattleVictoryPages,
type StoryPage
@@ -75,12 +78,22 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[fifthBattleScenario.id]: {
afterBattleId: fifthBattleScenario.id,
eyebrow: '다음 전장',
title: sixthBattleScenario.title,
description: '공손찬의 군영에 의탁한 세 형제는 북방 전선의 보급로를 지키는 임무를 맡습니다. 계교로 향하는 길목에서 원소군 별동대를 상대해야 합니다.',
rewardHint: `예상 보상: ${sixthBattleScenario.title} 개방`,
nextBattleId: sixthBattleScenario.id,
campaignStep: 'sixth-battle',
pages: [...fifthBattleVictoryPages, ...sixthBattleIntroPages]
},
[sixthBattleScenario.id]: {
afterBattleId: sixthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '공손찬 의탁',
description: '사수관 앞 전초전을 넘긴 유비군은 공손찬 진영에서 연합군의 큰 흐름을 살피게 됩니다. 다음 줄기는 공손찬 의탁과 그 뒤의 서주로 이어집니다.',
rewardHint: '다음 장: 공손찬 의탁 준비 중',
pages: fifthBattleVictoryPages,
unavailableNotice: '공손찬 의탁 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'
title: '도겸의 서주',
description: '계교 원군로를 열어 공손찬의 신뢰를 얻은 유비군에게 서주의 도겸이 원군을 청합니다. 다음 줄기는 서주 인수로 이어집니다.',
rewardHint: '다음 장: 도겸에게 서주 인수 준비 중',
pages: sixthBattleVictoryPages,
unavailableNotice: '도겸과 서주의 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'
}
};

View File

@@ -428,6 +428,62 @@ export const fifthBattleVictoryPages: StoryPage[] = [
}
];
export const sixthBattleIntroPages: StoryPage[] = [
{
id: 'sixth-gongsun-zan-order',
bgm: 'militia-theme',
chapter: '공손찬의 명',
background: 'story-three-heroes',
text: '사수관 전초전 뒤, 유비군은 공손찬의 군영에 머물며 북방의 정세를 살폈다. 원소군의 별동대가 계교로 향하는 보급로를 위협하자, 공손찬은 세 형제에게 길목을 맡겼다.'
},
{
id: 'sixth-liu-bei-service',
bgm: 'battle-prep',
chapter: '의탁의 무게',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '남의 깃발 아래에 서더라도 백성을 지키는 뜻은 변하지 않는다. 공손찬 장군의 군영에 머무는 동안에도 우리의 길을 분명히 하자.'
},
{
id: 'sixth-guan-yu-road',
bgm: 'battle-prep',
chapter: '계교 원군로',
background: 'story-militia',
speaker: '관우',
portrait: 'guanYu',
text: '계교로 향하는 길은 강과 숲이 얽혀 있습니다. 원소군의 궁병과 기병을 나누어 상대해야 길을 열 수 있습니다.'
},
{
id: 'sixth-zhang-fei-sortie',
bgm: 'battle-prep',
chapter: '북방의 먼지',
background: 'story-sortie',
speaker: '장비',
portrait: 'zhangFei',
text: '원소군이든 누구든 길을 막는 놈은 비켜서게 만들면 됩니다. 형님, 이번엔 제가 먼저 먼지를 일으키겠습니다!'
}
];
export const sixthBattleVictoryPages: StoryPage[] = [
{
id: 'sixth-victory-jieqiao',
bgm: 'militia-theme',
chapter: '계교의 길',
background: 'story-militia',
text: '계교로 향하는 보급로가 열리자 공손찬의 군영에도 한숨 돌릴 여유가 생겼다. 그러나 북방의 전공은 세 형제의 마음을 오래 붙잡아 두지 못했다.'
},
{
id: 'sixth-xuzhou-envoy',
bgm: 'battle-prep',
chapter: '서주의 사신',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '도겸 공이 서주에서 원군을 청한다면, 백성의 고통을 외면할 수 없소. 공손찬 장군께 허락을 구하고 서주로 향할 준비를 하자.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -561,6 +617,33 @@ export const fifthBattleMap: BattleMap = {
]
};
export const sixthBattleMap: BattleMap = {
width: 24,
height: 20,
terrain: [
['forest', 'hill', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'hill', 'hill', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'fort', 'fort', 'plain', 'hill', 'hill', 'forest', 'forest', 'plain'],
['hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'road', 'river', 'river', 'road', 'plain', 'fort', 'plain', 'plain', 'plain', 'hill', 'forest', 'plain', 'plain'],
['plain', 'plain', 'forest', 'plain', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'road', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain'],
['plain', 'forest', 'forest', 'plain', 'road', 'plain', 'hill', 'forest', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'road', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain'],
['plain', 'plain', 'forest', 'road', 'road', 'plain', 'hill', 'plain', 'plain', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill'],
['plain', 'plain', 'plain', 'road', 'plain', 'plain', 'plain', 'plain', 'forest', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'road', 'plain', 'village', 'plain', 'forest', 'forest', 'plain', 'hill'],
['forest', 'plain', 'plain', 'road', 'plain', 'hill', 'hill', 'plain', 'forest', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'plain', 'village', 'plain', 'plain', 'forest', 'plain', 'plain'],
['forest', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'road', 'forest', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain'],
['plain', 'plain', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'road', 'forest', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain'],
['plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'hill', 'road', 'road', 'road', 'road', 'road', 'road', 'river', 'river', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'plain'],
['road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'road', 'plain', 'plain', 'hill', 'plain', 'plain'],
['plain', 'road', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'hill', 'plain', 'plain'],
['plain', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'plain', 'plain'],
['plain', 'road', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'hill', 'plain'],
['camp', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain'],
['camp', 'camp', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain'],
['plain', 'camp', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'plain'],
['plain', 'plain', 'camp', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'plain'],
['plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain'],
['plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain']
]
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -1872,6 +1955,270 @@ export const fifthBattleUnits: UnitData[] = [
}
];
const sixthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 16 },
'guan-yu': { x: 3, y: 16 },
'zhang-fei': { x: 2, y: 17 }
};
export const sixthBattleUnits: UnitData[] = [
...firstBattleUnits
.filter((unit) => unit.faction === 'ally')
.map((unit) => placeScenarioUnit(unit, sixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
{
id: 'jieqiao-skirmisher-a',
name: '원소군 척후',
faction: 'enemy',
className: '척후병',
classKey: 'bandit',
level: 6,
exp: 36,
hp: 32,
maxHp: 32,
attack: 11,
move: 4,
stats: { might: 72, intelligence: 43, leadership: 50, agility: 68, luck: 50 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 14 },
armor: { itemId: 'rebel-vest', level: 2, exp: 6 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 5,
y: 12
},
{
id: 'jieqiao-infantry-a',
name: '원소 보병',
faction: 'enemy',
className: '원소 보병',
classKey: 'yellowTurban',
level: 6,
exp: 36,
hp: 36,
maxHp: 36,
attack: 11,
move: 3,
stats: { might: 73, intelligence: 42, leadership: 58, agility: 56, luck: 49 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 14 },
armor: { itemId: 'rebel-vest', level: 2, exp: 6 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 7,
y: 11
},
{
id: 'jieqiao-archer-a',
name: '원소 궁병',
faction: 'enemy',
className: '원소 궁병',
classKey: 'archer',
level: 6,
exp: 38,
hp: 29,
maxHp: 29,
attack: 12,
move: 3,
stats: { might: 66, intelligence: 54, leadership: 54, agility: 66, luck: 50 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 16 },
armor: { itemId: 'cloth-armor', level: 2, exp: 4 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 9,
y: 10
},
{
id: 'jieqiao-cavalry-a',
name: '원소 기병',
faction: 'enemy',
className: '원소 경기병',
classKey: 'cavalry',
level: 7,
exp: 40,
hp: 39,
maxHp: 39,
attack: 13,
move: 5,
stats: { might: 78, intelligence: 42, leadership: 60, agility: 78, luck: 52 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 18 },
armor: { itemId: 'rebel-vest', level: 2, exp: 8 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 12,
y: 9
},
{
id: 'jieqiao-skirmisher-b',
name: '강가 척후',
faction: 'enemy',
className: '척후병',
classKey: 'bandit',
level: 6,
exp: 38,
hp: 33,
maxHp: 33,
attack: 12,
move: 4,
stats: { might: 74, intelligence: 44, leadership: 51, agility: 69, luck: 51 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 16 },
armor: { itemId: 'rebel-vest', level: 2, exp: 6 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 6,
y: 7
},
{
id: 'jieqiao-archer-b',
name: '강안 궁병',
faction: 'enemy',
className: '원소 궁병',
classKey: 'archer',
level: 7,
exp: 40,
hp: 30,
maxHp: 30,
attack: 13,
move: 3,
stats: { might: 68, intelligence: 56, leadership: 56, agility: 67, luck: 51 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 18 },
armor: { itemId: 'cloth-armor', level: 2, exp: 6 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 11,
y: 6
},
{
id: 'jieqiao-cavalry-b',
name: '원소 기병',
faction: 'enemy',
className: '원소 경기병',
classKey: 'cavalry',
level: 7,
exp: 42,
hp: 40,
maxHp: 40,
attack: 13,
move: 5,
stats: { might: 79, intelligence: 42, leadership: 61, agility: 79, luck: 52 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 20 },
armor: { itemId: 'rebel-vest', level: 2, exp: 8 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 15,
y: 8
},
{
id: 'jieqiao-guard-a',
name: '계교 수비병',
faction: 'enemy',
className: '원소 보병',
classKey: 'yellowTurban',
level: 7,
exp: 42,
hp: 38,
maxHp: 38,
attack: 12,
move: 3,
stats: { might: 76, intelligence: 45, leadership: 62, agility: 58, luck: 51 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 20 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 8 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 16,
y: 5
},
{
id: 'jieqiao-cavalry-c',
name: '측면 기병',
faction: 'enemy',
className: '원소 경기병',
classKey: 'cavalry',
level: 7,
exp: 44,
hp: 41,
maxHp: 41,
attack: 13,
move: 5,
stats: { might: 80, intelligence: 43, leadership: 62, agility: 80, luck: 53 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 22 },
armor: { itemId: 'rebel-vest', level: 2, exp: 10 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 20,
y: 5
},
{
id: 'jieqiao-guard-b',
name: '계교 수비병',
faction: 'enemy',
className: '원소 보병',
classKey: 'yellowTurban',
level: 7,
exp: 44,
hp: 39,
maxHp: 39,
attack: 12,
move: 3,
stats: { might: 77, intelligence: 45, leadership: 63, agility: 58, luck: 52 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 22 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 8 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 18,
y: 3
},
{
id: 'jieqiao-archer-c',
name: '계교 궁병',
faction: 'enemy',
className: '원소 궁병',
classKey: 'archer',
level: 7,
exp: 44,
hp: 31,
maxHp: 31,
attack: 13,
move: 3,
stats: { might: 69, intelligence: 57, leadership: 57, agility: 68, luck: 52 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 22 },
armor: { itemId: 'cloth-armor', level: 2, exp: 8 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 16,
y: 1
},
{
id: 'jieqiao-leader-qu-yi',
name: '곡의',
faction: 'enemy',
className: '원소군 교위',
classKey: 'rebelLeader',
level: 8,
exp: 48,
hp: 54,
maxHp: 54,
attack: 15,
move: 3,
stats: { might: 84, intelligence: 62, leadership: 78, agility: 62, luck: 56 },
equipment: {
weapon: { itemId: 'leader-axe', level: 2, exp: 26 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 14 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 17,
y: 1
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -1903,6 +2250,7 @@ export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleB
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fifthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const sixthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {