Add Luo Castle main gate chapter
This commit is contained in:
@@ -43,6 +43,10 @@ import {
|
||||
tenthBattleMap,
|
||||
tenthBattleUnits,
|
||||
tenthBattleVictoryPages,
|
||||
thirtyFirstBattleBonds,
|
||||
thirtyFirstBattleMap,
|
||||
thirtyFirstBattleUnits,
|
||||
thirtyFirstBattleVictoryPages,
|
||||
thirteenthBattleBonds,
|
||||
thirteenthBattleMap,
|
||||
thirteenthBattleUnits,
|
||||
@@ -155,7 +159,8 @@ export type BattleScenarioId =
|
||||
| 'twenty-seventh-battle-yizhou-relief-road'
|
||||
| 'twenty-eighth-battle-fu-pass-entry'
|
||||
| 'twenty-ninth-battle-luo-outer-wall'
|
||||
| 'thirtieth-battle-luofeng-ambush';
|
||||
| 'thirtieth-battle-luofeng-ambush'
|
||||
| 'thirty-first-battle-luo-main-gate';
|
||||
|
||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||
|
||||
@@ -1905,6 +1910,69 @@ export const thirtiethBattleScenario: BattleScenarioDefinition = {
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const thirtyFirstBattleScenario: BattleScenarioDefinition = {
|
||||
id: 'thirty-first-battle-luo-main-gate',
|
||||
title: '낙성 본성 공략',
|
||||
victoryConditionLabel: '장임 본성 격파',
|
||||
defeatConditionLabel: '유비 또는 방통 퇴각',
|
||||
openingObjectiveLines: [
|
||||
'낙성 본성은 높은 성벽과 좁은 성문으로 버티고 있습니다. 성문을 무너뜨리되 백성 피해를 줄여야 다음 익주 진입이 쉬워집니다.',
|
||||
'엄안이 성 안의 노장으로 버티고 있습니다. 황충, 법정, 오의를 데려가면 성벽 궁병과 항복 설득을 함께 압박하기 좋습니다.',
|
||||
'유비와 방통을 지키며 장임을 격파하십시오. 낙성이 열리면 익주 심부와 성도 압박으로 나아갑니다.'
|
||||
],
|
||||
map: thirtyFirstBattleMap,
|
||||
units: thirtyFirstBattleUnits,
|
||||
bonds: thirtyFirstBattleBonds,
|
||||
mapTextureKey: 'battle-map-thirty-first',
|
||||
leaderUnitId: 'luo-main-leader-zhang-ren',
|
||||
quickVictoryTurnLimit: 42,
|
||||
baseVictoryGold: 4720,
|
||||
objectives: [
|
||||
{
|
||||
id: 'leader',
|
||||
kind: 'defeat-leader',
|
||||
label: '장임 본성 격파',
|
||||
rewardGold: 2920,
|
||||
unitId: 'luo-main-leader-zhang-ren'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '유비 생존',
|
||||
rewardGold: 900,
|
||||
unitId: 'liu-bei'
|
||||
},
|
||||
{
|
||||
id: 'pang-tong',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '방통 생존',
|
||||
rewardGold: 1220,
|
||||
unitId: 'pang-tong'
|
||||
},
|
||||
{
|
||||
id: 'main-gate',
|
||||
kind: 'secure-terrain',
|
||||
label: '낙성 성문 확보',
|
||||
rewardGold: 1480,
|
||||
terrain: 'fort'
|
||||
},
|
||||
{
|
||||
id: 'quick',
|
||||
kind: 'quick-victory',
|
||||
label: '42턴 이내 승리',
|
||||
rewardGold: 1120,
|
||||
maxTurn: 42
|
||||
}
|
||||
],
|
||||
defeatConditions: [
|
||||
{ kind: 'unit-defeated', unitId: 'liu-bei' },
|
||||
{ kind: 'unit-defeated', unitId: 'pang-tong' }
|
||||
],
|
||||
itemRewards: ['콩 16', '상처약 12', '탁주 7', '낙성 항복 권고문 +1', '엄안 합류'],
|
||||
victoryPages: thirtyFirstBattleVictoryPages,
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
||||
|
||||
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
||||
@@ -1937,7 +2005,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
|
||||
'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario,
|
||||
'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario,
|
||||
'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario,
|
||||
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario
|
||||
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario,
|
||||
'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario
|
||||
};
|
||||
|
||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
seventeenthBattleScenario,
|
||||
tenthBattleScenario,
|
||||
thirteenthBattleScenario,
|
||||
thirtyFirstBattleScenario,
|
||||
thirtiethBattleScenario,
|
||||
twentyEighthBattleScenario,
|
||||
twentyNinthBattleScenario,
|
||||
@@ -66,6 +67,8 @@ import {
|
||||
tenthBattleVictoryPages,
|
||||
thirteenthBattleIntroPages,
|
||||
thirteenthBattleVictoryPages,
|
||||
thirtyFirstBattleIntroPages,
|
||||
thirtyFirstBattleVictoryPages,
|
||||
thirtiethBattleIntroPages,
|
||||
thirtiethBattleVictoryPages,
|
||||
twentyEighthBattleIntroPages,
|
||||
@@ -417,13 +420,24 @@ const sortieFlows: Record<string, SortieFlow> = {
|
||||
},
|
||||
[thirtiethBattleScenario.id]: {
|
||||
afterBattleId: thirtiethBattleScenario.id,
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '낙성 본성 공략 준비',
|
||||
eyebrow: '다음 전장',
|
||||
title: thirtyFirstBattleScenario.title,
|
||||
description:
|
||||
'낙봉파의 매복을 무너뜨리고 방통을 살려 냈습니다. 장임은 낙성 본성으로 물러났고, 익주의 성문과 장수들의 마음을 함께 열어야 할 차례입니다.',
|
||||
rewardHint: '다음 장: 낙성 본성 공략 준비 중',
|
||||
pages: thirtiethBattleVictoryPages,
|
||||
unavailableNotice: '낙성 본성 공략은 다음 작업에서 이어집니다. 군영에서 방통, 법정, 오의의 정보와 출전 조합을 더 다듬어 두십시오.'
|
||||
rewardHint: `예상 보상: ${thirtyFirstBattleScenario.title} 개방 / 엄안 합류`,
|
||||
nextBattleId: thirtyFirstBattleScenario.id,
|
||||
campaignStep: 'thirty-first-battle',
|
||||
pages: [...thirtiethBattleVictoryPages, ...thirtyFirstBattleIntroPages]
|
||||
},
|
||||
[thirtyFirstBattleScenario.id]: {
|
||||
afterBattleId: thirtyFirstBattleScenario.id,
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '성도 압박 준비',
|
||||
description:
|
||||
'낙성 본성이 열리고 엄안이 합류했습니다. 이제 유비군은 익주의 심부를 흔들며 성도까지 이어지는 길을 준비해야 합니다.',
|
||||
rewardHint: '다음 장: 성도 압박 준비 중',
|
||||
pages: thirtyFirstBattleVictoryPages,
|
||||
unavailableNotice: '성도 압박과 익주 최종 설득은 다음 작업에서 이어집니다. 새로 합류한 엄안을 포함해 성벽과 고지 전투 조합을 준비하십시오.'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1952,6 +1952,68 @@ export const thirtiethBattleVictoryPages: StoryPage[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const thirtyFirstBattleIntroPages: StoryPage[] = [
|
||||
{
|
||||
id: 'thirty-first-luo-main-gate',
|
||||
bgm: 'story-dark',
|
||||
chapter: '낙성 본성의 문',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '법정',
|
||||
text: '장임은 낙봉파에서 물러난 뒤 낙성 본성의 문을 모두 닫았습니다. 성벽은 높고, 성 안의 장수들은 아직 유비공의 뜻을 의심하고 있습니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirty-first-veteran-wall',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '노장 엄안',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '오의',
|
||||
text: '성 안에는 엄안이라는 노장이 있습니다. 장임과 뜻을 같이해 버티고 있으나, 백성을 해치지 않겠다는 약속을 보이면 마음이 움직일 수 있습니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirty-first-siege-choice',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '돌파와 설득',
|
||||
background: 'story-militia',
|
||||
speaker: '제갈량',
|
||||
text: '성문을 부수는 힘만으로는 낙성을 얻을 수 없습니다. 방통, 법정, 오의를 함께 쓰면 설득의 길이 열리고, 무장을 더 세우면 짧은 돌파가 가능해집니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirty-first-sortie',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '낙성 본성 공략',
|
||||
background: 'story-sortie',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '성문을 열되 백성의 마음은 닫지 않겠소. 장임의 방어선을 깨고 엄안 같은 장수에게 우리가 지키려는 뜻을 보여 주시오.'
|
||||
}
|
||||
];
|
||||
|
||||
export const thirtyFirstBattleVictoryPages: StoryPage[] = [
|
||||
{
|
||||
id: 'thirty-first-victory-gate-opened',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '낙성의 문이 열리다',
|
||||
background: 'story-sortie',
|
||||
text: '낙성 본성의 문은 마침내 열렸습니다. 성벽 위의 깃발은 내려갔고, 장임의 마지막 방어선도 유비군의 꾸준한 압박 앞에 무너졌습니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirty-first-yan-yan-joins',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '엄안의 결심',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '엄안',
|
||||
text: '성을 잃고도 백성이 다치지 않았다면, 장수는 패배 속에서도 새 주군을 볼 수 있습니다. 노장의 활이 이제 유비공의 군영을 지키겠습니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirty-first-next-chengdu-road',
|
||||
bgm: 'story-dark',
|
||||
chapter: '성도 압박의 길',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '법정',
|
||||
text: '낙성이 열렸으니 익주의 심장부가 흔들립니다. 이제 유비군은 성도를 향한 길에서 더 많은 항복과 저항을 동시에 맞이하게 될 것입니다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleMap: BattleMap = {
|
||||
width: 20,
|
||||
height: 18,
|
||||
@@ -2277,6 +2339,12 @@ export const thirtiethBattleMap: BattleMap = {
|
||||
terrain: createThirtiethBattleTerrain()
|
||||
};
|
||||
|
||||
export const thirtyFirstBattleMap: BattleMap = {
|
||||
width: 62,
|
||||
height: 44,
|
||||
terrain: createThirtyFirstBattleTerrain()
|
||||
};
|
||||
|
||||
export const firstBattleUnits: UnitData[] = [
|
||||
{
|
||||
id: 'liu-bei',
|
||||
@@ -4422,6 +4490,30 @@ export const luoCastleRecruitUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const luoCastleProperRecruitUnits: UnitData[] = [
|
||||
{
|
||||
id: 'yan-yan',
|
||||
name: '엄안',
|
||||
faction: 'ally',
|
||||
className: '익주 노장',
|
||||
classKey: 'archer',
|
||||
level: 19,
|
||||
exp: 0,
|
||||
hp: 42,
|
||||
maxHp: 42,
|
||||
attack: 14,
|
||||
move: 4,
|
||||
stats: { might: 78, intelligence: 70, leadership: 88, agility: 68, luck: 74 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 3, exp: 18 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 20 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 10,
|
||||
y: 27
|
||||
}
|
||||
];
|
||||
|
||||
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||
'liu-bei': { x: 2, y: 17 },
|
||||
'guan-yu': { x: 3, y: 17 },
|
||||
@@ -12782,6 +12874,169 @@ export const thirtiethBattleUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const thirtyFirstBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||
'liu-bei': { x: 4, y: 39 },
|
||||
'guan-yu': { x: 5, y: 37 },
|
||||
'zhang-fei': { x: 4, y: 41 },
|
||||
'jian-yong': { x: 3, y: 42 },
|
||||
'mi-zhu': { x: 5, y: 42 },
|
||||
'sun-qian': { x: 7, y: 41 },
|
||||
'zhao-yun': { x: 8, y: 38 },
|
||||
'zhuge-liang': { x: 6, y: 37 },
|
||||
'ma-liang': { x: 8, y: 42 },
|
||||
'yi-ji': { x: 9, y: 40 },
|
||||
'gong-zhi': { x: 10, y: 42 },
|
||||
'huang-zhong': { x: 7, y: 39 },
|
||||
'wei-yan': { x: 10, y: 37 },
|
||||
'pang-tong': { x: 9, y: 38 },
|
||||
'fa-zheng': { x: 11, y: 40 },
|
||||
'wu-yi': { x: 12, y: 38 }
|
||||
};
|
||||
|
||||
const createThirtyFirstEnemyUnit = (
|
||||
id: string,
|
||||
name: string,
|
||||
className: string,
|
||||
classKey: UnitClassKey,
|
||||
level: number,
|
||||
hp: number,
|
||||
attack: number,
|
||||
x: number,
|
||||
y: number
|
||||
): UnitData => {
|
||||
const equipmentByClass: Record<UnitClassKey, UnitData['equipment']> = {
|
||||
lord: {
|
||||
weapon: { itemId: 'twin-oath-blades', level: 5, exp: 70 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 62 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
infantry: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 74 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 66 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
cavalry: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 78 },
|
||||
armor: { itemId: 'lamellar-armor', level: 5, exp: 68 },
|
||||
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||
},
|
||||
spearman: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 76 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 66 },
|
||||
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||
},
|
||||
archer: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 72 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 58 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
strategist: {
|
||||
weapon: { itemId: 'white-feather-fan', level: 5, exp: 64 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 54 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
quartermaster: {
|
||||
weapon: { itemId: 'white-feather-fan', level: 5, exp: 56 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 48 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
bandit: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 66 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 54 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
yellowTurban: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 78 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 68 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
rebelLeader: {
|
||||
weapon: { itemId: 'leader-axe', level: 5, exp: 82 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 76 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
}
|
||||
};
|
||||
const moveByClass: Record<UnitClassKey, number> = {
|
||||
lord: 4,
|
||||
infantry: 3,
|
||||
cavalry: 5,
|
||||
spearman: 3,
|
||||
archer: 3,
|
||||
strategist: 4,
|
||||
quartermaster: 4,
|
||||
bandit: 4,
|
||||
yellowTurban: 3,
|
||||
rebelLeader: 4
|
||||
};
|
||||
const statLift = Math.max(0, level - 52) * 2;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
faction: 'enemy',
|
||||
className,
|
||||
classKey,
|
||||
level,
|
||||
exp: Math.min(98, 84 + statLift),
|
||||
hp,
|
||||
maxHp: hp,
|
||||
attack,
|
||||
move: moveByClass[classKey],
|
||||
stats: {
|
||||
might: 148 + statLift + (classKey === 'rebelLeader' ? 34 : classKey === 'cavalry' ? 24 : classKey === 'yellowTurban' ? 20 : 0),
|
||||
intelligence: 108 + statLift + (classKey === 'strategist' ? 34 : classKey === 'archer' ? 12 : 0),
|
||||
leadership: 132 + statLift + (classKey === 'rebelLeader' ? 34 : classKey === 'yellowTurban' ? 18 : 0),
|
||||
agility: 112 + statLift + (classKey === 'bandit' ? 28 : classKey === 'cavalry' ? 26 : classKey === 'archer' ? 18 : 0),
|
||||
luck: 90 + Math.floor(statLift / 2)
|
||||
},
|
||||
equipment: equipmentByClass[classKey],
|
||||
x,
|
||||
y
|
||||
};
|
||||
};
|
||||
|
||||
export const thirtyFirstBattleUnits: UnitData[] = [
|
||||
...[
|
||||
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
|
||||
...xuzhouRecruitUnits,
|
||||
...caoBreakRecruitUnits,
|
||||
...liuBiaoRecruitUnits,
|
||||
...zhugeRecruitUnits,
|
||||
...jingzhouRecruitUnits,
|
||||
...guiyangRecruitUnits,
|
||||
...wulingRecruitUnits,
|
||||
...changshaRecruitUnits,
|
||||
...yizhouRecruitUnits,
|
||||
...fuPassRecruitUnits,
|
||||
...luoCastleRecruitUnits
|
||||
].map((unit) => placeScenarioUnit(unit, thirtyFirstBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
|
||||
createThirtyFirstEnemyUnit('luo-main-scout-a', '낙성 정찰병', '성밖 정찰병', 'bandit', 53, 148, 55, 18, 39),
|
||||
createThirtyFirstEnemyUnit('luo-main-scout-b', '낙성 정찰병', '수로 정찰병', 'bandit', 53, 148, 55, 23, 36),
|
||||
createThirtyFirstEnemyUnit('luo-main-infantry-a', '낙성 보병', '외성 보병', 'yellowTurban', 54, 210, 66, 28, 34),
|
||||
createThirtyFirstEnemyUnit('luo-main-infantry-b', '낙성 보병', '외성 보병', 'yellowTurban', 54, 210, 66, 33, 32),
|
||||
createThirtyFirstEnemyUnit('luo-main-infantry-c', '낙성 보병', '성문 보병', 'yellowTurban', 55, 220, 68, 40, 28),
|
||||
createThirtyFirstEnemyUnit('luo-main-infantry-d', '낙성 보병', '성문 보병', 'yellowTurban', 55, 220, 68, 44, 26),
|
||||
createThirtyFirstEnemyUnit('luo-main-infantry-e', '낙성 보병', '내성 보병', 'yellowTurban', 56, 230, 70, 51, 23),
|
||||
createThirtyFirstEnemyUnit('luo-main-archer-a', '낙성 궁병', '외벽 궁병', 'archer', 54, 146, 55, 31, 29),
|
||||
createThirtyFirstEnemyUnit('luo-main-archer-b', '낙성 궁병', '성벽 궁병', 'archer', 55, 152, 56, 42, 22),
|
||||
createThirtyFirstEnemyUnit('luo-main-archer-c', '낙성 궁병', '성벽 궁병', 'archer', 55, 152, 56, 49, 18),
|
||||
createThirtyFirstEnemyUnit('luo-main-archer-d', '낙성 궁병', '내성 궁병', 'archer', 56, 158, 58, 55, 24),
|
||||
createThirtyFirstEnemyUnit('luo-main-cavalry-a', '낙성 기병', '우익 기병', 'cavalry', 55, 220, 70, 29, 40),
|
||||
createThirtyFirstEnemyUnit('luo-main-cavalry-b', '낙성 기병', '좌익 기병', 'cavalry', 55, 220, 70, 39, 36),
|
||||
createThirtyFirstEnemyUnit('luo-main-cavalry-c', '낙성 기병', '내성 기병', 'cavalry', 56, 232, 72, 53, 29),
|
||||
createThirtyFirstEnemyUnit('luo-main-guard-a', '장임 친위대', '성문 친위대', 'yellowTurban', 56, 236, 72, 48, 21),
|
||||
createThirtyFirstEnemyUnit('luo-main-guard-b', '장임 친위대', '성문 친위대', 'yellowTurban', 56, 236, 72, 51, 20),
|
||||
createThirtyFirstEnemyUnit('luo-main-guard-c', '장임 친위대', '내성 친위대', 'yellowTurban', 57, 246, 74, 56, 19),
|
||||
createThirtyFirstEnemyUnit('luo-main-strategist-a', '낙성 책사', '성문 책사', 'strategist', 55, 150, 56, 45, 18),
|
||||
createThirtyFirstEnemyUnit('luo-main-strategist-b', '낙성 책사', '내성 책사', 'strategist', 56, 156, 58, 55, 15),
|
||||
createThirtyFirstEnemyUnit('luo-main-ambusher-a', '낙성 복병', '남벽 복병', 'bandit', 54, 156, 58, 34, 39),
|
||||
createThirtyFirstEnemyUnit('luo-main-ambusher-b', '낙성 복병', '동문 복병', 'bandit', 54, 156, 58, 48, 32),
|
||||
createThirtyFirstEnemyUnit('luo-main-ambusher-c', '낙성 복병', '북벽 복병', 'bandit', 55, 164, 60, 57, 11),
|
||||
createThirtyFirstEnemyUnit('luo-main-veteran-yan-yan', '엄안', '익주 노장', 'archer', 57, 210, 68, 50, 22),
|
||||
createThirtyFirstEnemyUnit('luo-main-officer-a', '낙성 부장', '성문 부장', 'rebelLeader', 57, 260, 75, 54, 20),
|
||||
createThirtyFirstEnemyUnit('luo-main-leader-zhang-ren', '장임', '낙성 본성 주장', 'rebelLeader', 58, 286, 78, 58, 18)
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
@@ -13106,6 +13361,33 @@ export const luoCastleRecruitBonds: BattleBond[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const luoCastleProperRecruitBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__yan-yan',
|
||||
unitIds: ['liu-bei', 'yan-yan'],
|
||||
title: '노장의 항복',
|
||||
level: 32,
|
||||
exp: 0,
|
||||
description: '유비의 명분과 엄안의 노련한 절개가 만나, 익주의 성문과 백성을 함께 지키는 신뢰로 이어집니다.'
|
||||
},
|
||||
{
|
||||
id: 'huang-zhong__yan-yan',
|
||||
unitIds: ['huang-zhong', 'yan-yan'],
|
||||
title: '노장 쌍궁',
|
||||
level: 30,
|
||||
exp: 0,
|
||||
description: '황충의 침착한 활과 엄안의 성벽 방어 경험이 맞물려, 고지와 성문에서 강한 원거리 공명을 만듭니다.'
|
||||
},
|
||||
{
|
||||
id: 'zhang-fei__yan-yan',
|
||||
unitIds: ['zhang-fei', 'yan-yan'],
|
||||
title: '호통과 절개',
|
||||
level: 28,
|
||||
exp: 0,
|
||||
description: '장비의 거친 돌파력과 엄안의 굽히지 않는 절개가 부딪치며, 서로의 강함을 인정하는 관계로 자랍니다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
@@ -13141,6 +13423,7 @@ export const twentySeventhBattleBonds: BattleBond[] = [...twentySixthBattleBonds
|
||||
export const twentyEighthBattleBonds: BattleBond[] = [...twentySeventhBattleBonds, ...yizhouRecruitBonds].map(cloneBattleBondForScenario);
|
||||
export const twentyNinthBattleBonds: BattleBond[] = [...twentyEighthBattleBonds, ...fuPassRecruitBonds].map(cloneBattleBondForScenario);
|
||||
export const thirtiethBattleBonds: BattleBond[] = [...twentyNinthBattleBonds, ...luoCastleRecruitBonds].map(cloneBattleBondForScenario);
|
||||
export const thirtyFirstBattleBonds: BattleBond[] = [...thirtiethBattleBonds].map(cloneBattleBondForScenario);
|
||||
|
||||
function createEighthBattleTerrain(): TerrainType[][] {
|
||||
return Array.from({ length: 22 }, (_, y) =>
|
||||
@@ -14338,6 +14621,70 @@ function createThirtiethBattleTerrain(): TerrainType[][] {
|
||||
);
|
||||
}
|
||||
|
||||
function createThirtyFirstBattleTerrain(): TerrainType[][] {
|
||||
return Array.from({ length: 44 }, (_, y) =>
|
||||
Array.from({ length: 62 }, (_, x): TerrainType => {
|
||||
if (x <= 4 && y >= 37) {
|
||||
return 'camp';
|
||||
}
|
||||
if (
|
||||
(x >= 47 && x <= 60 && y >= 13 && y <= 25) ||
|
||||
(x >= 43 && x <= 52 && y >= 9 && y <= 17) ||
|
||||
(x >= 52 && x <= 60 && y >= 17 && y <= 28)
|
||||
) {
|
||||
return 'fort';
|
||||
}
|
||||
if (
|
||||
(x >= 18 && x <= 21 && y >= 35 && y <= 37) ||
|
||||
(x >= 35 && x <= 37 && y >= 29 && y <= 31) ||
|
||||
(x >= 45 && x <= 47 && y >= 24 && y <= 26)
|
||||
) {
|
||||
return 'village';
|
||||
}
|
||||
if (
|
||||
(y >= 39 && x >= 4 && x <= 18) ||
|
||||
(x >= 17 && x <= 30 && y >= 35 && y <= 39) ||
|
||||
(x >= 29 && x <= 42 && y >= 30 && y <= 35) ||
|
||||
(x >= 41 && x <= 52 && y >= 24 && y <= 30) ||
|
||||
(x >= 51 && x <= 59 && y >= 19 && y <= 24) ||
|
||||
(x >= 56 && x <= 60 && y >= 18 && y <= 20)
|
||||
) {
|
||||
return 'road';
|
||||
}
|
||||
if ((x === 13 || x === 14) && y >= 0 && y <= 33) {
|
||||
return 'river';
|
||||
}
|
||||
if ((x === 33 || x === 34) && y >= 4 && y <= 28) {
|
||||
return 'river';
|
||||
}
|
||||
if ((x === 44 || x === 45) && y >= 31 && y <= 43) {
|
||||
return 'river';
|
||||
}
|
||||
if (
|
||||
(x >= 4 && x <= 20 && y >= 8 && y <= 28) ||
|
||||
(x >= 21 && x <= 32 && y >= 14 && y <= 29) ||
|
||||
(x >= 35 && x <= 49 && y >= 32 && y <= 43) ||
|
||||
(x >= 47 && x <= 61 && y >= 28 && y <= 42)
|
||||
) {
|
||||
return 'forest';
|
||||
}
|
||||
if (
|
||||
(x >= 0 && x <= 11 && y <= 15) ||
|
||||
(x >= 21 && x <= 32 && y <= 13) ||
|
||||
(x >= 34 && x <= 50 && y >= 2 && y <= 14) ||
|
||||
(x >= 53 && x <= 61 && y <= 12) ||
|
||||
(x >= 25 && x <= 36 && y >= 36 && y <= 43)
|
||||
) {
|
||||
return 'hill';
|
||||
}
|
||||
if ((x <= 2 && y <= 36) || (x >= 60 && y >= 26) || (x >= 47 && x <= 50 && y <= 9)) {
|
||||
return 'cliff';
|
||||
}
|
||||
return 'plain';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||
return {
|
||||
...cloneUnitForScenario(unit),
|
||||
|
||||
@@ -37,6 +37,7 @@ const unitTexture: Record<string, string> = {
|
||||
'pang-tong': 'unit-liu-bei',
|
||||
'fa-zheng': 'unit-liu-bei',
|
||||
'wu-yi': 'unit-guan-yu',
|
||||
'yan-yan': 'unit-rebel-archer',
|
||||
'rebel-a': 'unit-rebel',
|
||||
'rebel-b': 'unit-rebel',
|
||||
'rebel-c': 'unit-rebel-archer',
|
||||
@@ -401,7 +402,32 @@ const unitTexture: Record<string, string> = {
|
||||
'luofeng-ambusher-b': 'unit-rebel',
|
||||
'luofeng-ambusher-c': 'unit-rebel',
|
||||
'luofeng-ambusher-d': 'unit-rebel',
|
||||
'luofeng-leader-zhang-ren': 'unit-rebel-leader'
|
||||
'luofeng-leader-zhang-ren': 'unit-rebel-leader',
|
||||
'luo-main-scout-a': 'unit-rebel',
|
||||
'luo-main-scout-b': 'unit-rebel',
|
||||
'luo-main-infantry-a': 'unit-rebel',
|
||||
'luo-main-infantry-b': 'unit-rebel',
|
||||
'luo-main-infantry-c': 'unit-rebel',
|
||||
'luo-main-infantry-d': 'unit-rebel',
|
||||
'luo-main-infantry-e': 'unit-rebel',
|
||||
'luo-main-archer-a': 'unit-rebel-archer',
|
||||
'luo-main-archer-b': 'unit-rebel-archer',
|
||||
'luo-main-archer-c': 'unit-rebel-archer',
|
||||
'luo-main-archer-d': 'unit-rebel-archer',
|
||||
'luo-main-cavalry-a': 'unit-rebel-cavalry',
|
||||
'luo-main-cavalry-b': 'unit-rebel-cavalry',
|
||||
'luo-main-cavalry-c': 'unit-rebel-cavalry',
|
||||
'luo-main-guard-a': 'unit-rebel',
|
||||
'luo-main-guard-b': 'unit-rebel',
|
||||
'luo-main-guard-c': 'unit-rebel',
|
||||
'luo-main-strategist-a': 'unit-rebel-archer',
|
||||
'luo-main-strategist-b': 'unit-rebel-archer',
|
||||
'luo-main-ambusher-a': 'unit-rebel',
|
||||
'luo-main-ambusher-b': 'unit-rebel',
|
||||
'luo-main-ambusher-c': 'unit-rebel',
|
||||
'luo-main-veteran-yan-yan': 'unit-rebel-archer',
|
||||
'luo-main-officer-a': 'unit-rebel-leader',
|
||||
'luo-main-leader-zhang-ren': 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
|
||||
@@ -1266,7 +1292,32 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
||||
'luofeng-ambusher-b': 'aggressive',
|
||||
'luofeng-ambusher-c': 'aggressive',
|
||||
'luofeng-ambusher-d': 'aggressive',
|
||||
'luofeng-leader-zhang-ren': 'guard'
|
||||
'luofeng-leader-zhang-ren': 'guard',
|
||||
'luo-main-scout-a': 'aggressive',
|
||||
'luo-main-scout-b': 'aggressive',
|
||||
'luo-main-infantry-a': 'guard',
|
||||
'luo-main-infantry-b': 'guard',
|
||||
'luo-main-infantry-c': 'guard',
|
||||
'luo-main-infantry-d': 'guard',
|
||||
'luo-main-infantry-e': 'guard',
|
||||
'luo-main-archer-a': 'hold',
|
||||
'luo-main-archer-b': 'hold',
|
||||
'luo-main-archer-c': 'hold',
|
||||
'luo-main-archer-d': 'hold',
|
||||
'luo-main-cavalry-a': 'aggressive',
|
||||
'luo-main-cavalry-b': 'aggressive',
|
||||
'luo-main-cavalry-c': 'aggressive',
|
||||
'luo-main-guard-a': 'guard',
|
||||
'luo-main-guard-b': 'guard',
|
||||
'luo-main-guard-c': 'guard',
|
||||
'luo-main-strategist-a': 'hold',
|
||||
'luo-main-strategist-b': 'hold',
|
||||
'luo-main-ambusher-a': 'aggressive',
|
||||
'luo-main-ambusher-b': 'aggressive',
|
||||
'luo-main-ambusher-c': 'aggressive',
|
||||
'luo-main-veteran-yan-yan': 'hold',
|
||||
'luo-main-officer-a': 'guard',
|
||||
'luo-main-leader-zhang-ren': 'guard'
|
||||
};
|
||||
|
||||
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
||||
|
||||
@@ -16,6 +16,7 @@ import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-m
|
||||
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 thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg';
|
||||
import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg';
|
||||
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
||||
import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
|
||||
@@ -118,6 +119,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('battle-map-twenty-eighth', twentyEighthBattleMapUrl);
|
||||
this.load.image('battle-map-twenty-ninth', twentyNinthBattleMapUrl);
|
||||
this.load.image('battle-map-thirtieth', thirtiethBattleMapUrl);
|
||||
this.load.image('battle-map-thirty-first', thirtyFirstBattleMapUrl);
|
||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
jingzhouRecruitUnits,
|
||||
liuBiaoRecruitBonds,
|
||||
liuBiaoRecruitUnits,
|
||||
luoCastleProperRecruitBonds,
|
||||
luoCastleProperRecruitUnits,
|
||||
luoCastleRecruitBonds,
|
||||
luoCastleRecruitUnits,
|
||||
wulingRecruitBonds,
|
||||
@@ -119,6 +121,8 @@ type SortieUnitTacticalSummary = {
|
||||
terrainLine: string;
|
||||
};
|
||||
|
||||
type SortieTerrainCounts = Partial<Record<TerrainType, number>>;
|
||||
|
||||
type SortieRecommendation = {
|
||||
unitId: string;
|
||||
reason: string;
|
||||
@@ -317,7 +321,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
||||
'twenty-seventh-battle-yizhou-relief-road',
|
||||
'twenty-eighth-battle-fu-pass-entry',
|
||||
'twenty-ninth-battle-luo-outer-wall',
|
||||
'thirtieth-battle-luofeng-ambush'
|
||||
'thirtieth-battle-luofeng-ambush',
|
||||
'thirty-first-battle-luo-main-gate'
|
||||
],
|
||||
nextHints: ['형주 방위', '익주 진입', '방통·법정 합류']
|
||||
},
|
||||
@@ -361,7 +366,8 @@ const campBattleIds = {
|
||||
twentySeventh: 'twenty-seventh-battle-yizhou-relief-road',
|
||||
twentyEighth: 'twenty-eighth-battle-fu-pass-entry',
|
||||
twentyNinth: 'twenty-ninth-battle-luo-outer-wall',
|
||||
thirtieth: 'thirtieth-battle-luofeng-ambush'
|
||||
thirtieth: 'thirtieth-battle-luofeng-ambush',
|
||||
thirtyFirst: 'thirty-first-battle-luo-main-gate'
|
||||
} as const;
|
||||
|
||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||
@@ -696,6 +702,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
|
||||
{ unitId: 'huang-zhong', reason: '고지 궁병과 매복 부대를 안정적으로 견제하는 원거리 전력입니다.' }
|
||||
],
|
||||
note: '낙봉파 매복전은 열여섯 명 중 여섯 명만 출전합니다. 오의가 새로 후보에 들어오며, 방통 생존과 매복 대응 때문에 책사와 기동 무장 사이의 선택이 더 중요해집니다.'
|
||||
},
|
||||
'thirty-first-battle-luo-main-gate': {
|
||||
maxUnits: 6,
|
||||
recommended: [
|
||||
{ unitId: 'liu-bei', reason: '낙성 본성의 항복 명분과 패배 조건의 중심입니다.' },
|
||||
{ unitId: 'pang-tong', reason: '성문 돌파와 퇴로 확보를 동시에 계산하는 핵심 책사입니다.' },
|
||||
{ unitId: 'fa-zheng', reason: '익주 장수들의 마음을 읽고 엄안 설득의 말길을 여는 역할입니다.' },
|
||||
{ unitId: 'wu-yi', reason: '낙성 내부 지형과 성문 방어 습관을 아는 새 합류 장수입니다.' },
|
||||
{ unitId: 'huang-zhong', reason: '성벽 궁병과 노장 엄안의 원거리 압박에 대응하기 좋습니다.' },
|
||||
{ unitId: 'zhang-fei', reason: '성문 앞 친위대를 짧게 무너뜨리는 돌파력이 필요합니다.' }
|
||||
],
|
||||
note: '낙성 본성 공략은 열여섯 명 중 여섯 명만 출전합니다. 설득을 노리면 책사와 오의를, 빠른 돌파를 노리면 장비와 황충 같은 전투 무장을 우선하십시오.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3210,6 +3228,87 @@ const campDialogues: CampDialogue[] = [
|
||||
rewardExp: 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-yan-after-luomain',
|
||||
title: '노장의 항복',
|
||||
availableAfterBattleIds: [campBattleIds.thirtyFirst],
|
||||
unitIds: ['liu-bei', 'yan-yan'],
|
||||
bondId: 'liu-bei__yan-yan',
|
||||
rewardExp: 34,
|
||||
lines: [
|
||||
'엄안: 성문을 잃었으나 백성을 잃지 않았습니다. 패장은 목숨을 구걸하지 않지만, 그 뜻만은 보았습니다.',
|
||||
'유비: 노장의 절개를 꺾으려 온 것이 아니오. 익주의 백성과 장수들이 함께 살 길을 열고자 왔소.',
|
||||
'엄안: 그렇다면 이 활은 성벽 위에서가 아니라 유비공의 진영에서 백성을 지키겠습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'honor-yan-yan-resolve',
|
||||
label: '엄안의 절개를 예우한다',
|
||||
response: '유비가 포박을 풀게 하자 엄안은 고개를 숙이고도 눈빛을 잃지 않았습니다.',
|
||||
rewardExp: 13
|
||||
},
|
||||
{
|
||||
id: 'ask-yan-yan-yizhou-road',
|
||||
label: '익주 안쪽 길을 묻는다',
|
||||
response: '엄안은 성도까지 이어지는 창고와 물길을 짚어 주며, 무리한 약탈을 피해야 한다고 조언했습니다.',
|
||||
rewardExp: 12
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'huang-yan-after-luomain',
|
||||
title: '노장 쌍궁',
|
||||
availableAfterBattleIds: [campBattleIds.thirtyFirst],
|
||||
unitIds: ['huang-zhong', 'yan-yan'],
|
||||
bondId: 'huang-zhong__yan-yan',
|
||||
rewardExp: 32,
|
||||
lines: [
|
||||
'황충: 성벽 위 활이 흔들리지 않더군. 적으로 만났으나 노장의 손맛은 분명했습니다.',
|
||||
'엄안: 황 장군의 활도 고지의 바람을 거슬렀습니다. 이제 같은 편이라면 서로의 사각을 메울 수 있겠지요.',
|
||||
'황충: 다음 성벽은 함께 바라봅시다. 젊은 장수들에게도 노장의 활이 아직 살아 있음을 보여 줘야겠소.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'compare-old-bow-stance',
|
||||
label: '활 자세를 비교한다',
|
||||
response: '두 노장이 말없이 활시위를 당기자 군영 뒤편의 표적이 거의 동시에 흔들렸습니다.',
|
||||
rewardExp: 12
|
||||
},
|
||||
{
|
||||
id: 'plan-wall-archery-luomain',
|
||||
label: '성벽 사격을 연구한다',
|
||||
response: '황충과 엄안은 다음 전장의 고지와 성벽을 나눠 맡는 방법을 병사들에게 가르쳤습니다.',
|
||||
rewardExp: 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'fa-wu-after-luomain',
|
||||
title: '성도 압박의 정보',
|
||||
availableAfterBattleIds: [campBattleIds.thirtyFirst],
|
||||
unitIds: ['fa-zheng', 'wu-yi'],
|
||||
bondId: 'fa-zheng__wu-yi',
|
||||
rewardExp: 32,
|
||||
lines: [
|
||||
'법정: 낙성이 열렸으니 유장의 마음도 흔들릴 것입니다. 하지만 성도는 겁만으로 열리지 않습니다.',
|
||||
'오의: 익주 장수들은 유비공이 약속을 지키는지 보고 있습니다. 엄안의 합류가 그 증거가 될 수 있습니다.',
|
||||
'법정: 그렇다면 다음 길은 성문보다 말문을 먼저 열어야겠군요.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'write-chengdu-pressure-letter',
|
||||
label: '성도에 보낼 글을 쓴다',
|
||||
response: '법정은 위협 대신 약속을 앞세우고, 오의는 익주 장수들이 믿을 만한 이름들을 덧붙였습니다.',
|
||||
rewardExp: 12
|
||||
},
|
||||
{
|
||||
id: 'mark-inner-yizhou-route',
|
||||
label: '익주 안쪽 길을 표시한다',
|
||||
response: '오의가 군량 길을 긋고 법정이 사람의 마음길을 덧대자, 성도 압박의 첫 윤곽이 잡혔습니다.',
|
||||
rewardExp: 11
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -4139,6 +4238,66 @@ const campVisits: CampVisitDefinition[] = [
|
||||
itemRewards: ['낙성 수로도 1']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'luo-main-gate-relief',
|
||||
title: '낙성 성문 구호',
|
||||
location: '낙성 본성 성문',
|
||||
availableAfterBattleIds: [campBattleIds.thirtyFirst],
|
||||
bondId: 'liu-bei__yan-yan',
|
||||
description: '유비와 엄안이 열린 성문 앞의 백성을 달래고, 전투 뒤 무너진 성문과 창고를 정리합니다.',
|
||||
lines: [
|
||||
'엄안: 성문을 지키던 병사들도 결국 익주의 백성입니다. 이들을 함부로 다루면 다음 성도 길은 닫힙니다.',
|
||||
'유비: 부상자는 먼저 치료하고, 창고의 곡식은 명단을 적어 돌려주시오.',
|
||||
'낙성 백성들은 아직 두려움 속에 있었지만, 군영의 약탈 금지 명령을 들으며 문틈으로 고개를 내밀기 시작했습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'treat-luo-gate-wounded',
|
||||
label: '성문 부상자를 치료한다',
|
||||
response: '상처약이 성문 양쪽에 나뉘어 쓰이자 엄안은 말없이 유비에게 한 번 더 예를 표했습니다.',
|
||||
bondExp: 26,
|
||||
itemRewards: ['상처약 4']
|
||||
},
|
||||
{
|
||||
id: 'publish-no-looting-order',
|
||||
label: '약탈 금지 명령을 붙인다',
|
||||
response: '성문 앞에 붙은 명령문은 낙성 안쪽으로 빠르게 퍼졌고, 다음 항복 권고의 근거가 되었습니다.',
|
||||
bondExp: 24,
|
||||
gold: 380,
|
||||
itemRewards: ['낙성 안정문 1']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chengdu-pressure-council',
|
||||
title: '성도 압박 회의',
|
||||
location: '낙성 군영 본막',
|
||||
availableAfterBattleIds: [campBattleIds.thirtyFirst],
|
||||
bondId: 'fa-zheng__wu-yi',
|
||||
description: '법정, 오의, 엄안이 성도까지 이어지는 길과 유장의 흔들리는 장수들을 정리합니다.',
|
||||
lines: [
|
||||
'법정: 낙성이 열렸다는 소식은 성도까지 하루면 닿을 것입니다. 그 소식이 공포가 아니라 약속으로 들려야 합니다.',
|
||||
'오의: 익주의 길을 아는 장수들이 이제 우리 군영에 있습니다. 길은 좁아도 말은 넓게 보내야 합니다.',
|
||||
'엄안: 노장 하나가 항복했다고 모든 성이 열리지는 않습니다. 하지만 백성을 살렸다는 말은 오래 갑니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'send-chengdu-parley-envoy',
|
||||
label: '성도에 사자를 보낸다',
|
||||
response: '사자는 항복 권고문과 낙성 안정문을 함께 들고 떠났고, 군영에는 다음 전장 후보지가 표시되었습니다.',
|
||||
bondExp: 26,
|
||||
itemRewards: ['성도 권고문 초안 1']
|
||||
},
|
||||
{
|
||||
id: 'survey-inner-yizhou-storehouses',
|
||||
label: '익주 창고를 조사한다',
|
||||
response: '창고와 물길을 먼저 장악하자는 의견이 모이며, 다음 전투의 보급 목표가 분명해졌습니다.',
|
||||
bondExp: 24,
|
||||
gold: 400,
|
||||
itemRewards: ['익주 창고 장부 1']
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -4155,6 +4314,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private tabButtons: CampTabButtonView[] = [];
|
||||
private visitedTabs = new Set<CampTab>();
|
||||
private selectedSortieUnitIds: string[] = [];
|
||||
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
|
||||
|
||||
constructor() {
|
||||
super('CampScene');
|
||||
@@ -4224,6 +4384,41 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (battleId === campBattleIds.thirtyFirst) {
|
||||
this.campaign = ensureCampaignRosterUnits(
|
||||
[
|
||||
...xuzhouRecruitUnits,
|
||||
...caoBreakRecruitUnits,
|
||||
...liuBiaoRecruitUnits,
|
||||
...zhugeRecruitUnits,
|
||||
...jingzhouRecruitUnits,
|
||||
...guiyangRecruitUnits,
|
||||
...wulingRecruitUnits,
|
||||
...changshaRecruitUnits,
|
||||
...yizhouRecruitUnits,
|
||||
...fuPassRecruitUnits,
|
||||
...luoCastleRecruitUnits,
|
||||
...luoCastleProperRecruitUnits
|
||||
],
|
||||
[
|
||||
...xuzhouRecruitBonds,
|
||||
...caoBreakRecruitBonds,
|
||||
...liuBiaoRecruitBonds,
|
||||
...zhugeRecruitBonds,
|
||||
...jingzhouRecruitBonds,
|
||||
...guiyangRecruitBonds,
|
||||
...wulingRecruitBonds,
|
||||
...changshaRecruitBonds,
|
||||
...yizhouRecruitBonds,
|
||||
...fuPassRecruitBonds,
|
||||
...luoCastleRecruitBonds,
|
||||
...luoCastleProperRecruitBonds
|
||||
]
|
||||
);
|
||||
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||
return;
|
||||
}
|
||||
|
||||
if (battleId === campBattleIds.thirtieth || battleId === campBattleIds.twentyNinth) {
|
||||
this.campaign = ensureCampaignRosterUnits(
|
||||
[
|
||||
@@ -4465,6 +4660,9 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private currentCampTitle() {
|
||||
const battleId = this.currentCampBattleId();
|
||||
if (battleId === campBattleIds.thirtyFirst) {
|
||||
return '낙성 본성 공략 후 군영';
|
||||
}
|
||||
if (battleId === campBattleIds.thirtieth) {
|
||||
return '낙봉파 매복전 후 군영';
|
||||
}
|
||||
@@ -4848,7 +5046,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const column = index % 2;
|
||||
const row = Math.floor(index / 2);
|
||||
const rowX = x + 18 + column * 176;
|
||||
const rowY = y + 218 + row * 28;
|
||||
const rowY = y + 208 + row * 24;
|
||||
const names = slot.unitNames.length > 0 ? slot.unitNames.join(', ') : '미배치';
|
||||
const color = slot.unitNames.length > 0 ? '#a8ffd0' : '#9fb0bf';
|
||||
this.trackSortie(this.add.text(rowX, rowY, slot.label, this.textStyle(12, color, true))).setDepth(depth + 1);
|
||||
@@ -4857,7 +5055,7 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
const warning = plan.warnings[0] ?? plan.reserveLine;
|
||||
this.trackSortie(this.add.text(x + 18, y + height - 28, this.compactText(`조언: ${warning}`, 42), this.textStyle(12, plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + height - 24, this.compactText(`조언: ${warning}`, 42), this.textStyle(12, plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true))).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderSortieEquipmentIcons(unit: UnitData, x: number, y: number, selected: boolean, depth: number) {
|
||||
@@ -4947,8 +5145,13 @@ export class CampScene extends Phaser.Scene {
|
||||
return `지형 ${grade}${bestTerrains ? ` · ${bestTerrains}` : ''}`;
|
||||
}
|
||||
|
||||
private passableTerrainCounts(scenario: BattleScenarioDefinition) {
|
||||
const counts = {} as Record<TerrainType, number>;
|
||||
private passableTerrainCounts(scenario: BattleScenarioDefinition): SortieTerrainCounts {
|
||||
const cached = this.terrainCountCache.get(scenario.id);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const counts: SortieTerrainCounts = {};
|
||||
scenario.map.terrain.forEach((row) => {
|
||||
row.forEach((terrain) => {
|
||||
if (terrainRules[terrain].passable === false) {
|
||||
@@ -4957,6 +5160,7 @@ export class CampScene extends Phaser.Scene {
|
||||
counts[terrain] = (counts[terrain] ?? 0) + 1;
|
||||
});
|
||||
});
|
||||
this.terrainCountCache.set(scenario.id, counts);
|
||||
return counts;
|
||||
}
|
||||
|
||||
@@ -5065,7 +5269,7 @@ export class CampScene extends Phaser.Scene {
|
||||
warnings.push(`아직 ${maxCount - selectedUnits.length}명 더 출전할 수 있습니다.`);
|
||||
}
|
||||
if (missingRecommended.length > 0) {
|
||||
warnings.push(`추천 무장 ${missingRecommended.map((entry) => this.unitName(entry.unitId)).join(', ')}이 대기 중입니다.`);
|
||||
warnings.push(`추천 무장 ${missingRecommended.map((entry) => this.unitName(entry.unitId)).join(', ')} 대기 중입니다.`);
|
||||
}
|
||||
if (roleCounts.front <= 0) {
|
||||
warnings.push('전열 병과가 없어 길목을 버티기 어렵습니다.');
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
seventeenthBattleScenario,
|
||||
tenthBattleScenario,
|
||||
thirteenthBattleScenario,
|
||||
thirtyFirstBattleScenario,
|
||||
thirtiethBattleScenario,
|
||||
twentyEighthBattleScenario,
|
||||
twentyFirstBattleScenario,
|
||||
@@ -359,7 +360,8 @@ export class TitleScene extends Phaser.Scene {
|
||||
campaign.step === 'twenty-seventh-camp' ||
|
||||
campaign.step === 'twenty-eighth-camp' ||
|
||||
campaign.step === 'twenty-ninth-camp' ||
|
||||
campaign.step === 'thirtieth-camp'
|
||||
campaign.step === 'thirtieth-camp' ||
|
||||
campaign.step === 'thirty-first-camp'
|
||||
) {
|
||||
this.scene.start('CampScene');
|
||||
return;
|
||||
@@ -515,6 +517,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'thirty-first-battle') {
|
||||
this.scene.start('BattleScene', { battleId: thirtyFirstBattleScenario.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'first-victory-story') {
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
|
||||
@@ -101,7 +101,9 @@ export type CampaignStep =
|
||||
| 'twenty-ninth-battle'
|
||||
| 'twenty-ninth-camp'
|
||||
| 'thirtieth-battle'
|
||||
| 'thirtieth-camp';
|
||||
| 'thirtieth-camp'
|
||||
| 'thirty-first-battle'
|
||||
| 'thirty-first-camp';
|
||||
|
||||
export type CampaignUnitProgressSnapshot = {
|
||||
unitId: string;
|
||||
@@ -193,7 +195,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
|
||||
'twenty-seventh-battle-yizhou-relief-road': { victory: 'twenty-seventh-camp', retry: 'twenty-seventh-battle' },
|
||||
'twenty-eighth-battle-fu-pass-entry': { victory: 'twenty-eighth-camp', retry: 'twenty-eighth-battle' },
|
||||
'twenty-ninth-battle-luo-outer-wall': { victory: 'twenty-ninth-camp', retry: 'twenty-ninth-battle' },
|
||||
'thirtieth-battle-luofeng-ambush': { victory: 'thirtieth-camp', retry: 'thirtieth-battle' }
|
||||
'thirtieth-battle-luofeng-ambush': { victory: 'thirtieth-camp', retry: 'thirtieth-battle' },
|
||||
'thirty-first-battle-luo-main-gate': { victory: 'thirty-first-camp', retry: 'thirty-first-battle' }
|
||||
};
|
||||
|
||||
export type CampaignSaveSlotSummary = {
|
||||
|
||||
Reference in New Issue
Block a user