Add Cao break chapter and Sun Qian sortie

This commit is contained in:
2026-06-23 04:08:15 +09:00
parent 3cdbbe8d5a
commit 09c1390ba5
11 changed files with 900 additions and 23 deletions

View File

@@ -1,5 +1,9 @@
import {
firstBattleBonds,
fourteenthBattleBonds,
fourteenthBattleMap,
fourteenthBattleUnits,
fourteenthBattleVictoryPages,
eighthBattleBonds,
eighthBattleMap,
eighthBattleUnits,
@@ -70,7 +74,8 @@ export type BattleScenarioId =
| 'tenth-battle-xuzhou-breakout'
| 'eleventh-battle-xudu-refuge-road'
| 'twelfth-battle-xiapi-outer-siege'
| 'thirteenth-battle-xiapi-final';
| 'thirteenth-battle-xiapi-final'
| 'fourteenth-battle-cao-break';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -798,6 +803,59 @@ export const thirteenthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const fourteenthBattleScenario: BattleScenarioDefinition = {
id: 'fourteenth-battle-cao-break',
title: '서주 재기',
victoryConditionLabel: '차주 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'조조의 감시대가 서주 관문과 역참을 틀어쥐고 있습니다. 차주를 격파하면 유비군은 조조 휘하를 벗어날 첫 발판을 세웁니다.',
'조조군 기병은 길을 따라 추격하고, 궁병은 관문 뒤에서 사거리를 잡습니다. 손건의 문서와 간옹의 말로 민심을 흔들며 길목을 열어야 합니다.',
'서주 관문을 확보하고 22턴 안에 승리하면 원소 진영으로 향할 명분과 보급을 더 많이 보존합니다.'
],
map: fourteenthBattleMap,
units: fourteenthBattleUnits,
bonds: fourteenthBattleBonds,
mapTextureKey: 'battle-map-fourteenth',
leaderUnitId: 'cao-break-leader-che-zhou',
quickVictoryTurnLimit: 22,
baseVictoryGold: 1660,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '차주 격파',
rewardGold: 1040,
unitId: 'cao-break-leader-che-zhou'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 380,
unitId: 'liu-bei'
},
{
id: 'gate',
kind: 'secure-terrain',
label: '서주 관문 확보',
rewardGold: 600,
terrain: 'fort'
},
{
id: 'quick',
kind: 'quick-victory',
label: '22턴 이내 승리',
rewardGold: 460,
maxTurn: 22
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 5', '상처약 3', '탁주 2', '손건 문서 +1'],
victoryPages: fourteenthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -813,7 +871,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'tenth-battle-xuzhou-breakout': tenthBattleScenario,
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario,
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario,
'thirteenth-battle-xiapi-final': thirteenthBattleScenario
'thirteenth-battle-xiapi-final': thirteenthBattleScenario,
'fourteenth-battle-cao-break': fourteenthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -4,6 +4,7 @@ import {
eighthBattleScenario,
eleventhBattleScenario,
fifthBattleScenario,
fourteenthBattleScenario,
fourthBattleScenario,
ninthBattleScenario,
secondBattleScenario,
@@ -22,6 +23,8 @@ import {
eleventhBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
fourteenthBattleIntroPages,
fourteenthBattleVictoryPages,
firstBattleVictoryPages,
fourthBattleIntroPages,
fourthBattleVictoryPages,
@@ -179,12 +182,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[thirteenthBattleScenario.id]: {
afterBattleId: thirteenthBattleScenario.id,
eyebrow: '다음 전장',
title: fourteenthBattleScenario.title,
description:
'여포가 무너진 뒤 조조의 경계는 더 깊어졌습니다. 손건이 합류해 서주 길목의 문서와 민심을 준비하고, 유비군은 조조의 장막 밖으로 나갈 첫 전투를 맞습니다.',
rewardHint: `예상 보상: ${fourteenthBattleScenario.title} 개방 / 손건 합류`,
nextBattleId: fourteenthBattleScenario.id,
campaignStep: 'fourteenth-battle',
pages: [...thirteenthBattleVictoryPages, ...fourteenthBattleIntroPages]
},
[fourteenthBattleScenario.id]: {
afterBattleId: fourteenthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '조조 이탈의 불씨',
description: '여포가 무너지고 하비 결전은 끝났지만, 조조의 칭찬과 경계는 동시에 깊어졌습니다. 다음 흐름은 조조 휘하를 벗어날 명분과 때를 준비하는 장으로 이어집니다.',
rewardHint: '다음 장: 조조 이탈과 원소 의탁 준비 중',
pages: thirteenthBattleVictoryPages,
unavailableNotice: '조조 이탈과 원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.'
title: '원소 의탁의 길',
description:
'조조의 장막을 벗어난 유비군은 원소 진영으로 향할 수밖에 없습니다. 다음 흐름은 원소 의탁과 조조의 추격을 피해 북상하는 장으로 이어집니다.',
rewardHint: '다음 장: 원소 의탁 준비 중',
pages: fourteenthBattleVictoryPages,
unavailableNotice: '원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.'
}
};

View File

@@ -877,6 +877,70 @@ export const thirteenthBattleVictoryPages: StoryPage[] = [
}
];
export const fourteenthBattleIntroPages: StoryPage[] = [
{
id: 'fourteenth-secret-edict-shadow',
bgm: 'story-dark',
chapter: '조조의 그늘',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '하비의 공은 컸지만 조조의 시선은 더욱 무거워졌다. 허도의 잔치와 칭찬 사이에서, 유비는 황실의 명분과 자신의 뜻이 더는 같은 장막 안에 머물 수 없음을 느꼈다.'
},
{
id: 'fourteenth-sun-qian-arrives',
bgm: 'militia-theme',
chapter: '새로 온 책사',
background: 'story-militia',
speaker: '손건',
text: '현덕공, 저는 손건이라 합니다. 서주와 허도 사이의 길을 오가며 민심과 조조군의 움직임을 살펴 왔습니다. 이제 떠날 때를 고르신다면, 그 길의 말과 문서를 제가 맡겠습니다.'
},
{
id: 'fourteenth-brothers-break-from-cao',
bgm: 'battle-prep',
chapter: '서주로 나아가다',
background: 'story-three-heroes',
speaker: '장비',
portrait: 'zhangFei',
text: '조조의 눈치를 보느라 칼집만 만지고 있을 순 없습니다. 형님, 서주 길목의 감시대를 뚫고 백성들이 우리 깃발을 다시 볼 수 있게 합시다.'
},
{
id: 'fourteenth-xuzhou-gate-sortie',
bgm: 'battle-prep',
chapter: '서주 재기',
background: 'story-sortie',
speaker: '간옹',
text: '차주는 조조의 명을 앞세워 성문과 역참을 잡고 있습니다. 길목을 끊고 차주를 격파하면, 조조의 장막 밖으로 나갈 첫 명분을 세울 수 있습니다.'
}
];
export const fourteenthBattleVictoryPages: StoryPage[] = [
{
id: 'fourteenth-victory-xuzhou-gate-opens',
bgm: 'militia-theme',
chapter: '다시 열린 서주 길',
background: 'story-militia',
text: '차주의 감시대가 흩어지자 서주로 향하는 관문이 열렸다. 백성들은 유비군의 깃발을 알아보았고, 조조의 이름보다 현덕의 이름을 먼저 속삭이기 시작했다.'
},
{
id: 'fourteenth-cao-cao-anger',
bgm: 'story-dark',
chapter: '돌이킬 수 없는 길',
background: 'story-liu-bei',
speaker: '손건',
text: '이 승리는 조조에게는 반역의 조짐으로 보일 것입니다. 허도로 돌아갈 길은 사실상 끊겼고, 다음으로 기대야 할 큰 세력은 원소입니다.'
},
{
id: 'fourteenth-road-to-yuan-shao',
bgm: 'battle-prep',
chapter: '원소를 향하여',
background: 'story-three-heroes',
speaker: '유비',
portrait: 'liuBei',
text: '조조를 떠난 이상, 더 큰 파도 속으로 들어가야 한다. 원소의 진영은 안전한 항구가 아니라 또 다른 시험일 것이다. 그래도 뜻을 이어 가려면 그 길을 지나야 한다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1100,6 +1164,12 @@ export const thirteenthBattleMap: BattleMap = {
terrain: createThirteenthBattleTerrain()
};
export const fourteenthBattleMap: BattleMap = {
width: 32,
height: 24,
terrain: createFourteenthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -2984,6 +3054,30 @@ export const xuzhouRecruitUnits: UnitData[] = [
}
];
export const caoBreakRecruitUnits: UnitData[] = [
{
id: 'sun-qian',
name: '손건',
faction: 'ally',
className: '책사',
classKey: 'strategist',
level: 5,
exp: 10,
hp: 27,
maxHp: 27,
attack: 7,
move: 4,
stats: { might: 38, intelligence: 82, leadership: 68, agility: 62, luck: 78 },
equipment: {
weapon: { itemId: 'training-sword', level: 1, exp: 8 },
armor: { itemId: 'cloth-armor', level: 1, exp: 8 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 5,
y: 20
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -4580,6 +4674,336 @@ export const thirteenthBattleUnits: UnitData[] = [
}
];
const fourteenthBattleAllyPositions: 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 },
'sun-qian': { x: 5, y: 21 }
};
export const fourteenthBattleUnits: UnitData[] = [
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits, ...caoBreakRecruitUnits].map((unit) =>
placeScenarioUnit(unit, fourteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
),
{
id: 'cao-break-scout-a',
name: '조조군 척후',
faction: 'enemy',
className: '조조 척후',
classKey: 'bandit',
level: 14,
exp: 86,
hp: 52,
maxHp: 52,
attack: 19,
move: 4,
stats: { might: 88, intelligence: 61, leadership: 66, agility: 86, luck: 62 },
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: 11,
y: 15
},
{
id: 'cao-break-scout-b',
name: '조조군 척후',
faction: 'enemy',
className: '조조 척후',
classKey: 'bandit',
level: 14,
exp: 86,
hp: 52,
maxHp: 52,
attack: 19,
move: 4,
stats: { might: 88, intelligence: 61, leadership: 66, agility: 86, luck: 62 },
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: 13,
y: 20
},
{
id: 'cao-break-infantry-a',
name: '조조 보병',
faction: 'enemy',
className: '조조 보병',
classKey: 'yellowTurban',
level: 14,
exp: 86,
hp: 58,
maxHp: 58,
attack: 18,
move: 3,
stats: { might: 90, intelligence: 60, leadership: 76, agility: 68, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 26 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 15,
y: 13
},
{
id: 'cao-break-infantry-b',
name: '조조 보병',
faction: 'enemy',
className: '조조 보병',
classKey: 'yellowTurban',
level: 14,
exp: 88,
hp: 59,
maxHp: 59,
attack: 18,
move: 3,
stats: { might: 91, intelligence: 60, leadership: 77, agility: 69, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 28 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 16,
y: 14
},
{
id: 'cao-break-infantry-c',
name: '조조 보병',
faction: 'enemy',
className: '조조 보병',
classKey: 'yellowTurban',
level: 14,
exp: 88,
hp: 59,
maxHp: 59,
attack: 18,
move: 3,
stats: { might: 91, intelligence: 60, leadership: 77, agility: 69, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 28 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 19,
y: 10
},
{
id: 'cao-break-infantry-d',
name: '조조 보병',
faction: 'enemy',
className: '조조 보병',
classKey: 'yellowTurban',
level: 15,
exp: 90,
hp: 61,
maxHp: 61,
attack: 19,
move: 3,
stats: { might: 92, intelligence: 61, leadership: 79, agility: 70, luck: 61 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 66 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 30 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 22,
y: 15
},
{
id: 'cao-break-archer-a',
name: '조조 궁병',
faction: 'enemy',
className: '조조 궁병',
classKey: 'archer',
level: 14,
exp: 86,
hp: 48,
maxHp: 48,
attack: 19,
move: 3,
stats: { might: 81, intelligence: 72, leadership: 70, agility: 82, luck: 61 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 62 },
armor: { itemId: 'cloth-armor', level: 2, exp: 24 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 18,
y: 8
},
{
id: 'cao-break-archer-b',
name: '조조 궁병',
faction: 'enemy',
className: '조조 궁병',
classKey: 'archer',
level: 14,
exp: 88,
hp: 49,
maxHp: 49,
attack: 19,
move: 3,
stats: { might: 82, intelligence: 73, leadership: 71, agility: 83, luck: 61 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 64 },
armor: { itemId: 'cloth-armor', level: 2, exp: 26 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 24,
y: 9
},
{
id: 'cao-break-archer-c',
name: '조조 궁병',
faction: 'enemy',
className: '조조 궁병',
classKey: 'archer',
level: 14,
exp: 88,
hp: 49,
maxHp: 49,
attack: 19,
move: 3,
stats: { might: 82, intelligence: 73, leadership: 71, agility: 83, luck: 61 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 64 },
armor: { itemId: 'cloth-armor', level: 2, exp: 26 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 26,
y: 16
},
{
id: 'cao-break-cavalry-a',
name: '조조 기병',
faction: 'enemy',
className: '조조 경기병',
classKey: 'cavalry',
level: 15,
exp: 90,
hp: 66,
maxHp: 66,
attack: 22,
move: 5,
stats: { might: 100, intelligence: 58, leadership: 80, agility: 100, luck: 63 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 66 },
armor: { itemId: 'rebel-vest', level: 2, exp: 30 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 20,
y: 6
},
{
id: 'cao-break-cavalry-b',
name: '조조 기병',
faction: 'enemy',
className: '조조 경기병',
classKey: 'cavalry',
level: 15,
exp: 90,
hp: 66,
maxHp: 66,
attack: 22,
move: 5,
stats: { might: 100, intelligence: 58, leadership: 80, agility: 100, luck: 63 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 66 },
armor: { itemId: 'rebel-vest', level: 2, exp: 30 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 25,
y: 13
},
{
id: 'cao-break-cavalry-c',
name: '조조 기병',
faction: 'enemy',
className: '조조 경기병',
classKey: 'cavalry',
level: 15,
exp: 92,
hp: 68,
maxHp: 68,
attack: 22,
move: 5,
stats: { might: 101, intelligence: 59, leadership: 81, agility: 101, luck: 64 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 68 },
armor: { itemId: 'rebel-vest', level: 2, exp: 32 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 28,
y: 18
},
{
id: 'cao-break-guard-a',
name: '서주 관문 수비대',
faction: 'enemy',
className: '조조 수비대',
classKey: 'yellowTurban',
level: 15,
exp: 92,
hp: 64,
maxHp: 64,
attack: 19,
move: 3,
stats: { might: 93, intelligence: 64, leadership: 82, agility: 70, luck: 62 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 68 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 25,
y: 11
},
{
id: 'cao-break-guard-b',
name: '서주 관문 수비대',
faction: 'enemy',
className: '조조 수비대',
classKey: 'yellowTurban',
level: 15,
exp: 92,
hp: 64,
maxHp: 64,
attack: 19,
move: 3,
stats: { might: 93, intelligence: 64, leadership: 82, agility: 70, luck: 62 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 68 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 26,
y: 12
},
{
id: 'cao-break-leader-che-zhou',
name: '차주',
faction: 'enemy',
className: '조조군 장수',
classKey: 'rebelLeader',
level: 17,
exp: 98,
hp: 88,
maxHp: 88,
attack: 24,
move: 4,
stats: { might: 102, intelligence: 72, leadership: 92, agility: 78, luck: 65 },
equipment: {
weapon: { itemId: 'leader-axe', level: 3, exp: 10 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 36 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 27,
y: 11
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -4626,6 +5050,25 @@ export const xuzhouRecruitBonds: BattleBond[] = [
}
];
export const caoBreakRecruitBonds: BattleBond[] = [
{
id: 'liu-bei__sun-qian',
unitIds: ['liu-bei', 'sun-qian'],
title: '문서와 명분',
level: 40,
exp: 0,
description: '손건은 조조의 감시망 속에서 유비군이 움직일 명분과 길을 정리해 주는 책사입니다.'
},
{
id: 'jian-yong__sun-qian',
unitIds: ['jian-yong', 'sun-qian'],
title: '말과 글',
level: 34,
exp: 0,
description: '간옹의 말과 손건의 문서는 유비군이 난세의 틈을 지나갈 때 서로를 보완합니다.'
}
];
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -4638,6 +5081,9 @@ export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBo
export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario);
export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario);
export const thirteenthBattleBonds: BattleBond[] = twelfthBattleBonds.map(cloneBattleBondForScenario);
export const fourteenthBattleBonds: BattleBond[] = [...thirteenthBattleBonds, ...caoBreakRecruitBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -4888,6 +5334,60 @@ function createThirteenthBattleTerrain(): TerrainType[][] {
);
}
function createFourteenthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 24 }, (_, y) =>
Array.from({ length: 32 }, (_, x): TerrainType => {
if (x <= 2 && y >= 18 && y <= 22) {
return 'camp';
}
if (
(x >= 23 && x <= 29 && y >= 8 && y <= 14) ||
(x >= 25 && x <= 30 && y >= 15 && y <= 18)
) {
return 'fort';
}
if ((x >= 10 && x <= 12 && y >= 16 && y <= 18) || (x >= 17 && x <= 19 && y >= 10 && y <= 12)) {
return 'village';
}
if ((x === 15 || x === 16) && y >= 0 && y <= 22) {
return 'river';
}
if ((x === 21 || x === 22) && y >= 4 && y <= 20) {
return 'river';
}
if (
(y === 19 && x >= 2 && x <= 20) ||
(y === 12 && x >= 12 && x <= 27) ||
(x === 24 && y >= 7 && y <= 18)
) {
return 'road';
}
if ((x >= 3 && x <= 16 && y === 22 - x) || (x >= 9 && x <= 24 && y === x - 4)) {
return 'road';
}
if (
(x <= 8 && y <= 7) ||
(x >= 4 && x <= 12 && y >= 8 && y <= 13) ||
(x >= 2 && x <= 9 && y >= 14 && y <= 17) ||
(x >= 27 && y >= 18 && y <= 22)
) {
return 'forest';
}
if (
(x >= 6 && x <= 11 && y >= 2 && y <= 5) ||
(x >= 27 && x <= 31 && y >= 1 && y <= 6) ||
(x >= 9 && x <= 13 && y >= 20)
) {
return 'hill';
}
if ((x >= 30 && y <= 7) || (x >= 30 && y >= 15) || (x <= 1 && y <= 4)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -88,7 +88,22 @@ const unitTexture: Record<string, string> = {
'xiapi-final-raider-a': 'unit-rebel',
'xiapi-final-raider-b': 'unit-rebel',
'xiapi-final-strategist-chen-gong': 'unit-rebel-archer',
'xiapi-final-leader-lu-bu': 'unit-rebel-leader'
'xiapi-final-leader-lu-bu': 'unit-rebel-leader',
'cao-break-scout-a': 'unit-rebel',
'cao-break-scout-b': 'unit-rebel',
'cao-break-infantry-a': 'unit-rebel',
'cao-break-infantry-b': 'unit-rebel',
'cao-break-infantry-c': 'unit-rebel',
'cao-break-infantry-d': 'unit-rebel',
'cao-break-archer-a': 'unit-rebel-archer',
'cao-break-archer-b': 'unit-rebel-archer',
'cao-break-archer-c': 'unit-rebel-archer',
'cao-break-cavalry-a': 'unit-rebel-cavalry',
'cao-break-cavalry-b': 'unit-rebel-cavalry',
'cao-break-cavalry-c': 'unit-rebel-cavalry',
'cao-break-guard-a': 'unit-rebel',
'cao-break-guard-b': 'unit-rebel',
'cao-break-leader-che-zhou': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -510,7 +525,8 @@ const initialItemStocks: Record<string, Record<string, number>> = {
'guan-yu': { bean: 1 },
'zhang-fei': { bean: 1, wine: 1 },
'jian-yong': { bean: 1 },
'mi-zhu': { bean: 2, salve: 1 }
'mi-zhu': { bean: 2, salve: 1 },
'sun-qian': { bean: 1, salve: 1 }
};
const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
@@ -624,7 +640,22 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'xiapi-final-raider-a': 'aggressive',
'xiapi-final-raider-b': 'aggressive',
'xiapi-final-strategist-chen-gong': 'hold',
'xiapi-final-leader-lu-bu': 'guard'
'xiapi-final-leader-lu-bu': 'guard',
'cao-break-scout-a': 'aggressive',
'cao-break-scout-b': 'aggressive',
'cao-break-infantry-a': 'guard',
'cao-break-infantry-b': 'guard',
'cao-break-infantry-c': 'guard',
'cao-break-infantry-d': 'guard',
'cao-break-archer-a': 'hold',
'cao-break-archer-b': 'hold',
'cao-break-archer-c': 'hold',
'cao-break-cavalry-a': 'aggressive',
'cao-break-cavalry-b': 'aggressive',
'cao-break-cavalry-c': 'aggressive',
'cao-break-guard-a': 'guard',
'cao-break-guard-b': 'guard',
'cao-break-leader-che-zhou': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -3,6 +3,7 @@ import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg
import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg';
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
@@ -84,6 +85,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-eleventh', eleventhBattleMapUrl);
this.load.image('battle-map-twelfth', twelfthBattleMapUrl);
this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl);
this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -4,7 +4,16 @@ import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type
import { getUnitClass, terrainRules, type TerrainType } from '../data/battleRules';
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles';
import { getSortieFlow } from '../data/campaignFlow';
import { firstBattleBonds, firstBattleUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, type PortraitKey, type UnitData } from '../data/scenario';
import {
caoBreakRecruitBonds,
caoBreakRecruitUnits,
firstBattleBonds,
firstBattleUnits,
xuzhouRecruitBonds,
xuzhouRecruitUnits,
type PortraitKey,
type UnitData
} from '../data/scenario';
import {
applyCampBondExp,
ensureCampaignRosterUnits,
@@ -183,7 +192,8 @@ const campBattleIds = {
tenth: 'tenth-battle-xuzhou-breakout',
eleventh: 'eleventh-battle-xudu-refuge-road',
twelfth: 'twelfth-battle-xiapi-outer-siege',
thirteenth: 'thirteenth-battle-xiapi-final'
thirteenth: 'thirteenth-battle-xiapi-final',
fourteenth: 'fourteenth-battle-cao-break'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -316,6 +326,17 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'jian-yong', reason: '여포 처분과 조조의 시선을 둘러싼 명분 조율에 필요합니다.' }
],
note: '하비 결전은 전열과 돌파가 강해야 하지만, 전후 조조와의 긴장을 생각하면 책사 한 명을 챙기는 편성이 좋습니다.'
},
'fourteenth-battle-cao-break': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '조조 휘하를 벗어나는 명분과 생존 조건의 중심입니다.' },
{ unitId: 'guan-yu', reason: '관문 수비병을 정면에서 밀어낼 전열 핵심입니다.' },
{ unitId: 'zhang-fei', reason: '길목으로 돌진하는 조조군 기병을 받아치는 역할입니다.' },
{ unitId: 'sun-qian', reason: '문서와 민심을 정리해 서주 관문 확보 목표에 힘을 보탭니다.' },
{ unitId: 'mi-zhu', reason: '보급과 회복 도구 운용으로 장기전을 안정시킵니다.' }
],
note: '차주의 관문은 길과 강이 겹쳐 전열, 기동, 책사, 보급이 모두 필요합니다. 새로 합류한 손건도 출전 후보로 살펴보십시오.'
}
};
@@ -1426,6 +1447,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 8
}
]
},
{
id: 'liu-sun-after-cao-break',
title: '명분의 문서',
availableAfterBattleIds: [campBattleIds.fourteenth],
unitIds: ['liu-bei', 'sun-qian'],
bondId: 'liu-bei__sun-qian',
rewardExp: 22,
lines: [
'손건: 주공, 차주의 관문은 열렸으나 조조의 눈은 더 날카로워질 것입니다. 이제부터는 말 한마디와 문서 한 장이 모두 칼날입니다.',
'유비: 힘으로만 길을 뚫는다면 백성의 마음을 잃겠지. 그대의 글이 우리 군의 숨길을 열어 주었소.',
'손건: 그렇다면 다음 길목에서는 싸움보다 먼저 명분을 세우겠습니다. 원소에게 가는 길도 그냥 피난이 되어서는 안 됩니다.'
],
choices: [
{
id: 'write-for-people',
label: '백성을 앞세운 격문을 쓴다',
response: '손건은 유비군이 조조를 떠나는 이유를 백성의 안위와 서주의 책임으로 정리해 각 진영에 보낼 문안을 준비했다.',
rewardExp: 10
},
{
id: 'write-for-alliance',
label: '원소에게 보낼 명분을 다듬는다',
response: '손건은 북상하는 길의 위험과 원소 진영에서 필요한 예법을 정리하며 유비가 다음 의탁을 준비하도록 도왔다.',
rewardExp: 9
}
]
},
{
id: 'jian-sun-after-cao-break',
title: '말과 글의 길',
availableAfterBattleIds: [campBattleIds.fourteenth],
unitIds: ['jian-yong', 'sun-qian'],
bondId: 'jian-yong__sun-qian',
rewardExp: 20,
lines: [
'간옹: 손 선생, 글은 곧지만 세상은 구부러져 있소. 너무 반듯하게 쓰면 칼보다 먼저 부러질 때도 있지.',
'손건: 그렇기에 간옹 님의 말이 필요합니다. 글이 문을 두드리면, 말은 그 문을 열게 만들 수 있으니까요.',
'간옹: 좋소. 그럼 나는 웃으며 시간을 벌 테니, 선생은 그 틈에 빠져나갈 길을 문서로 만들구려.'
],
choices: [
{
id: 'soften-message',
label: '문장을 부드럽게 고친다',
response: '간옹은 손건의 문서에 숨 쉴 틈을 더했고, 손건은 그 말투 속에서도 뜻이 흐려지지 않게 문장을 다시 세웠다.',
rewardExp: 9
},
{
id: 'prepare-messenger-route',
label: '사자 동선을 짠다',
response: '두 사람은 관문과 역참을 피해 갈 사자 동선을 나누어 적의 감시망을 늦게 흔들 계획을 세웠다.',
rewardExp: 8
}
]
},
{
id: 'liu-mi-after-cao-break',
title: '떠나는 군량',
availableAfterBattleIds: [campBattleIds.fourteenth],
unitIds: ['liu-bei', 'mi-zhu'],
bondId: 'liu-bei__mi-zhu',
rewardExp: 18,
lines: [
'미축: 주공, 북쪽 길은 멀고 조조군의 추격은 빠릅니다. 군량을 아끼면 병사가 지치고, 풀어 쓰면 다음 진영에 닿기 전 바닥날 수 있습니다.',
'유비: 백성에게서 억지로 거둘 수는 없소. 우리가 떠나는 길 또한 의로워야 하오.',
'미축: 그래서 서주의 상단을 나누어 보내겠습니다. 병사에게는 당장의 밥을, 백성에게는 우리가 돌아올 이유를 남기겠습니다.'
],
choices: [
{
id: 'protect-refugee-supplies',
label: '피난민 몫을 남긴다',
response: '미축은 유비군의 짐을 줄이는 대신 피난민에게 남길 곡식을 따로 묶어, 떠나는 길의 민심을 보존했다.',
rewardExp: 8
},
{
id: 'stock-northern-road',
label: '북상 보급을 숨겨 둔다',
response: '미축은 상단의 표식을 숨긴 짐수레를 북쪽 길에 먼저 보내, 추격을 피해 움직일 여지를 만들었다.',
rewardExp: 8
}
]
}
];
@@ -1504,15 +1606,26 @@ export class CampScene extends Phaser.Scene {
private ensureCurrentCampRecruitment() {
const battleId = this.currentCampBattleId();
if (!this.campaign) {
return;
}
if (battleId === campBattleIds.thirteenth || battleId === campBattleIds.fourteenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (
(battleId !== campBattleIds.seventh &&
battleId !== campBattleIds.eighth &&
battleId !== campBattleIds.ninth &&
battleId !== campBattleIds.tenth &&
battleId !== campBattleIds.eleventh &&
battleId !== campBattleIds.twelfth &&
battleId !== campBattleIds.thirteenth) ||
!this.campaign
battleId !== campBattleIds.twelfth)
) {
return;
}
@@ -1533,6 +1646,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.fourteenth) {
return '서주 재기 후 군영';
}
if (battleId === campBattleIds.thirteenth) {
return '하비 결전 후 군영';
}

View File

@@ -5,6 +5,7 @@ import {
eighthBattleScenario,
eleventhBattleScenario,
fifthBattleScenario,
fourteenthBattleScenario,
fourthBattleScenario,
ninthBattleScenario,
secondBattleScenario,
@@ -325,7 +326,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'tenth-camp' ||
campaign.step === 'eleventh-camp' ||
campaign.step === 'twelfth-camp' ||
campaign.step === 'thirteenth-camp'
campaign.step === 'thirteenth-camp' ||
campaign.step === 'fourteenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -396,6 +398,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'fourteenth-battle') {
this.scene.start('BattleScene', { battleId: fourteenthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -65,7 +65,9 @@ export type CampaignStep =
| 'twelfth-battle'
| 'twelfth-camp'
| 'thirteenth-battle'
| 'thirteenth-camp';
| 'thirteenth-camp'
| 'fourteenth-battle'
| 'fourteenth-camp';
export type CampaignUnitProgressSnapshot = {
unitId: string;
@@ -129,7 +131,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'tenth-battle-xuzhou-breakout': { victory: 'tenth-camp', retry: 'tenth-battle' },
'eleventh-battle-xudu-refuge-road': { victory: 'eleventh-camp', retry: 'eleventh-battle' },
'twelfth-battle-xiapi-outer-siege': { victory: 'twelfth-camp', retry: 'twelfth-battle' },
'thirteenth-battle-xiapi-final': { victory: 'thirteenth-camp', retry: 'thirteenth-battle' }
'thirteenth-battle-xiapi-final': { victory: 'thirteenth-camp', retry: 'thirteenth-battle' },
'fourteenth-battle-cao-break': { victory: 'fourteenth-camp', retry: 'fourteenth-battle' }
};
export type CampaignSaveSlotSummary = {