Add Wolong recruitment chapter

This commit is contained in:
2026-06-23 05:09:14 +09:00
parent c209107b43
commit 64af9c1f68
12 changed files with 1033 additions and 22 deletions

View File

@@ -76,6 +76,15 @@ export const itemCatalog: Record<string, ItemDefinition> = {
attackBonus: 6,
effects: ['단독으로 돌파할 때 피해 보너스']
},
'white-feather-fan': {
id: 'white-feather-fan',
name: '백우선',
slot: 'weapon',
rank: 'treasure',
description: '제갈량이 책략을 펼칠 때 드는 흰 깃털 부채. 전장의 바람과 마음을 읽는다.',
strategyBonus: 5,
effects: ['책략 피해와 공명 지원 경험치 보정']
},
'yellow-turban-saber': {
id: 'yellow-turban-saber',
name: '황건도',

View File

@@ -60,6 +60,10 @@ import {
sixteenthBattleMap,
sixteenthBattleUnits,
sixteenthBattleVictoryPages,
seventeenthBattleBonds,
seventeenthBattleMap,
seventeenthBattleUnits,
seventeenthBattleVictoryPages,
thirdBattleMap,
thirdBattleUnits,
thirdBattleVictoryPages,
@@ -85,7 +89,8 @@ export type BattleScenarioId =
| 'thirteenth-battle-xiapi-final'
| 'fourteenth-battle-cao-break'
| 'fifteenth-battle-yuan-refuge-road'
| 'sixteenth-battle-liu-biao-refuge';
| 'sixteenth-battle-liu-biao-refuge'
| 'seventeenth-battle-wolong-visit-road';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -972,6 +977,59 @@ export const sixteenthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const seventeenthBattleScenario: BattleScenarioDefinition = {
id: 'seventeenth-battle-wolong-visit-road',
title: '융중 방문로',
victoryConditionLabel: '채순 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'형주에서 모은 와룡의 단서를 따라 유비군은 융중으로 향합니다. 그러나 산길에는 채씨 가병이 먼저 길목을 잡고 있습니다.',
'숲과 언덕, 좁은 물길이 많은 전장입니다. 조운의 기동력과 책사들의 후원을 살려 산길의 궁병과 감시병을 끊어 내십시오.',
'채순을 격파하고 융중 초가로 향하는 길을 확보하십시오. 26턴 안에 승리하면 제갈량 영입의 명분과 보급을 더 단단히 챙길 수 있습니다.'
],
map: seventeenthBattleMap,
units: seventeenthBattleUnits,
bonds: seventeenthBattleBonds,
mapTextureKey: 'battle-map-seventeenth',
leaderUnitId: 'wolong-road-leader-cai-xun',
quickVictoryTurnLimit: 26,
baseVictoryGold: 1960,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '채순 격파',
rewardGold: 1260,
unitId: 'wolong-road-leader-cai-xun'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 440,
unitId: 'liu-bei'
},
{
id: 'longzhong-cottage',
kind: 'secure-terrain',
label: '융중 초가 길 확보',
rewardGold: 720,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '26턴 이내 승리',
rewardGold: 560,
maxTurn: 26
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 6', '상처약 4', '탁주 2', '와룡의 서찰 +1'],
victoryPages: seventeenthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -990,7 +1048,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'thirteenth-battle-xiapi-final': thirteenthBattleScenario,
'fourteenth-battle-cao-break': fourteenthBattleScenario,
'fifteenth-battle-yuan-refuge-road': fifteenthBattleScenario,
'sixteenth-battle-liu-biao-refuge': sixteenthBattleScenario
'sixteenth-battle-liu-biao-refuge': sixteenthBattleScenario,
'seventeenth-battle-wolong-visit-road': seventeenthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -12,6 +12,7 @@ import {
seventhBattleScenario,
sixthBattleScenario,
sixteenthBattleScenario,
seventeenthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
twelfthBattleScenario,
@@ -42,6 +43,8 @@ import {
sixthBattleVictoryPages,
sixteenthBattleIntroPages,
sixteenthBattleVictoryPages,
seventeenthBattleIntroPages,
seventeenthBattleVictoryPages,
tenthBattleIntroPages,
tenthBattleVictoryPages,
thirteenthBattleIntroPages,
@@ -221,13 +224,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[sixteenthBattleScenario.id]: {
afterBattleId: sixteenthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '제갈량을 향한 길',
eyebrow: '다음 장',
title: seventeenthBattleScenario.title,
description:
'유표에게 의탁한 유비군은 형주에서 숨을 돌리지만, 손님으로 머무는 것만으로 천하의 길을 열 수는 없습니다. 다음 흐름은 와룡의 이름을 찾아가는 제갈량 영입 장으로 이어집니다.',
rewardHint: '다음 장: 제갈량 영입 준비 중',
pages: sixteenthBattleVictoryPages,
unavailableNotice: '제갈량 영입 장은 다음 작업에서 이어집니다. 군영에서 새로 합류한 조운과 공명도를 다지고 출전 구성을 정비하십시오.'
'유표에게 의탁한 유비군은 형주에서 숨을 돌리지만, 손님으로 머무는 것만으로 천하의 길을 열 수는 없습니다. 형주에서 모은 와룡의 단서를 따라 융중으로 향해야 합니다.',
rewardHint: `예상 보상: ${seventeenthBattleScenario.title} 개방 / 제갈량 영입`,
nextBattleId: seventeenthBattleScenario.id,
campaignStep: 'seventeenth-battle',
pages: [...sixteenthBattleVictoryPages, ...seventeenthBattleIntroPages]
},
[seventeenthBattleScenario.id]: {
afterBattleId: seventeenthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '적벽으로 향하는 바람',
description:
'제갈량이 합류하며 유비군은 비로소 천하의 큰 판을 바라보게 됩니다. 다음 흐름은 형주의 불안과 강동의 바람이 맞물리는 적벽대전 준비로 이어집니다.',
rewardHint: '다음 장: 적벽대전 준비 중',
pages: seventeenthBattleVictoryPages,
unavailableNotice: '적벽대전 장은 다음 작업에서 이어집니다. 군영에서 제갈량과의 공명도를 다지고 출전 구성을 정비하십시오.'
}
};

View File

@@ -1069,6 +1069,70 @@ export const sixteenthBattleVictoryPages: StoryPage[] = [
}
];
export const seventeenthBattleIntroPages: StoryPage[] = [
{
id: 'seventeenth-wolong-clue-gathered',
bgm: 'story-dark',
chapter: '와룡의 단서',
background: 'story-militia',
speaker: '손건',
text: '형주 선비들의 말이 하나로 모입니다. 융중의 초가에 머무는 제갈공명, 사람들은 그를 와룡이라 부릅니다. 주공께서 직접 찾아가 예를 다한다면 길이 열릴지도 모릅니다.'
},
{
id: 'seventeenth-liu-bei-third-visit',
bgm: 'militia-theme',
chapter: '삼고초려',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '한 번 찾아가 만나지 못했다 하여 뜻이 얕아지는 것은 아니오. 천하를 구할 계책을 구하려면, 내가 먼저 몸을 낮추어 그 문 앞에 서야 하오.'
},
{
id: 'seventeenth-road-to-longzhong',
bgm: 'battle-prep',
chapter: '융중 방문로',
background: 'story-three-heroes',
speaker: '조운',
text: '융중으로 향하는 산길에 수상한 가병들이 숨어 있습니다. 주공의 방문이 형주 안의 권신들에게 알려진 듯합니다. 길을 먼저 정리하겠습니다.'
},
{
id: 'seventeenth-longzhong-sortie',
bgm: 'battle-prep',
chapter: '와룡을 찾아',
background: 'story-sortie',
speaker: '장비',
portrait: 'zhangFei',
text: '군사 하나 만나러 가는데 길목마다 잔챙이들이 성가시게 하는군요. 그래도 형님 뜻이라면 길부터 시원하게 열어 보겠습니다.'
}
];
export const seventeenthBattleVictoryPages: StoryPage[] = [
{
id: 'seventeenth-victory-longzhong-road',
bgm: 'militia-theme',
chapter: '열린 초가 길',
background: 'story-sortie',
text: '융중으로 향하는 길목의 가병들이 흩어지고, 산길은 조용해졌다. 유비는 갑옷의 먼지를 털지도 않은 채 제갈량의 초가 앞에 다시 섰다.'
},
{
id: 'seventeenth-zhuge-liang-counsel',
bgm: 'story-dark',
chapter: '천하삼분의 말',
background: 'story-liu-bei',
speaker: '제갈량',
text: '한실을 일으키려면 먼저 형주와 익주를 얻어 근본을 세우고, 때를 기다려 북벌의 길을 여십시오. 주공의 뜻이 백성에게 닿는다면, 저는 그 길에 지혜를 보태겠습니다.'
},
{
id: 'seventeenth-zhuge-liang-joins',
bgm: 'battle-prep',
chapter: '와룡 출려',
background: 'story-three-heroes',
speaker: '유비',
portrait: 'liuBei',
text: '공명 선생이 함께한다면 우리 군은 비로소 길을 보는 눈을 얻은 것이오. 이제 형주의 머무름은 다음 큰 전장을 준비하는 시간이 될 것이오.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1310,6 +1374,12 @@ export const sixteenthBattleMap: BattleMap = {
terrain: createSixteenthBattleTerrain()
};
export const seventeenthBattleMap: BattleMap = {
width: 34,
height: 26,
terrain: createSeventeenthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -3242,6 +3312,30 @@ export const liuBiaoRecruitUnits: UnitData[] = [
}
];
export const zhugeRecruitUnits: UnitData[] = [
{
id: 'zhuge-liang',
name: '제갈량',
faction: 'ally',
className: '군사',
classKey: 'strategist',
level: 8,
exp: 0,
hp: 28,
maxHp: 28,
attack: 8,
move: 4,
stats: { might: 34, intelligence: 100, leadership: 88, agility: 74, luck: 86 },
equipment: {
weapon: { itemId: 'white-feather-fan', level: 1, exp: 0 },
armor: { itemId: 'oath-robe', level: 1, exp: 0 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 7,
y: 20
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -5937,6 +6031,340 @@ export const sixteenthBattleUnits: UnitData[] = [
}
];
const seventeenthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 3, y: 21 },
'guan-yu': { x: 4, y: 20 },
'zhang-fei': { x: 4, y: 22 },
'jian-yong': { x: 5, y: 21 },
'mi-zhu': { x: 3, y: 23 },
'sun-qian': { x: 5, y: 23 },
'zhao-yun': { x: 6, y: 22 }
};
export const seventeenthBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits
].map((unit) => placeScenarioUnit(unit, seventeenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
{
id: 'wolong-road-scout-a',
name: '산길 척후',
faction: 'enemy',
className: '형주 척후',
classKey: 'bandit',
level: 18,
exp: 16,
hp: 58,
maxHp: 58,
attack: 22,
move: 4,
stats: { might: 94, intelligence: 66, leadership: 72, agility: 92, luck: 64 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 82 },
armor: { itemId: 'rebel-vest', level: 2, exp: 38 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 10,
y: 20
},
{
id: 'wolong-road-scout-b',
name: '산길 척후',
faction: 'enemy',
className: '형주 척후',
classKey: 'bandit',
level: 18,
exp: 16,
hp: 58,
maxHp: 58,
attack: 22,
move: 4,
stats: { might: 94, intelligence: 66, leadership: 72, agility: 92, luck: 64 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 82 },
armor: { itemId: 'rebel-vest', level: 2, exp: 38 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 12,
y: 23
},
{
id: 'wolong-road-infantry-a',
name: '형주 가병',
faction: 'enemy',
className: '산길 보병',
classKey: 'yellowTurban',
level: 18,
exp: 18,
hp: 70,
maxHp: 70,
attack: 24,
move: 3,
stats: { might: 100, intelligence: 68, leadership: 86, agility: 74, luck: 64 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 86 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 42 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 15,
y: 18
},
{
id: 'wolong-road-infantry-b',
name: '형주 가병',
faction: 'enemy',
className: '산길 보병',
classKey: 'yellowTurban',
level: 18,
exp: 18,
hp: 70,
maxHp: 70,
attack: 24,
move: 3,
stats: { might: 100, intelligence: 68, leadership: 86, agility: 74, luck: 64 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 86 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 42 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 17,
y: 20
},
{
id: 'wolong-road-infantry-c',
name: '형주 가병',
faction: 'enemy',
className: '초가 수비병',
classKey: 'yellowTurban',
level: 19,
exp: 20,
hp: 74,
maxHp: 74,
attack: 25,
move: 3,
stats: { might: 102, intelligence: 70, leadership: 88, agility: 74, luck: 65 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 90 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 46 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 26,
y: 11
},
{
id: 'wolong-road-infantry-d',
name: '형주 가병',
faction: 'enemy',
className: '초가 수비병',
classKey: 'yellowTurban',
level: 19,
exp: 20,
hp: 74,
maxHp: 74,
attack: 25,
move: 3,
stats: { might: 102, intelligence: 70, leadership: 88, agility: 74, luck: 65 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 90 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 46 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 27,
y: 15
},
{
id: 'wolong-road-archer-a',
name: '형주 궁병',
faction: 'enemy',
className: '숲길 궁병',
classKey: 'archer',
level: 18,
exp: 18,
hp: 56,
maxHp: 56,
attack: 23,
move: 3,
stats: { might: 88, intelligence: 80, leadership: 78, agility: 88, luck: 65 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 84 },
armor: { itemId: 'cloth-armor', level: 2, exp: 40 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 16,
y: 14
},
{
id: 'wolong-road-archer-b',
name: '형주 궁병',
faction: 'enemy',
className: '숲길 궁병',
classKey: 'archer',
level: 18,
exp: 18,
hp: 56,
maxHp: 56,
attack: 23,
move: 3,
stats: { might: 88, intelligence: 80, leadership: 78, agility: 88, luck: 65 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 84 },
armor: { itemId: 'cloth-armor', level: 2, exp: 40 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 22,
y: 10
},
{
id: 'wolong-road-archer-c',
name: '형주 궁병',
faction: 'enemy',
className: '초가 궁병',
classKey: 'archer',
level: 19,
exp: 20,
hp: 58,
maxHp: 58,
attack: 24,
move: 3,
stats: { might: 90, intelligence: 82, leadership: 80, agility: 88, luck: 66 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 88 },
armor: { itemId: 'cloth-armor', level: 2, exp: 42 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 29,
y: 13
},
{
id: 'wolong-road-cavalry-a',
name: '형주 기병',
faction: 'enemy',
className: '산길 기병',
classKey: 'cavalry',
level: 19,
exp: 20,
hp: 76,
maxHp: 76,
attack: 26,
move: 5,
stats: { might: 104, intelligence: 64, leadership: 82, agility: 96, luck: 66 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 90 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 44 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 13,
y: 19
},
{
id: 'wolong-road-cavalry-b',
name: '형주 기병',
faction: 'enemy',
className: '산길 기병',
classKey: 'cavalry',
level: 19,
exp: 20,
hp: 76,
maxHp: 76,
attack: 26,
move: 5,
stats: { might: 104, intelligence: 64, leadership: 82, agility: 96, luck: 66 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 90 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 44 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 20,
y: 18
},
{
id: 'wolong-road-guard-a',
name: '채씨 가병',
faction: 'enemy',
className: '초가 감시병',
classKey: 'yellowTurban',
level: 19,
exp: 22,
hp: 78,
maxHp: 78,
attack: 25,
move: 3,
stats: { might: 102, intelligence: 74, leadership: 90, agility: 75, luck: 65 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 92 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 48 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 30,
y: 14
},
{
id: 'wolong-road-guard-b',
name: '채씨 가병',
faction: 'enemy',
className: '초가 감시병',
classKey: 'yellowTurban',
level: 19,
exp: 22,
hp: 78,
maxHp: 78,
attack: 25,
move: 3,
stats: { might: 102, intelligence: 74, leadership: 90, agility: 75, luck: 65 },
equipment: {
weapon: { itemId: 'iron-spear', level: 2, exp: 92 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 48 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 31,
y: 12
},
{
id: 'wolong-road-strategist-a',
name: '형주 문객',
faction: 'enemy',
className: '채씨 문객',
classKey: 'archer',
level: 19,
exp: 22,
hp: 60,
maxHp: 60,
attack: 24,
move: 3,
stats: { might: 74, intelligence: 92, leadership: 82, agility: 80, luck: 68 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 90 },
armor: { itemId: 'cloth-armor', level: 2, exp: 44 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 28,
y: 12
},
{
id: 'wolong-road-leader-cai-xun',
name: '채순',
faction: 'enemy',
className: '채씨 가병장',
classKey: 'rebelLeader',
level: 20,
exp: 24,
hp: 104,
maxHp: 104,
attack: 28,
move: 4,
stats: { might: 108, intelligence: 76, leadership: 98, agility: 82, luck: 66 },
equipment: {
weapon: { itemId: 'leader-axe', level: 3, exp: 32 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 52 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 31,
y: 14
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -6021,6 +6449,33 @@ export const liuBiaoRecruitBonds: BattleBond[] = [
}
];
export const zhugeRecruitBonds: BattleBond[] = [
{
id: 'liu-bei__zhuge-liang',
unitIds: ['liu-bei', 'zhuge-liang'],
title: '군신의 만남',
level: 58,
exp: 0,
description: '유비의 뜻과 제갈량의 계책이 만나 천하를 향한 큰 그림을 열기 시작합니다.'
},
{
id: 'zhao-yun__zhuge-liang',
unitIds: ['zhao-yun', 'zhuge-liang'],
title: '창과 책략',
level: 36,
exp: 0,
description: '조운의 기동력과 제갈량의 판단은 빠른 구원과 정확한 돌파를 함께 만듭니다.'
},
{
id: 'guan-yu__zhuge-liang',
unitIds: ['guan-yu', 'zhuge-liang'],
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);
@@ -6040,6 +6495,7 @@ export const fifteenthBattleBonds: BattleBond[] = fourteenthBattleBonds.map(clon
export const sixteenthBattleBonds: BattleBond[] = [...fifteenthBattleBonds, ...liuBiaoRecruitBonds].map(
cloneBattleBondForScenario
);
export const seventeenthBattleBonds: BattleBond[] = sixteenthBattleBonds.map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -6448,6 +6904,55 @@ function createSixteenthBattleTerrain(): TerrainType[][] {
);
}
function createSeventeenthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 26 }, (_, y) =>
Array.from({ length: 34 }, (_, x): TerrainType => {
if ((x <= 2 && y >= 20 && y <= 24) || (x === 3 && y >= 22 && y <= 24)) {
return 'camp';
}
if ((x >= 29 && x <= 33 && y >= 10 && y <= 15) || (x >= 27 && x <= 30 && y >= 8 && y <= 10)) {
return 'fort';
}
if ((x >= 24 && x <= 26 && y >= 11 && y <= 13) || (x >= 12 && x <= 14 && y >= 18 && y <= 20)) {
return 'village';
}
if ((x === 18 || x === 19) && y >= 4 && y <= 23) {
return 'river';
}
if (
(y === 21 && x >= 2 && x <= 15) ||
(y === 18 && x >= 13 && x <= 24) ||
(y === 13 && x >= 22 && x <= 32) ||
(x === 29 && y >= 10 && y <= 18)
) {
return 'road';
}
if ((x >= 5 && x <= 15 && y === 25 - x) || (x >= 15 && x <= 29 && y === x - 11)) {
return 'road';
}
if (
(x <= 10 && y <= 9) ||
(x >= 4 && x <= 15 && y >= 10 && y <= 17) ||
(x >= 20 && x <= 28 && y >= 16 && y <= 24) ||
(x >= 25 && y <= 7)
) {
return 'forest';
}
if (
(x >= 8 && x <= 15 && y >= 2 && y <= 7) ||
(x >= 15 && x <= 23 && y >= 20 && y <= 25) ||
(x >= 27 && x <= 33 && y >= 1 && y <= 6)
) {
return 'hill';
}
if ((x >= 32 && y <= 9) || (x >= 32 && y >= 17) || (x <= 1 && y <= 6)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -28,6 +28,7 @@ const unitTexture: Record<string, string> = {
'guan-yu': 'unit-guan-yu',
'zhang-fei': 'unit-zhang-fei',
'zhao-yun': 'unit-rebel-cavalry',
'zhuge-liang': 'unit-liu-bei',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer',
@@ -139,7 +140,22 @@ const unitTexture: Record<string, string> = {
'liu-biao-road-guard-a': 'unit-rebel',
'liu-biao-road-guard-b': 'unit-rebel',
'liu-biao-road-strategist-a': 'unit-rebel-archer',
'liu-biao-road-leader-cao-ren': 'unit-rebel-leader'
'liu-biao-road-leader-cao-ren': 'unit-rebel-leader',
'wolong-road-scout-a': 'unit-rebel',
'wolong-road-scout-b': 'unit-rebel',
'wolong-road-infantry-a': 'unit-rebel',
'wolong-road-infantry-b': 'unit-rebel',
'wolong-road-infantry-c': 'unit-rebel',
'wolong-road-infantry-d': 'unit-rebel',
'wolong-road-archer-a': 'unit-rebel-archer',
'wolong-road-archer-b': 'unit-rebel-archer',
'wolong-road-archer-c': 'unit-rebel-archer',
'wolong-road-cavalry-a': 'unit-rebel-cavalry',
'wolong-road-cavalry-b': 'unit-rebel-cavalry',
'wolong-road-guard-a': 'unit-rebel',
'wolong-road-guard-b': 'unit-rebel',
'wolong-road-strategist-a': 'unit-rebel-archer',
'wolong-road-leader-cai-xun': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -553,7 +569,9 @@ const unitStrategyIds: Record<string, string[]> = {
'guan-yu': ['fireTactic'],
'zhang-fei': ['roar'],
'jian-yong': ['aid', 'encourage', 'fireTactic'],
'mi-zhu': ['aid', 'encourage']
'mi-zhu': ['aid', 'encourage'],
'sun-qian': ['aid', 'encourage', 'fireTactic'],
'zhuge-liang': ['aid', 'encourage', 'fireTactic']
};
const initialItemStocks: Record<string, Record<string, number>> = {
@@ -563,7 +581,8 @@ const initialItemStocks: Record<string, Record<string, number>> = {
'jian-yong': { bean: 1 },
'mi-zhu': { bean: 2, salve: 1 },
'sun-qian': { bean: 1, salve: 1 },
'zhao-yun': { bean: 1, wine: 1 }
'zhao-yun': { bean: 1, wine: 1 },
'zhuge-liang': { bean: 1, wine: 1 }
};
const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
@@ -727,7 +746,22 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'liu-biao-road-guard-a': 'guard',
'liu-biao-road-guard-b': 'guard',
'liu-biao-road-strategist-a': 'hold',
'liu-biao-road-leader-cao-ren': 'guard'
'liu-biao-road-leader-cao-ren': 'guard',
'wolong-road-scout-a': 'aggressive',
'wolong-road-scout-b': 'aggressive',
'wolong-road-infantry-a': 'guard',
'wolong-road-infantry-b': 'guard',
'wolong-road-infantry-c': 'guard',
'wolong-road-infantry-d': 'guard',
'wolong-road-archer-a': 'hold',
'wolong-road-archer-b': 'hold',
'wolong-road-archer-c': 'hold',
'wolong-road-cavalry-a': 'aggressive',
'wolong-road-cavalry-b': 'aggressive',
'wolong-road-guard-a': 'guard',
'wolong-road-guard-b': 'guard',
'wolong-road-strategist-a': 'hold',
'wolong-road-leader-cai-xun': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -11,6 +11,7 @@ import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg';
import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
@@ -90,6 +91,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl);
this.load.image('battle-map-fifteenth', fifteenthBattleMapUrl);
this.load.image('battle-map-sixteenth', sixteenthBattleMapUrl);
this.load.image('battle-map-seventeenth', seventeenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
@@ -132,6 +134,7 @@ export class BootScene extends Phaser.Scene {
this.createItemIcon('item-green-dragon-glaive', (graphics) => this.drawPolearmIcon(graphics, 0x5fbf8f, 0xd8b15f));
this.createItemIcon('item-serpent-spear', (graphics) => this.drawSpearIcon(graphics, 0xe0e8ef, 0xb86b55));
this.createItemIcon('item-sky-piercer-halberd', (graphics) => this.drawPolearmIcon(graphics, 0xd8dfe6, 0xd95f4f));
this.createItemIcon('item-white-feather-fan', (graphics) => this.drawFanIcon(graphics, 0xf3f0df, 0x6e8eb8));
this.createItemIcon('item-yellow-turban-saber', (graphics) => this.drawSaberIcon(graphics, 0xd8b15f, 0x9c6b2f));
this.createItemIcon('item-short-bow', (graphics) => this.drawBowIcon(graphics, 0xc58a4c, 0xe8dfca));
this.createItemIcon('item-leader-axe', (graphics) => this.drawAxeIcon(graphics, 0xd8dfe6, 0x8d5a3a));
@@ -258,6 +261,30 @@ export class BootScene extends Phaser.Scene {
graphics.fillTriangle(16, 5, 9, 9, 15, 14);
}
private drawFanIcon(graphics: Phaser.GameObjects.Graphics, featherColor: number, ribColor: number) {
graphics.lineStyle(2, ribColor, 1);
graphics.beginPath();
graphics.moveTo(16, 25);
graphics.lineTo(5, 8);
graphics.moveTo(16, 25);
graphics.lineTo(10, 4);
graphics.moveTo(16, 25);
graphics.lineTo(16, 3);
graphics.moveTo(16, 25);
graphics.lineTo(22, 4);
graphics.moveTo(16, 25);
graphics.lineTo(27, 8);
graphics.strokePath();
graphics.fillStyle(featherColor, 0.96);
graphics.fillEllipse(5, 9, 7, 15);
graphics.fillEllipse(10, 6, 7, 17);
graphics.fillEllipse(16, 5, 8, 18);
graphics.fillEllipse(22, 6, 7, 17);
graphics.fillEllipse(27, 9, 7, 15);
graphics.fillStyle(0xd8b15f, 1);
graphics.fillCircle(16, 25, 3);
}
private drawArmorIcon(graphics: Phaser.GameObjects.Graphics, baseColor: number, accentColor: number) {
graphics.fillStyle(baseColor, 1);
graphics.fillTriangle(14, 4, 24, 11, 20, 24);

View File

@@ -13,6 +13,8 @@ import {
liuBiaoRecruitUnits,
xuzhouRecruitBonds,
xuzhouRecruitUnits,
zhugeRecruitBonds,
zhugeRecruitUnits,
type PortraitKey,
type UnitData
} from '../data/scenario';
@@ -218,7 +220,8 @@ const campBattleIds = {
thirteenth: 'thirteenth-battle-xiapi-final',
fourteenth: 'fourteenth-battle-cao-break',
fifteenth: 'fifteenth-battle-yuan-refuge-road',
sixteenth: 'sixteenth-battle-liu-biao-refuge'
sixteenth: 'sixteenth-battle-liu-biao-refuge',
seventeenth: 'seventeenth-battle-wolong-visit-road'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -385,6 +388,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'mi-zhu', reason: '큰 맵의 장기전을 대비한 보급과 회복 운용에 유리합니다.' }
],
note: '유표 의탁로부터는 합류 무장이 출전 제한보다 많아집니다. 조운의 기동력을 시험할지, 책사와 보급을 두껍게 둘지 직접 고르십시오.'
},
'seventeenth-battle-wolong-visit-road': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '와룡을 직접 찾아가는 주장입니다.' },
{ unitId: 'zhao-yun', reason: '산길 척후와 기병을 빠르게 끊는 기동 역할입니다.' },
{ unitId: 'guan-yu', reason: '초가 앞 수비병을 정면에서 밀어낼 전열 핵심입니다.' },
{ unitId: 'zhang-fei', reason: '좁은 길목을 뚫고 채순을 압박하는 돌파 역할입니다.' },
{ unitId: 'sun-qian', reason: '와룡 방문의 예와 명분을 정리하는 책사 역할입니다.' },
{ unitId: 'mi-zhu', reason: '산길 장기전에 필요한 보급과 회복 운용을 맡습니다.' }
],
note: '와룡 단서를 모은 뒤 융중으로 향합니다. 출전 제한보다 무장이 많으니 조운의 기동, 책사의 지원, 보급 중 무엇을 중시할지 선택하십시오.'
}
};
@@ -1738,6 +1753,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 9
}
]
},
{
id: 'liu-zhuge-after-wolong-visit',
title: '초가에서 열린 큰 그림',
availableAfterBattleIds: [campBattleIds.seventeenth],
unitIds: ['liu-bei', 'zhuge-liang'],
bondId: 'liu-bei__zhuge-liang',
rewardExp: 28,
lines: [
'제갈량: 주공께서 세 번이나 초가를 찾으신 것은 한 사람을 얻기 위함이 아니라, 백성을 살릴 길을 묻기 위함이라 보았습니다.',
'유비: 나는 아직 작은 군영 하나도 온전히 지키기 어려운 몸이오. 그러나 선생의 말은 우리 군이 가야 할 길을 눈앞에 펼쳤소.',
'제갈량: 그 길은 서두르면 흐트러지고, 늦추면 빼앗깁니다. 이제부터는 뜻과 때를 함께 살피겠습니다.'
],
choices: [
{
id: 'share-three-kingdoms-plan',
label: '천하삼분의 뜻을 묻는다',
response: '제갈량은 형주와 익주, 그리고 북벌의 때를 차분히 설명했고, 유비는 그 말을 군영의 첫 방침으로 삼았다.',
rewardExp: 12
},
{
id: 'entrust-camp-strategy',
label: '군영의 계책을 맡긴다',
response: '유비는 제갈량에게 군영의 큰 판단을 맡겼고, 공명은 병사와 장수의 이름부터 조용히 살피기 시작했다.',
rewardExp: 10
}
]
},
{
id: 'zhao-zhuge-after-wolong-visit',
title: '말발굽과 부채',
availableAfterBattleIds: [campBattleIds.seventeenth],
unitIds: ['zhao-yun', 'zhuge-liang'],
bondId: 'zhao-yun__zhuge-liang',
rewardExp: 22,
lines: [
'조운: 선생의 계책은 멀리 보되, 실제 길목의 먼지까지 살피는군요. 제 말이 달릴 곳을 미리 본 듯했습니다.',
'제갈량: 자룡의 창은 빠르지만 앞서 나가도 군의 마음을 놓치지 않습니다. 그래서 빠른 구원이 가능해집니다.',
'조운: 그렇다면 다음 전장에서는 선생의 지시에 맞춰, 가장 필요한 곳에 먼저 닿겠습니다.'
],
choices: [
{
id: 'map-relief-routes',
label: '구원로를 함께 살핀다',
response: '제갈량은 지형 위에 구원로를 표시했고, 조운은 말이 지나갈 수 있는 폭과 위험한 샛길을 덧붙였다.',
rewardExp: 10
},
{
id: 'coordinate-feint-charge',
label: '유인 돌격을 맞춘다',
response: '조운은 일부러 빈틈을 보이는 돌격법을 익혔고, 제갈량은 그 틈에 적을 몰아넣는 순서를 정했다.',
rewardExp: 9
}
]
},
{
id: 'guan-zhuge-after-wolong-visit',
title: '의와 계책의 첫 인사',
availableAfterBattleIds: [campBattleIds.seventeenth],
unitIds: ['guan-yu', 'zhuge-liang'],
bondId: 'guan-yu__zhuge-liang',
rewardExp: 20,
lines: [
'관우: 선생의 말은 크고 깊으나, 전장에서는 한 걸음의 늦음도 군을 잃게 합니다. 그 점을 잊지 않으셨으면 하오.',
'제갈량: 운장께서 전열을 지키는 까닭도 결국 주공의 뜻을 잃지 않기 위함입니다. 저는 그 의를 가벼이 여기지 않겠습니다.',
'관우: 그렇다면 나 또한 선생의 계책을 먼저 듣겠소. 의가 길을 잃지 않게 해 주시오.'
],
choices: [
{
id: 'align-duty-and-plan',
label: '의와 계책을 맞춘다',
response: '관우는 제갈량의 병력 운용을 듣고 전열의 기준을 새로 세웠고, 제갈량은 관우가 지켜야 할 선을 존중했다.',
rewardExp: 9
},
{
id: 'promise-clear-orders',
label: '명령의 기준을 분명히 한다',
response: '제갈량은 전장 명령의 우선순위를 문서로 정리했고, 관우는 그 분명함을 보고 조용히 고개를 끄덕였다.',
rewardExp: 8
}
]
}
];
@@ -1829,6 +1925,65 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['탁주 1']
}
]
},
{
id: 'longzhong-cottage-records',
title: '융중 초가 정리',
location: '융중 초가',
availableAfterBattleIds: [campBattleIds.seventeenth],
bondId: 'liu-bei__zhuge-liang',
description: '제갈량과 함께 초가의 지도와 서책을 정리하며 다음 큰 흐름을 준비합니다.',
lines: [
'제갈량: 이 지도들은 아직 군영의 전장 지도가 아니라 천하의 형세입니다. 그러나 전장은 늘 여기서 시작됩니다.',
'유비: 우리 군이 지금 가진 것은 많지 않소. 그래서 더더욱 무엇을 먼저 지킬지 알아야 하오.',
'초가의 낮은 책상 위에는 형주와 강동, 북방의 길이 하나씩 펼쳐집니다.'
],
choices: [
{
id: 'study-red-cliff-winds',
label: '강동의 바람을 살핀다',
response: '제갈량은 강동과 조조군의 충돌 가능성을 짚었고, 유비군은 다음 큰 전장의 이름을 조심스럽게 떠올렸다.',
bondExp: 24,
itemRewards: ['강동 풍문 1']
},
{
id: 'organize-wolong-scrolls',
label: '와룡의 서책을 정리한다',
response: '손상되기 쉬운 서책을 군영으로 옮기며 제갈량의 계책을 병사들도 이해할 수 있는 말로 옮기기 시작했다.',
bondExp: 20,
itemRewards: ['와룡의 서찰 1']
}
]
},
{
id: 'xinye-merchant-after-wolong',
title: '신야 상인 접견',
location: '신야 장터',
availableAfterBattleIds: [campBattleIds.seventeenth],
bondId: 'liu-bei__mi-zhu',
description: '제갈량 합류 소문을 듣고 찾아온 상인들과 다음 전투를 위한 보급 조건을 조율합니다.',
lines: [
'미축: 와룡이 주공의 군영에 들었다는 말이 퍼지자 상인들의 셈도 달라졌습니다. 지금 보급선을 고정할 기회입니다.',
'유비: 값이 싸다고 무리하게 사들이지는 마시오. 백성의 삶을 해치지 않는 선에서 군을 세워야 하오.',
'상인들은 유비군의 앞날에 걸지, 당장의 이익만 챙길지 서로 눈치를 봅니다.'
],
choices: [
{
id: 'secure-medicine-contract',
label: '약재 계약을 맺는다',
response: '미축은 오래 보관할 수 있는 약재 계약을 맺었고, 군영의 치료 물자가 안정되었다.',
bondExp: 14,
itemRewards: ['상처약 2']
},
{
id: 'preserve-war-funds',
label: '군자금을 아낀다',
response: '미축은 급하지 않은 거래를 미루고 군자금을 보존했다. 병사들은 검소한 군영의 기준을 이해했다.',
bondExp: 12,
gold: 160,
itemRewards: ['콩 2']
}
]
}
];
@@ -1914,6 +2069,15 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.seventeenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits, ...zhugeRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds, ...zhugeRecruitBonds]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (battleId === campBattleIds.fifteenth || battleId === campBattleIds.sixteenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits],
@@ -1962,6 +2126,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.seventeenth) {
return '와룡 출려 후 군영';
}
if (battleId === campBattleIds.sixteenth) {
return '형주 의탁 후 군영';
}
@@ -2701,6 +2868,14 @@ export class CampScene extends Phaser.Scene {
return;
}
if (flow.nextBattleId === campBattleIds.seventeenth && this.wolongClueCount() <= 0) {
this.hideSortiePrep();
this.activeTab = 'visit';
this.render();
this.showCampNotice('와룡의 단서를 먼저 모으십시오. 방문 탭에서 형주 선비들의 말을 들어 보세요.');
return;
}
markCampaignStep(flow.campaignStep);
this.scene.start('StoryScene', {
pages: flow.pages,
@@ -3058,7 +3233,7 @@ export class CampScene extends Phaser.Scene {
bg.setStrokeStyle(1, palette.blue, 0.42);
this.track(this.add.text(x + 14, y + 12, '현지 준비', this.textStyle(17, '#f2e3bf', true)));
this.track(this.add.text(x + 14, y + 40, `방문 ${completed.length}/${visits.length} 완료`, this.textStyle(13, completed.length >= visits.length && visits.length > 0 ? '#a8ffd0' : '#d4dce6', true)));
const insightCount = this.inventoryAmount('와룡 소문') + this.inventoryAmount('민심 기록');
const insightCount = this.wolongClueCount();
this.track(this.add.text(x + 14, y + 66, `제갈량 단서 ${insightCount} · 군자금 ${this.campaign?.gold ?? 0}`, this.textStyle(13, '#9fb0bf', true)));
}
@@ -3586,6 +3761,10 @@ export class CampScene extends Phaser.Scene {
return this.campaign?.inventory[label] ?? 0;
}
private wolongClueCount() {
return this.inventoryAmount('와룡 소문') + this.inventoryAmount('민심 기록');
}
private drawBar(x: number, y: number, width: number, height: number, ratio: number, color: number) {
const track = this.track(this.add.rectangle(x, y, width, height, 0x070b10, 0.86));
track.setOrigin(0);

View File

@@ -13,6 +13,7 @@ import {
seventhBattleScenario,
sixthBattleScenario,
sixteenthBattleScenario,
seventeenthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
twelfthBattleScenario,
@@ -331,7 +332,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'thirteenth-camp' ||
campaign.step === 'fourteenth-camp' ||
campaign.step === 'fifteenth-camp' ||
campaign.step === 'sixteenth-camp'
campaign.step === 'sixteenth-camp' ||
campaign.step === 'seventeenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -417,6 +419,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'seventeenth-battle') {
this.scene.start('BattleScene', { battleId: seventeenthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -72,7 +72,9 @@ export type CampaignStep =
| 'fifteenth-battle'
| 'fifteenth-camp'
| 'sixteenth-battle'
| 'sixteenth-camp';
| 'sixteenth-camp'
| 'seventeenth-battle'
| 'seventeenth-camp';
export type CampaignUnitProgressSnapshot = {
unitId: string;
@@ -140,7 +142,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'thirteenth-battle-xiapi-final': { victory: 'thirteenth-camp', retry: 'thirteenth-battle' },
'fourteenth-battle-cao-break': { victory: 'fourteenth-camp', retry: 'fourteenth-battle' },
'fifteenth-battle-yuan-refuge-road': { victory: 'fifteenth-camp', retry: 'fifteenth-battle' },
'sixteenth-battle-liu-biao-refuge': { victory: 'sixteenth-camp', retry: 'sixteenth-battle' }
'sixteenth-battle-liu-biao-refuge': { victory: 'sixteenth-camp', retry: 'sixteenth-battle' },
'seventeenth-battle-wolong-visit-road': { victory: 'seventeenth-camp', retry: 'seventeenth-battle' }
};
export type CampaignSaveSlotSummary = {