15809 lines
763 KiB
TypeScript
15809 lines
763 KiB
TypeScript
import Phaser from 'phaser';
|
|
import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png';
|
|
import { soundDirector } from '../audio/SoundDirector';
|
|
import { battleUiIconFrames, battleUiIconTextureKey, loadBattleUiIcons, type BattleUiIconKey } from '../data/battleUiIcons';
|
|
import {
|
|
equipmentExpToNext,
|
|
equipmentSlotLabels,
|
|
equipmentSlots,
|
|
findItemByName,
|
|
getItem,
|
|
type EquipmentSlot,
|
|
type ItemDefinition
|
|
} from '../data/battleItems';
|
|
import { getUnitClass, terrainRules, type TerrainType, type UnitClassKey } from '../data/battleRules';
|
|
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles';
|
|
import { getSortieFlow } from '../data/campaignFlow';
|
|
import { portraitAssetEntriesForKey, portraitTextureKey, type PortraitAssetEntry } from '../data/portraitAssets';
|
|
import {
|
|
createSortieDeploymentPlan,
|
|
normalizeSortieFormationAssignments,
|
|
type SortieDeploymentSlot,
|
|
type SortieFormationAssignments,
|
|
type SortieFormationRole
|
|
} from '../data/sortieDeployment';
|
|
import {
|
|
caoBreakRecruitBonds,
|
|
caoBreakRecruitUnits,
|
|
chengduPressureRecruitBonds,
|
|
chengduPressureRecruitUnits,
|
|
chengduSurrenderRecruitBonds,
|
|
chengduSurrenderRecruitUnits,
|
|
changshaRecruitBonds,
|
|
changshaRecruitUnits,
|
|
firstBattleBonds,
|
|
firstBattleUnits,
|
|
fuPassRecruitBonds,
|
|
fuPassRecruitUnits,
|
|
guiyangRecruitBonds,
|
|
guiyangRecruitUnits,
|
|
hanzhongDecisiveBonds,
|
|
hanRiverFloodBonds,
|
|
hanzhongOpeningRecruitBonds,
|
|
hanzhongOpeningRecruitUnits,
|
|
hanzhongMainRecruitBonds,
|
|
hanzhongMainRecruitUnits,
|
|
hanzhongScoutRecruitBonds,
|
|
hanzhongScoutRecruitUnits,
|
|
jiangWeiRecruitBonds,
|
|
jiangWeiRecruitUnits,
|
|
mengHuoFourthCaptureBonds,
|
|
mengHuoFifthCaptureBonds,
|
|
mengHuoSixthCaptureBonds,
|
|
mengHuoFinalCaptureBonds,
|
|
mengHuoThirdCaptureBonds,
|
|
fanCastleBonds,
|
|
fanCastleSiegeBonds,
|
|
jingCollapseBonds,
|
|
jingRearCrisisBonds,
|
|
jingDefenseBonds,
|
|
jingzhouRecruitBonds,
|
|
jingzhouRecruitUnits,
|
|
liuBiaoRecruitBonds,
|
|
liuBiaoRecruitUnits,
|
|
luoCastleProperRecruitBonds,
|
|
luoCastleProperRecruitUnits,
|
|
luoCastleRecruitBonds,
|
|
luoCastleRecruitUnits,
|
|
maichengIsolationBonds,
|
|
mengHuoPacificationBonds,
|
|
mengHuoSecondCaptureBonds,
|
|
nanzhongRecoveryBonds,
|
|
northernEighthBattleBonds,
|
|
northernFifthBattleBonds,
|
|
northernFourthBattleBonds,
|
|
northernFirstBattleBonds,
|
|
northernNinthBattleBonds,
|
|
northernEleventhBattleBonds,
|
|
northernSecondBattleBonds,
|
|
northernSeventhBattleBonds,
|
|
northernSixthBattleBonds,
|
|
northernTenthBattleBonds,
|
|
northernThirdBattleBonds,
|
|
northernTwelfthBattleBonds,
|
|
wulingRecruitBonds,
|
|
wulingRecruitUnits,
|
|
xuzhouRecruitBonds,
|
|
xuzhouRecruitUnits,
|
|
yizhouRecruitBonds,
|
|
yizhouRecruitUnits,
|
|
yilingFireBonds,
|
|
yilingVanguardBonds,
|
|
zhugeRecruitBonds,
|
|
zhugeRecruitUnits,
|
|
type PortraitKey,
|
|
type UnitData
|
|
} from '../data/scenario';
|
|
import {
|
|
applyCampVisitReward,
|
|
applyCampBondExp,
|
|
campaignReserveTrainingFocusDefinitions,
|
|
defaultCampaignReserveTrainingFocusId,
|
|
ensureCampaignRosterUnits,
|
|
getCampaignState,
|
|
getFirstBattleReport,
|
|
markCampaignStep,
|
|
listCampaignSaveSlots,
|
|
campaignSaveSlotCount,
|
|
saveCampaignState,
|
|
setCampaignReserveTrainingAssignment,
|
|
setCampaignReserveTrainingFocus,
|
|
type CampaignSaveSlotSummary,
|
|
type CampaignStep,
|
|
type CampaignState,
|
|
type CampaignReserveTrainingFocusDefinition,
|
|
type CampaignReserveTrainingFocusId,
|
|
type CampaignSortieItemAssignments,
|
|
type FirstBattleReport
|
|
} from '../state/campaignState';
|
|
import { palette } from '../ui/palette';
|
|
import { startLazyScene } from './lazyScenes';
|
|
|
|
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'equipment' | 'progress';
|
|
|
|
type CampDialogue = {
|
|
id: string;
|
|
title: string;
|
|
availableAfterBattleIds: string[];
|
|
availableDuringSteps?: CampaignStep[];
|
|
unitIds: [string, string];
|
|
bondId: string;
|
|
rewardExp: number;
|
|
lines: string[];
|
|
choices: CampDialogueChoice[];
|
|
};
|
|
|
|
type CampDialogueChoice = {
|
|
id: string;
|
|
label: string;
|
|
response: string;
|
|
rewardExp: number;
|
|
};
|
|
|
|
type CampVisitDefinition = {
|
|
id: string;
|
|
title: string;
|
|
location: string;
|
|
availableAfterBattleIds: string[];
|
|
availableDuringSteps?: CampaignStep[];
|
|
description: string;
|
|
bondId?: string;
|
|
lines: string[];
|
|
choices: CampVisitChoice[];
|
|
};
|
|
|
|
type CampVisitChoice = {
|
|
id: string;
|
|
label: string;
|
|
response: string;
|
|
bondExp?: number;
|
|
gold?: number;
|
|
itemRewards?: string[];
|
|
};
|
|
|
|
type CampTabButtonView = {
|
|
tab: CampTab;
|
|
bg: Phaser.GameObjects.Rectangle;
|
|
text: Phaser.GameObjects.Text;
|
|
};
|
|
|
|
type CampSupplyDefinition = {
|
|
label: string;
|
|
usableId: string;
|
|
title: string;
|
|
description: string;
|
|
healHp: number;
|
|
bondExp: number;
|
|
};
|
|
|
|
type MerchantItemDefinition = {
|
|
label: string;
|
|
title: string;
|
|
description: string;
|
|
price: number;
|
|
};
|
|
|
|
type SortieChecklistItem = {
|
|
label: string;
|
|
complete: boolean;
|
|
detail: string;
|
|
priority?: 'required' | 'recommended';
|
|
};
|
|
|
|
type SortieUnitTacticalSummary = {
|
|
statLine: string;
|
|
equipmentLine: string;
|
|
bondLine: string;
|
|
terrainLine: string;
|
|
};
|
|
|
|
type SortieFocusedUnitSummary = SortieUnitTacticalSummary & {
|
|
id: string;
|
|
name: string;
|
|
selected: boolean;
|
|
required: boolean;
|
|
recommended: boolean;
|
|
recommendationReason: string | null;
|
|
className: string;
|
|
classRole: string;
|
|
formationRole: SortieFormationRole;
|
|
reserveTrainingAssigned: boolean;
|
|
reserveTrainingFocusId: CampaignReserveTrainingFocusId;
|
|
reserveTrainingFocusLabel: string;
|
|
terrainScore: number;
|
|
terrainGrade: string;
|
|
equipment: {
|
|
slot: EquipmentSlot;
|
|
label: string;
|
|
itemName: string;
|
|
level: number;
|
|
exp: number;
|
|
next: number;
|
|
rank: string;
|
|
bonusText: string;
|
|
}[];
|
|
};
|
|
|
|
type SortieTerrainCounts = Partial<Record<TerrainType, number>>;
|
|
|
|
type SortieRecommendation = {
|
|
unitId: string;
|
|
reason: string;
|
|
role?: SortieFormationRole;
|
|
};
|
|
|
|
type SortieClassRecommendation = {
|
|
label: string;
|
|
classKeys: UnitClassKey[];
|
|
reason: string;
|
|
};
|
|
|
|
type SortieRuleDefinition = {
|
|
maxUnits: number;
|
|
requiredUnitIds?: string[];
|
|
excludedUnitIds?: string[];
|
|
recommended: SortieRecommendation[];
|
|
recommendedClasses?: SortieClassRecommendation[];
|
|
deploymentSlots?: SortieDeploymentSlot[];
|
|
note: string;
|
|
};
|
|
|
|
type SortieUnitAvailability = {
|
|
available: boolean;
|
|
label: string;
|
|
reason: string;
|
|
tone: 'ready' | 'selected' | 'recommended' | 'reserve' | 'disabled';
|
|
};
|
|
|
|
type SortieFormationSlot = {
|
|
role: SortieFormationRole;
|
|
label: string;
|
|
description: string;
|
|
unitNames: string[];
|
|
};
|
|
|
|
type SortiePlanSummary = {
|
|
selectedCount: number;
|
|
maxCount: number;
|
|
terrainScore: number;
|
|
terrainGrade: string;
|
|
activeBondCount: number;
|
|
recruitedCount: number;
|
|
selectedRecruitedCount: number;
|
|
reserveCount: number;
|
|
recommendedSelectedCount: number;
|
|
recommendedTotal: number;
|
|
deploymentLine: string;
|
|
recommendationLine: string;
|
|
recruitedLine: string;
|
|
reserveLine: string;
|
|
reserveTrainingLine: string;
|
|
reserveTrainingPreviewLine: string;
|
|
reserveTrainingFocusId: CampaignReserveTrainingFocusId;
|
|
reserveTrainingFocusLabel: string;
|
|
reserveTrainingExpPreview: number;
|
|
reserveTrainingEquipmentPreview: number;
|
|
reserveTrainingBondPreview: number;
|
|
objectiveLine: string;
|
|
formationSlots: SortieFormationSlot[];
|
|
warningLine: string;
|
|
warnings: string[];
|
|
};
|
|
|
|
type CampaignTimelineChapter = {
|
|
id: string;
|
|
title: string;
|
|
period: string;
|
|
description: string;
|
|
battleIds: BattleScenarioId[];
|
|
nextHints: string[];
|
|
};
|
|
|
|
type CampSceneData = {
|
|
openSortiePrep?: boolean;
|
|
skipIntroStory?: boolean;
|
|
};
|
|
|
|
const portraitByUnitId: Record<string, PortraitKey> = {
|
|
'liu-bei': 'liuBei',
|
|
'guan-yu': 'guanYu',
|
|
'zhang-fei': 'zhangFei',
|
|
'zhuge-liang': 'zhugeLiang',
|
|
'zhao-yun': 'zhaoYun',
|
|
'jiang-wei': 'jiangWei'
|
|
};
|
|
|
|
const campSupplies: CampSupplyDefinition[] = [
|
|
{
|
|
label: '콩',
|
|
usableId: 'bean',
|
|
title: '콩',
|
|
description: '선택 장수의 병력을 12 회복합니다.',
|
|
healHp: 12,
|
|
bondExp: 0
|
|
},
|
|
{
|
|
label: '탁주',
|
|
usableId: 'wine',
|
|
title: '탁주',
|
|
description: '선택 장수의 병력을 8 회복하고 연결된 공명 경험치를 2 올립니다.',
|
|
healHp: 8,
|
|
bondExp: 2
|
|
},
|
|
{
|
|
label: '상처약',
|
|
usableId: 'salve',
|
|
title: '상처약',
|
|
description: '선택 장수의 병력을 22 회복합니다.',
|
|
healHp: 22,
|
|
bondExp: 0
|
|
}
|
|
];
|
|
|
|
const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, supply]));
|
|
|
|
const merchantItems: MerchantItemDefinition[] = [
|
|
{
|
|
label: '콩',
|
|
title: '콩',
|
|
description: '값싸고 가벼운 회복 도구입니다.',
|
|
price: 40
|
|
},
|
|
{
|
|
label: '탁주',
|
|
title: '탁주',
|
|
description: '병력 회복과 작은 공명 상승에 유용합니다.',
|
|
price: 70
|
|
},
|
|
{
|
|
label: '상처약',
|
|
title: '상처약',
|
|
description: '큰 부상을 빠르게 회복합니다.',
|
|
price: 110
|
|
}
|
|
];
|
|
|
|
const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
|
{
|
|
id: 'yellow-turban',
|
|
title: '황건적 토벌',
|
|
period: '의용군의 시작',
|
|
description: '탁현에서 뜻을 세운 세 형제가 황건적을 토벌하며 이름과 명분을 얻는 초반 장입니다.',
|
|
battleIds: [
|
|
'first-battle-zhuo-commandery',
|
|
'second-battle-yellow-turban-pursuit',
|
|
'third-battle-guangzong-road',
|
|
'fourth-battle-guangzong-camp'
|
|
],
|
|
nextHints: ['의용군 명성', '세 형제 공명', '황건 토벌 공적']
|
|
},
|
|
{
|
|
id: 'anti-dong-gongsun',
|
|
title: '반동탁과 공손찬 의탁',
|
|
period: '난세의 큰 흐름',
|
|
description: '반동탁 연합의 바깥에서 공손찬 진영의 신뢰를 얻고, 서주 원군 요청으로 이어지는 장입니다.',
|
|
battleIds: ['fifth-battle-sishui-vanguard', 'sixth-battle-jieqiao-relief'],
|
|
nextHints: ['공손찬 신뢰', '북방 전선', '서주 원군 요청']
|
|
},
|
|
{
|
|
id: 'xuzhou-lubu',
|
|
title: '서주 인수와 여포의 그림자',
|
|
period: '얻고 잃는 땅',
|
|
description: '도겸에게 서주를 맡고, 여포를 의탁시킨 뒤 내부 불안과 배신으로 서주를 잃는 장입니다.',
|
|
battleIds: [
|
|
'seventh-battle-xuzhou-rescue',
|
|
'eighth-battle-xiaopei-supply-road',
|
|
'ninth-battle-xuzhou-gate-night-raid',
|
|
'tenth-battle-xuzhou-breakout'
|
|
],
|
|
nextHints: ['간옹·미축 합류', '서주 민심', '여포와의 긴장']
|
|
},
|
|
{
|
|
id: 'cao-refuge-xiapi',
|
|
title: '조조 의탁과 하비 결전',
|
|
period: '의탁 속의 경계',
|
|
description: '조조에게 몸을 의탁하지만 독자 명분을 잃지 않으며, 하비에서 여포의 운명을 마무리하는 장입니다.',
|
|
battleIds: [
|
|
'eleventh-battle-xudu-refuge-road',
|
|
'twelfth-battle-xiapi-outer-siege',
|
|
'thirteenth-battle-xiapi-final'
|
|
],
|
|
nextHints: ['조조군 감시', '하비 포위', '여포 격파']
|
|
},
|
|
{
|
|
id: 'break-cao-yuan',
|
|
title: '조조 이탈과 원소 의탁',
|
|
period: '다시 북쪽으로',
|
|
description: '조조의 장막을 벗어나 원소에게 향하지만, 객군의 한계와 다음 의탁의 필요를 확인하는 장입니다.',
|
|
battleIds: ['fourteenth-battle-cao-break', 'fifteenth-battle-yuan-refuge-road'],
|
|
nextHints: ['손건 합류', '원소 접선', '객군의 불안']
|
|
},
|
|
{
|
|
id: 'liu-biao-wolong',
|
|
title: '유표 의탁과 제갈량 영입',
|
|
period: '와룡을 찾아',
|
|
description: '형주에 머물며 조운이 재합류하고, 와룡의 단서를 좇아 제갈량을 얻는 전환점입니다.',
|
|
battleIds: ['sixteenth-battle-liu-biao-refuge', 'seventeenth-battle-wolong-visit-road'],
|
|
nextHints: ['조운 재합류', '와룡 단서', '천하삼분']
|
|
},
|
|
{
|
|
id: 'red-cliffs',
|
|
title: '적벽대전',
|
|
period: '강동의 바람',
|
|
description: '제갈량의 계책과 강동의 힘이 맞물려 조조의 대군을 막아서는 큰 전환 장입니다.',
|
|
battleIds: [
|
|
'eighteenth-battle-bowang-ambush',
|
|
'nineteenth-battle-changban-refuge',
|
|
'twentieth-battle-jiangdong-envoy',
|
|
'twenty-first-battle-red-cliffs-vanguard',
|
|
'twenty-second-battle-red-cliffs-fire'
|
|
],
|
|
nextHints: ['강동 사절', '화공 준비', '조조 남하']
|
|
},
|
|
{
|
|
id: 'jing-yi',
|
|
title: '형주·익주 확보',
|
|
period: '근본을 세우다',
|
|
description: '형주와 익주를 기반으로 삼아 유비군이 유랑군에서 국가의 뿌리로 바뀌는 장입니다.',
|
|
battleIds: [
|
|
'twenty-third-battle-jingzhou-south-entry',
|
|
'twenty-fourth-battle-guiyang-persuasion',
|
|
'twenty-fifth-battle-wuling-mountain-road',
|
|
'twenty-sixth-battle-changsha-veteran',
|
|
'twenty-seventh-battle-yizhou-relief-road',
|
|
'twenty-eighth-battle-fu-pass-entry',
|
|
'twenty-ninth-battle-luo-outer-wall',
|
|
'thirtieth-battle-luofeng-ambush',
|
|
'thirty-first-battle-luo-main-gate',
|
|
'thirty-second-battle-mianzhu-gate',
|
|
'thirty-third-battle-chengdu-surrender'
|
|
],
|
|
nextHints: ['형주 방위', '익주 진입', '성도 항복']
|
|
},
|
|
{
|
|
id: 'shu-han',
|
|
title: '촉한 건국',
|
|
period: '한실을 잇는 깃발',
|
|
description: '여러 전장을 지나 촉한을 세우고 북벌을 바라보는 장기 목표입니다.',
|
|
battleIds: [
|
|
'thirty-fourth-battle-jiameng-pass',
|
|
'thirty-fifth-battle-yangping-scout',
|
|
'thirty-sixth-battle-dingjun-vanguard',
|
|
'thirty-seventh-battle-hanzhong-decisive',
|
|
'thirty-eighth-battle-jing-defense',
|
|
'thirty-ninth-battle-fan-castle-vanguard',
|
|
'fortieth-battle-han-river-flood',
|
|
'forty-first-battle-fan-castle-siege',
|
|
'forty-second-battle-jing-rear-crisis',
|
|
'forty-third-battle-gongan-collapse',
|
|
'forty-fourth-battle-maicheng-isolation',
|
|
'forty-fifth-battle-yiling-vanguard',
|
|
'forty-sixth-battle-yiling-fire',
|
|
'forty-seventh-battle-nanzhong-stabilization',
|
|
'forty-eighth-battle-meng-huo-main-force',
|
|
'forty-ninth-battle-meng-huo-second-capture',
|
|
'fiftieth-battle-meng-huo-third-capture',
|
|
'fifty-first-battle-meng-huo-fourth-capture',
|
|
'fifty-second-battle-meng-huo-fifth-capture',
|
|
'fifty-third-battle-meng-huo-sixth-capture',
|
|
'fifty-fourth-battle-meng-huo-final-capture'
|
|
],
|
|
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금']
|
|
},
|
|
{
|
|
id: 'northern-campaign',
|
|
title: '북벌 준비',
|
|
period: '출사표를 향해',
|
|
description: '남중 평정으로 뒤를 안정시킨 뒤, 한중 창고와 출전 무장 편성을 정비해 첫 북벌을 준비하는 장입니다.',
|
|
battleIds: [
|
|
'fifty-fifth-battle-northern-qishan-road',
|
|
'fifty-sixth-battle-tianshui-advance',
|
|
'fifty-seventh-battle-jieting-crisis',
|
|
'fifty-eighth-battle-qishan-retreat',
|
|
'fifty-ninth-battle-chencang-siege',
|
|
'sixtieth-battle-wudu-yinping',
|
|
'sixty-first-battle-hanzhong-rain-defense',
|
|
'sixty-second-battle-qishan-renewed-offensive',
|
|
'sixty-third-battle-lucheng-pursuit',
|
|
'sixty-fourth-battle-weishui-camps',
|
|
'sixty-fifth-battle-weishui-northbank',
|
|
'sixty-sixth-battle-wuzhang-final'
|
|
],
|
|
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어', '기산 재출정', '노성 추격', '위수 진영', '위수 북안', '오장원 최종전']
|
|
}
|
|
];
|
|
|
|
const campBattleIds = {
|
|
first: defaultBattleScenario.id,
|
|
second: 'second-battle-yellow-turban-pursuit',
|
|
third: 'third-battle-guangzong-road',
|
|
fourth: 'fourth-battle-guangzong-camp',
|
|
fifth: 'fifth-battle-sishui-vanguard',
|
|
sixth: 'sixth-battle-jieqiao-relief',
|
|
seventh: 'seventh-battle-xuzhou-rescue',
|
|
eighth: 'eighth-battle-xiaopei-supply-road',
|
|
ninth: 'ninth-battle-xuzhou-gate-night-raid',
|
|
tenth: 'tenth-battle-xuzhou-breakout',
|
|
eleventh: 'eleventh-battle-xudu-refuge-road',
|
|
twelfth: 'twelfth-battle-xiapi-outer-siege',
|
|
thirteenth: 'thirteenth-battle-xiapi-final',
|
|
fourteenth: 'fourteenth-battle-cao-break',
|
|
fifteenth: 'fifteenth-battle-yuan-refuge-road',
|
|
sixteenth: 'sixteenth-battle-liu-biao-refuge',
|
|
seventeenth: 'seventeenth-battle-wolong-visit-road',
|
|
eighteenth: 'eighteenth-battle-bowang-ambush',
|
|
nineteenth: 'nineteenth-battle-changban-refuge',
|
|
twentieth: 'twentieth-battle-jiangdong-envoy',
|
|
twentyFirst: 'twenty-first-battle-red-cliffs-vanguard',
|
|
twentySecond: 'twenty-second-battle-red-cliffs-fire',
|
|
twentyThird: 'twenty-third-battle-jingzhou-south-entry',
|
|
twentyFourth: 'twenty-fourth-battle-guiyang-persuasion',
|
|
twentyFifth: 'twenty-fifth-battle-wuling-mountain-road',
|
|
twentySixth: 'twenty-sixth-battle-changsha-veteran',
|
|
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',
|
|
thirtyFirst: 'thirty-first-battle-luo-main-gate',
|
|
thirtySecond: 'thirty-second-battle-mianzhu-gate',
|
|
thirtyThird: 'thirty-third-battle-chengdu-surrender',
|
|
thirtyFourth: 'thirty-fourth-battle-jiameng-pass',
|
|
thirtyFifth: 'thirty-fifth-battle-yangping-scout',
|
|
thirtySixth: 'thirty-sixth-battle-dingjun-vanguard',
|
|
thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive',
|
|
thirtyEighth: 'thirty-eighth-battle-jing-defense',
|
|
thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard',
|
|
fortieth: 'fortieth-battle-han-river-flood',
|
|
fortyFirst: 'forty-first-battle-fan-castle-siege',
|
|
fortySecond: 'forty-second-battle-jing-rear-crisis',
|
|
fortyThird: 'forty-third-battle-gongan-collapse',
|
|
fortyFourth: 'forty-fourth-battle-maicheng-isolation',
|
|
fortyFifth: 'forty-fifth-battle-yiling-vanguard',
|
|
fortySixth: 'forty-sixth-battle-yiling-fire',
|
|
fortySeventh: 'forty-seventh-battle-nanzhong-stabilization',
|
|
fortyEighth: 'forty-eighth-battle-meng-huo-main-force',
|
|
fortyNinth: 'forty-ninth-battle-meng-huo-second-capture',
|
|
fiftieth: 'fiftieth-battle-meng-huo-third-capture',
|
|
fiftyFirst: 'fifty-first-battle-meng-huo-fourth-capture',
|
|
fiftySecond: 'fifty-second-battle-meng-huo-fifth-capture',
|
|
fiftyThird: 'fifty-third-battle-meng-huo-sixth-capture',
|
|
fiftyFourth: 'fifty-fourth-battle-meng-huo-final-capture',
|
|
fiftyFifth: 'fifty-fifth-battle-northern-qishan-road',
|
|
fiftySixth: 'fifty-sixth-battle-tianshui-advance',
|
|
fiftySeventh: 'fifty-seventh-battle-jieting-crisis',
|
|
fiftyEighth: 'fifty-eighth-battle-qishan-retreat',
|
|
fiftyNinth: 'fifty-ninth-battle-chencang-siege',
|
|
sixtieth: 'sixtieth-battle-wudu-yinping',
|
|
sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense',
|
|
sixtySecond: 'sixty-second-battle-qishan-renewed-offensive',
|
|
sixtyThird: 'sixty-third-battle-lucheng-pursuit',
|
|
sixtyFourth: 'sixty-fourth-battle-weishui-camps',
|
|
sixtyFifth: 'sixty-fifth-battle-weishui-northbank',
|
|
sixtySixth: 'sixty-sixth-battle-wuzhang-final'
|
|
} as const;
|
|
|
|
const defaultRequiredSortieUnitIds = ['liu-bei'];
|
|
const foundingSortieUnitIds = new Set(['liu-bei', 'guan-yu', 'zhang-fei']);
|
|
const maxSortieUnits = 6;
|
|
const sortieFormationSlotDefinitions: Omit<SortieFormationSlot, 'unitNames'>[] = [
|
|
{ role: 'front', label: '전열', description: '길목을 막고 반격을 받아냄' },
|
|
{ role: 'flank', label: '돌파', description: '측면 기동과 추격 담당' },
|
|
{ role: 'support', label: '후원', description: '원거리, 책략, 보급 지원' },
|
|
{ role: 'reserve', label: '예비', description: '빈틈 보강과 공명 연결' }
|
|
];
|
|
const campCompanionRoleDefinitions: Omit<SortieFormationSlot, 'unitNames'>[] = [
|
|
{ role: 'front', label: '대표', description: '대화의 중심과 군영 결정을 맡음' },
|
|
{ role: 'flank', label: '현장', description: '다음 장면의 현장 판단과 증언 담당' },
|
|
{ role: 'support', label: '보좌', description: '문서, 보급, 책략 기록을 보탬' },
|
|
{ role: 'reserve', label: '기록', description: '남은 기록과 공명 정리 담당' }
|
|
];
|
|
|
|
const defaultSortieRule: SortieRuleDefinition = {
|
|
maxUnits: maxSortieUnits,
|
|
requiredUnitIds: defaultRequiredSortieUnitIds,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '패배 조건인 주장이며 공명 중심입니다.' }
|
|
],
|
|
note: '유비 생존을 중심에 두고 전열, 돌파, 후원을 고르게 배치하십시오.'
|
|
};
|
|
|
|
const defaultCampSortieRule: SortieRuleDefinition = {
|
|
maxUnits: maxSortieUnits,
|
|
requiredUnitIds: defaultRequiredSortieUnitIds,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '군영 의정의 중심이며 마지막 결정을 정리합니다.' }
|
|
],
|
|
note: '유비 참석을 중심에 두고 대표, 현장, 보좌 역할을 고르게 정리하십시오.'
|
|
};
|
|
|
|
const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefinition>> = {
|
|
'second-battle-yellow-turban-pursuit': {
|
|
maxUnits: 3,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '마을 안정과 지휘를 맡습니다.' },
|
|
{ unitId: 'guan-yu', reason: '숲길 전열을 안정적으로 밀 수 있습니다.' },
|
|
{ unitId: 'zhang-fei', reason: '잔당 추격과 막타 돌파에 강합니다.' }
|
|
],
|
|
note: '첫 추격전은 추천 표시를 보고 세 형제를 모두 넣어 보는 전장입니다. 유비를 뒤에 두고 관우와 장비로 길목을 여십시오.'
|
|
},
|
|
'third-battle-guangzong-road': {
|
|
maxUnits: 3,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '강가 길목에서 부대를 묶어 줍니다.' },
|
|
{ unitId: 'guan-yu', reason: '요새와 숲의 방어선을 버팁니다.' },
|
|
{ unitId: 'zhang-fei', reason: '좁은 길의 적 전령을 빠르게 압박합니다.' }
|
|
],
|
|
note: '강과 요새가 많은 길목이라 전열 유지와 빠른 추격이 모두 필요합니다.'
|
|
},
|
|
'fourth-battle-guangzong-camp': {
|
|
maxUnits: 3,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '장각 격파까지 전선을 지휘합니다.' },
|
|
{ unitId: 'guan-yu', reason: '요새 방어선을 뚫는 전열 핵심입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '본영 측면을 흔드는 압박 담당입니다.' }
|
|
],
|
|
note: '황건 본영전은 세 형제 전원 출전으로 공명 피해와 전선 안정성을 확보하십시오.'
|
|
},
|
|
'fifth-battle-sishui-vanguard': {
|
|
maxUnits: 3,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '반동탁 연합에서 이름을 세울 주장입니다.' },
|
|
{ unitId: 'guan-yu', reason: '전초 요새 주변을 안정적으로 장악합니다.' },
|
|
{ unitId: 'zhang-fei', reason: '호진의 전열을 빠르게 흔듭니다.' }
|
|
],
|
|
note: '사수관 전초전은 중앙 길을 서두르되 숲길 척후를 놓치지 않는 편성이 좋습니다.'
|
|
},
|
|
'sixth-battle-jieqiao-relief': {
|
|
maxUnits: 3,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '공손찬 원군로의 신뢰를 지켜야 합니다.' },
|
|
{ unitId: 'guan-yu', reason: '강가와 진영 지형에서 방어 적성이 좋습니다.' },
|
|
{ unitId: 'zhang-fei', reason: '원소군 별동대 추격에 적합합니다.' }
|
|
],
|
|
note: '계교 원군로는 빠른 압박보다 길목 안정이 중요합니다. 유비를 보호 대상처럼 뒤에 두고 관우와 장비로 먼저 맞받으십시오.'
|
|
},
|
|
'seventh-battle-xuzhou-rescue': {
|
|
maxUnits: 4,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '서주 원군 요청의 중심 인물입니다.' },
|
|
{ unitId: 'guan-yu', reason: '하후돈 선봉을 받아낼 전열 핵심입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '조조군 기병을 압박할 돌파 역할입니다.' }
|
|
],
|
|
note: '서주 구원전부터 네 번째 출전 칸이 열립니다. 주력 셋을 고정한 뒤 추천 사유를 보고 보조 역할을 더하십시오.'
|
|
},
|
|
'eighth-battle-xiaopei-supply-road': {
|
|
maxUnits: 5,
|
|
recommended: [
|
|
{ unitId: 'guan-yu', reason: '보급로 앞 길목에서 고순 본대와 기병 압박을 받아냅니다.' },
|
|
{ unitId: 'jian-yong', reason: '병법서와 책략 보조가 잘 맞아 화계, 응급, 격려를 모두 활용하기 좋습니다.' },
|
|
{ unitId: 'mi-zhu', reason: '창고 주변 보급과 회복품 운용으로 장기전을 안정시킵니다.' },
|
|
{ unitId: 'zhang-fei', reason: '남쪽 길의 습격병과 기병을 빠르게 끊어냅니다.' },
|
|
{ unitId: 'liu-bei', reason: '후방에서 새 합류 무장과 전열의 균형을 잡습니다.' }
|
|
],
|
|
note: '새로 합류한 간옹과 미축을 바로 시험하기 좋은 전장입니다. 간옹은 병법서와 책략, 미축은 보급과 회복 역할로 비교해 편성하십시오.'
|
|
},
|
|
'ninth-battle-xuzhou-gate-night-raid': {
|
|
maxUnits: 5,
|
|
recommended: [
|
|
{ unitId: 'guan-yu', reason: '성문 정면에서 조표 수비병과 기병 압박을 받아낼 핵심 전열입니다.' },
|
|
{ unitId: 'jian-yong', reason: '화계와 격려로 성벽 궁병과 기병 돌입을 늦출 수 있습니다.' },
|
|
{ unitId: 'mi-zhu', reason: '8전에서 얻은 보급품과 군량 주머니를 성문 장기전 회복으로 연결합니다.' },
|
|
{ unitId: 'zhang-fei', reason: '남쪽 길로 파고드는 기병과 배반병을 빠르게 끊어냅니다.' },
|
|
{ unitId: 'liu-bei', reason: '서주 성문을 되찾는 명분의 중심이며 패배 조건입니다.' }
|
|
],
|
|
note: '성문 야습은 8전 보상과 새 합류 무장을 실제로 써 보는 전장입니다. 관우는 정면, 장비는 측면, 간옹과 미축은 책략과 보급으로 뒤를 받치게 편성하십시오.'
|
|
},
|
|
'tenth-battle-xuzhou-breakout': {
|
|
maxUnits: 5,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '퇴각 지휘의 중심이자 패배 조건이므로 후방 생존을 우선합니다.' },
|
|
{ unitId: 'guan-yu', reason: '여포군 추격대를 받아내며 유비와 보급 수레 뒤를 지킵니다.' },
|
|
{ unitId: 'zhang-fei', reason: '실책을 만회하고 남쪽 퇴로를 여는 돌파 담당입니다.' },
|
|
{ unitId: 'jian-yong', reason: '화계와 격려로 기병 돌입을 늦추고 퇴로 판단을 보조합니다.' },
|
|
{ unitId: 'mi-zhu', reason: '피난민과 보급 수레를 보존하며 회복품 운용을 안정시킵니다.' }
|
|
],
|
|
note: '서주 탈출전은 성을 되찾는 전투가 아니라 퇴로를 여는 전투입니다. 관우는 추격을 막고, 장비는 남쪽 길을 열며, 간옹과 미축은 책략과 보급으로 손실을 줄이는 편성이 좋습니다.'
|
|
},
|
|
'eleventh-battle-xudu-refuge-road': {
|
|
maxUnits: 5,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '조조가 지켜보는 의탁의 중심입니다. 후방에서 생존과 지휘를 맡기십시오.' },
|
|
{ unitId: 'guan-yu', reason: '기령 본대와 중앙 길목에서 맞설 정면 전열입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '남쪽 마을 약탈병과 기병 돌파를 끊는 측면 압박에 좋습니다.' },
|
|
{ unitId: 'jian-yong', reason: '감시 속 전황 보고와 책략 보조로 적 기병의 속도를 늦춥니다.' },
|
|
{ unitId: 'mi-zhu', reason: '길목 마을 보급로를 지키고 회복품 운용을 안정시킵니다.' }
|
|
],
|
|
note: '허도 입성로는 조조군 감시 아래 치르는 첫 시험입니다. 마을 보급로를 안정시키고 기령을 격파해 군율과 힘을 함께 증명하십시오.'
|
|
},
|
|
'twelfth-battle-xiapi-outer-siege': {
|
|
maxUnits: 5,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '조조 휘하 작전에서도 유비군의 명분을 지켜야 합니다. 후방 지휘와 생존이 우선입니다.' },
|
|
{ unitId: 'guan-yu', reason: '중앙 둑길에서 장료 선봉을 받아낼 정면 전열입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '여포군 기병 돌파를 남쪽 길목에서 끊는 측면 차단에 좋습니다.' },
|
|
{ unitId: 'jian-yong', reason: '진궁과 궁병 압박을 늦추고 조조군 보고 문서를 조율합니다.' },
|
|
{ unitId: 'mi-zhu', reason: '둑길 보급 진영을 지키고 회복품 운용을 안정시킵니다.' }
|
|
],
|
|
note: '하비 외곽전은 강줄기와 둑길이 전장을 나눕니다. 보급 진영을 먼저 안정시키고, 장비로 기병 돌파를 끊은 뒤 장료 선봉을 압박하십시오.'
|
|
},
|
|
'thirteenth-battle-xiapi-final': {
|
|
maxUnits: 5,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '여포의 끝과 조조 앞 명분을 함께 짊어진 후방 지휘관입니다.' },
|
|
{ unitId: 'guan-yu', reason: '하비 성문 요새의 전열을 정면에서 밀어낼 핵심입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '수문 둑길로 빠지는 여포군 기병의 돌파를 끊는 역할입니다.' },
|
|
{ unitId: 'jian-yong', reason: '진궁의 책략과 조조군의 시선을 살피며 전황 보고를 맡습니다.' },
|
|
{ unitId: 'mi-zhu', 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: '허도 밖으로 나가는 장기 행군에 필요한 회복품과 보급선을 맡습니다.' },
|
|
{ unitId: 'jian-yong', reason: '간옹의 말과 책략 지원은 감시망을 흔들고 관문 압박을 돕습니다.' }
|
|
],
|
|
note: '서주 재기는 조조의 감시망을 끊는 전투입니다. 손건으로 역참 문서를 잡고, 관우로 관문을 누르며, 장비로 남쪽 추격 기병을 막는 편성이 안정적입니다.'
|
|
},
|
|
'fifteenth-battle-yuan-refuge-road': {
|
|
maxUnits: 6,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '원소에게 의탁할 명분과 생존 조건을 모두 짊어지는 후방 지휘관입니다.' },
|
|
{ unitId: 'guan-yu', reason: '중앙 고갯길과 접선지 앞 수비선을 정면에서 밀어낼 전열 핵심입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '남쪽으로 붙는 추격 기병을 받아쳐 고갯길 후방을 끊는 역할입니다.' },
|
|
{ unitId: 'sun-qian', reason: '원소 사자와의 접선 문서를 정리해 접선지 확보 목표에 직접 힘을 보탭니다.' },
|
|
{ unitId: 'jian-yong', reason: '간옹의 말과 책략 지원은 채양 추격대의 전열을 늦추는 데 유리합니다.' },
|
|
{ unitId: 'mi-zhu', reason: '긴 도주전에서 회복품과 보급선을 유지해 다음 의탁로까지 피해를 줄입니다.' }
|
|
],
|
|
note: '원소 의탁로는 중앙 고갯길을 열고 채양 본진 봉화대를 끊은 뒤 북동쪽 접선지로 오르는 전투입니다. 장비가 남쪽 기병을 막고, 관우가 고갯길을 열며, 손건과 간옹이 접선 문서와 책략으로 전열을 받치는 편성이 안정적입니다.'
|
|
},
|
|
'sixteenth-battle-liu-biao-refuge': {
|
|
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: '유표 의탁로는 조운의 기동력을 처음 체감하는 전투입니다. 중앙 접선로를 조운이 열고, 관우와 장비가 관문 수비선을 나누어 밀며, 손건과 미축이 의탁 명분과 보급을 받치는 편성이 안정적입니다.'
|
|
},
|
|
'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: '융중 방문로는 제갈량 영입 직전의 예행 전투입니다. 문사 접선지를 먼저 확인하고, 조운으로 숲길 척후를 끊은 뒤, 관우와 장비가 초가 앞 감시선을 정리하는 편성이 안정적입니다.'
|
|
},
|
|
'eighteenth-battle-bowang-ambush': {
|
|
maxUnits: 6,
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '후방에서 전열을 묶는 지휘 주체이며 패배 조건입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '박망파 화공 매복과 혼란 책략을 맡는 핵심입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '추격 기병을 끊고 숲길 측면을 빠르게 흔듭니다.' },
|
|
{ unitId: 'guan-yu', reason: '좁은 길목에서 하후돈 친위와 보병 전열을 받아냅니다.' },
|
|
{ unitId: 'zhang-fei', reason: '매복 후 역습으로 적 선봉을 밀어내는 돌파 역할입니다.' },
|
|
{ unitId: 'sun-qian', reason: '전후 보고와 강동 사절로 이어질 명분을 정리합니다.' }
|
|
],
|
|
note: '제갈량의 첫 실전 계책입니다. 유비와 제갈량을 중심으로 조운이 기병을 끊고, 관우와 장비가 숲길 길목을 눌러 화공 매복지를 안정시키는 편성이 좋습니다.'
|
|
},
|
|
'nineteenth-battle-changban-refuge': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhao-yun'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '피난 행렬의 중심이며 패배 조건입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '피난로를 오가며 흩어진 병력과 백성을 붙잡는 핵심 기동 장수입니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '장판교와 좁은 길목에서 추격 기병을 늦추는 역할입니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '장비가 버티는 다리목 전열을 안정적으로 받쳐 줍니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '피난로 판단과 강동 사절 명분을 전투 뒤로 연결합니다.' },
|
|
{ unitId: 'sun-qian', role: 'support', reason: '강동에 보낼 기록과 외교 명분을 정리합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '피난 지휘', classKeys: ['lord'], reason: '유비가 후방에서 무너지지 않아야 피난 행렬이 유지됩니다.' },
|
|
{ label: '기동 구원', classKeys: ['cavalry'], reason: '조운 같은 기동 장수가 마을과 다리목 사이를 오가야 합니다.' },
|
|
{ label: '길목 전열', classKeys: ['infantry', 'spearman'], reason: '장판교 길목은 튼튼한 전열이 있어야 추격 기병을 늦출 수 있습니다.' },
|
|
{ label: '책략/기록', classKeys: ['strategist'], reason: '제갈량과 손건은 전투 결과를 다음 강동 사절로 이어 줍니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 22, role: 'reserve', unitId: 'liu-bei', label: '후방 피난 지휘' },
|
|
{ x: 5, y: 20, role: 'front', unitId: 'guan-yu', label: '전열 받침' },
|
|
{ x: 6, y: 22, role: 'front', unitId: 'zhang-fei', label: '장판교 전열' },
|
|
{ x: 8, y: 20, role: 'flank', unitId: 'zhao-yun', label: '피난로 기동' },
|
|
{ x: 5, y: 23, role: 'support', unitId: 'zhuge-liang', label: '퇴로 판단' },
|
|
{ x: 6, y: 24, role: 'support', unitId: 'sun-qian', label: '사절 기록' }
|
|
],
|
|
note: '장판파 피난로는 유비와 조운 생존이 핵심입니다. 조운으로 중앙 피난로를 열고, 장비와 관우가 장판교 길목을 막아 제갈량과 손건이 강동에 전할 근거를 남기는 편성이 좋습니다.'
|
|
},
|
|
'twentieth-battle-jiangdong-envoy': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang', 'sun-qian'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '사절을 보내는 결단과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '강동 설득 논리를 지키는 핵심 인물이며 반드시 보호해야 합니다.' },
|
|
{ unitId: 'sun-qian', role: 'support', reason: '사절 문서와 회담 기록을 맡는 생존 목표입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '추격 기병을 끊고 사절의 측면을 지키는 데 좋습니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '강변 길목에서 보병 전열과 포구 수비대를 받아냅니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '포구 수비대를 빠르게 흔들어 사절단이 길게 물리지 않게 합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '사절 보호', classKeys: ['strategist'], reason: '제갈량과 손건을 안전하게 유지해야 강동 회담으로 이어집니다.' },
|
|
{ label: '후방 지휘', classKeys: ['lord'], reason: '유비가 퇴로를 붙들어야 사절단이 끊기지 않습니다.' },
|
|
{ label: '기병 차단', classKeys: ['cavalry'], reason: '조운 같은 기동 장수가 중간 추격 기병을 먼저 끊어야 합니다.' },
|
|
{ label: '길목 전열', classKeys: ['infantry', 'spearman'], reason: '강변 길목과 포구 수비대를 안정적으로 받아낼 전열이 필요합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 22, role: 'reserve', unitId: 'liu-bei', label: '후방 지휘' },
|
|
{ x: 5, y: 20, role: 'front', unitId: 'guan-yu', label: '강변 전열' },
|
|
{ x: 6, y: 23, role: 'front', unitId: 'zhang-fei', label: '포구 돌파' },
|
|
{ x: 8, y: 21, role: 'flank', unitId: 'zhao-yun', label: '측면 호위' },
|
|
{ x: 6, y: 21, role: 'support', unitId: 'zhuge-liang', label: '회담 논리' },
|
|
{ x: 4, y: 24, role: 'support', unitId: 'sun-qian', label: '사절 문서' }
|
|
],
|
|
note: '강동 사절로는 제갈량과 손건을 무사히 나루까지 보내는 전투입니다. 유비는 후방 지휘, 조운은 측면 호위, 관우와 장비는 강변 길목과 포구 돌파를 맡는 편성이 안정적입니다.'
|
|
},
|
|
'twenty-first-battle-red-cliffs-vanguard': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '손유 동맹의 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '바람과 화공을 잇는 계책 담당이며 반드시 생존해야 합니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '강안의 기병과 척후를 빠르게 끊어 포구를 안정시킵니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '배다리와 포구 전열에서 적 수군을 받아낼 핵심 전열입니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '좁은 강변 길목을 밀어붙이며 적 보병을 흔드는 돌파 역할입니다.' },
|
|
{ unitId: 'sun-qian', role: 'support', reason: '강동 회담 기록과 동맹 명분을 전투 뒤 보고로 이어 줍니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '동맹 지휘', classKeys: ['lord'], reason: '유비가 무너지지 않아야 손유 동맹 전선이 유지됩니다.' },
|
|
{ label: '화공 책략', classKeys: ['strategist'], reason: '제갈량의 생존과 판단이 다음 적벽 화공전으로 이어집니다.' },
|
|
{ label: '강안 기동', classKeys: ['cavalry'], reason: '기동 장수가 강안 기병과 척후를 빠르게 끊어야 합니다.' },
|
|
{ label: '배다리 전열', classKeys: ['infantry', 'spearman'], reason: '배다리와 포구 수비대는 튼튼한 전열이 받아내야 합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 24, role: 'reserve', unitId: 'liu-bei', label: '동맹 지휘' },
|
|
{ x: 6, y: 22, role: 'front', unitId: 'guan-yu', label: '배다리 전열' },
|
|
{ x: 6, y: 25, role: 'front', unitId: 'zhang-fei', label: '포구 돌파' },
|
|
{ x: 8, y: 21, role: 'flank', unitId: 'zhao-yun', label: '강안 기동' },
|
|
{ x: 6, y: 23, role: 'support', unitId: 'zhuge-liang', label: '화공 판단' },
|
|
{ x: 8, y: 24, role: 'support', unitId: 'sun-qian', label: '동맹 기록' }
|
|
],
|
|
note: '적벽 전초전은 손유 동맹의 첫 강상 전투입니다. 유비와 제갈량을 반드시 포함하고, 조운으로 강안 기병을 끊은 뒤 관우와 장비가 배다리와 포구를 눌러 화공 논의의 발판을 만드는 편성이 좋습니다.'
|
|
},
|
|
'twenty-second-battle-red-cliffs-fire': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '적벽 결전의 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '동남풍과 화공 시점을 판단하는 핵심 책략 담당입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '화선을 노리는 강안 기병과 감시병을 빠르게 끊습니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '갑판 보병과 본선 친위를 정면에서 받아낼 전열입니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '배다리와 좁은 연환선 길을 밀어붙이는 돌파 역할입니다.' },
|
|
{ unitId: 'mi-zhu', role: 'support', reason: '화공전의 장기 피해를 버티기 위한 회복과 보급을 맡습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '동맹 지휘', classKeys: ['lord'], reason: '유비가 무너지지 않아야 손유 동맹의 결전 명분이 유지됩니다.' },
|
|
{ label: '동남풍 책략', classKeys: ['strategist'], reason: '제갈량의 생존과 판단이 화공 성공의 핵심입니다.' },
|
|
{ label: '갑판 전열', classKeys: ['infantry', 'spearman'], reason: '좁은 배다리와 갑판에서는 튼튼한 전열이 피해를 받아내야 합니다.' },
|
|
{ label: '강안 기동', classKeys: ['cavalry'], reason: '기동 장수가 감시병과 기병을 먼저 끊어 화선 접안로를 엽니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 23, role: 'reserve', unitId: 'liu-bei', label: '동맹 지휘' },
|
|
{ x: 6, y: 21, role: 'front', unitId: 'guan-yu', label: '갑판 전열' },
|
|
{ x: 6, y: 25, role: 'front', unitId: 'zhang-fei', label: '연환선 돌파' },
|
|
{ x: 8, y: 20, role: 'flank', unitId: 'zhao-yun', label: '강안 차단' },
|
|
{ x: 6, y: 22, role: 'support', unitId: 'zhuge-liang', label: '동남풍 판단' },
|
|
{ x: 5, y: 27, role: 'support', unitId: 'mi-zhu', label: '화공 보급' }
|
|
],
|
|
note: '적벽 화공전은 강 위의 결전입니다. 유비와 제갈량을 반드시 포함하고, 조운으로 감시병과 기병을 끊은 뒤 관우와 장비가 화선 접안로와 연환선을 열어야 합니다.'
|
|
},
|
|
'twenty-third-battle-jingzhou-south-entry': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '형주 남부 진입의 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '마을을 해치지 않고 길목과 관문을 여는 작전 핵심입니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '형도영의 보병 전열을 정면에서 받아낼 주력입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '척후와 기병을 빠르게 끊어 강가 길목을 안정시키기 좋습니다.' },
|
|
{ unitId: 'sun-qian', role: 'support', reason: '마량과 형주 선비층으로 이어질 외교 문안을 정리합니다.' },
|
|
{ unitId: 'mi-zhu', role: 'support', reason: '긴 남부 길목에서 회복과 보급을 맡기 좋습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '진입 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 형주 남부 진입의 명분이 유지됩니다.' },
|
|
{ label: '민심 책략', classKeys: ['strategist'], reason: '제갈량의 판단이 마을 피해와 다음 합류 보상을 좌우합니다.' },
|
|
{ label: '길목 전열', classKeys: ['infantry', 'spearman'], reason: '형도영 가병의 중앙 전열은 정면에서 눌러야 합니다.' },
|
|
{ label: '우회 차단', classKeys: ['cavalry'], reason: '기동 장수가 척후와 기병을 끊어 보급선을 안정시킵니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 25, role: 'reserve', unitId: 'liu-bei', label: '진입 명분' },
|
|
{ x: 6, y: 23, role: 'front', unitId: 'guan-yu', label: '길목 전열' },
|
|
{ x: 8, y: 22, role: 'flank', unitId: 'zhao-yun', label: '우회 차단' },
|
|
{ x: 6, y: 24, role: 'support', unitId: 'zhuge-liang', label: '민심 판단' },
|
|
{ x: 8, y: 25, role: 'support', unitId: 'sun-qian', label: '외교 문안' },
|
|
{ x: 5, y: 29, role: 'support', unitId: 'mi-zhu', label: '남부 보급' }
|
|
],
|
|
note: '형주 남부 진입전은 적벽 뒤 첫 근거지 확보 전투입니다. 유비와 제갈량을 반드시 포함하고, 남부 마을과 영릉 관문을 함께 안정시키면 백미 마량 합류가 자연스럽게 이어집니다.'
|
|
},
|
|
'twenty-fourth-battle-guiyang-persuasion': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang', 'ma-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '계양 설득의 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'ma-liang', role: 'support', reason: '계양 토호와 마을 장로에게 닿을 서신을 읽는 핵심 문사입니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '성채를 압박하면서도 항복 명분을 유지하는 작전 핵심입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '산길 척후와 남문 기병을 빠르게 끊어 서신로를 안정시킵니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '조범 친위대와 성문 보병을 받아낼 주력 전열입니다.' },
|
|
{ unitId: 'sun-qian', role: 'support', reason: '이적 합류로 이어질 외교 문안과 항복 조건을 정리합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '설득 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 계양의 항복 명분이 유지됩니다.' },
|
|
{ label: '서신 문관', classKeys: ['strategist'], reason: '마량과 제갈량의 생존이 마을 안정과 이적 합류의 핵심입니다.' },
|
|
{ label: '성문 전열', classKeys: ['infantry', 'spearman'], reason: '조범 친위대와 성문 보병은 튼튼한 전열이 받아내야 합니다.' },
|
|
{ label: '남문 차단', classKeys: ['cavalry'], reason: '기동 장수가 산길 척후와 남문 기병을 먼저 끊어야 서신로가 열립니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 5, y: 24, role: 'reserve', unitId: 'liu-bei', label: '설득 명분' },
|
|
{ x: 7, y: 22, role: 'front', unitId: 'guan-yu', label: '성문 전열' },
|
|
{ x: 9, y: 22, role: 'flank', unitId: 'zhao-yun', label: '남문 차단' },
|
|
{ x: 6, y: 24, role: 'support', unitId: 'zhuge-liang', label: '항복 조건' },
|
|
{ x: 8, y: 26, role: 'support', unitId: 'ma-liang', label: '백미 서신' },
|
|
{ x: 9, y: 25, role: 'support', unitId: 'sun-qian', label: '외교 문안' }
|
|
],
|
|
note: '계양 설득전은 마량이 처음으로 필수 출전하는 전장입니다. 유비, 제갈량, 마량을 지키며 마을과 서신 길목을 안정시킨 뒤 성문 친위대를 압박하는 편성이 좋습니다.'
|
|
},
|
|
'twenty-fifth-battle-wuling-mountain-road': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang', 'ma-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '무릉 군율의 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'ma-liang', role: 'support', reason: '산길 이름과 마을 장로 기록을 읽어 지점 목표를 연결합니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '좁은 산길 전열과 김선 설득 조건을 판단하는 작전 핵심입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '능선 척후와 산길 기병을 빠르게 끊어 고갯길을 안정시킵니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '협곡 보병 전열을 밀어내며 산길 입구를 여는 돌파 역할입니다.' },
|
|
{ unitId: 'yi-ji', role: 'support', reason: '공지에게 보낼 서약 문안과 백성 보호 명분을 정리합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '군율 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 무릉 마을이 유비군의 약속을 믿습니다.' },
|
|
{ label: '산길 문사', classKeys: ['strategist'], reason: '마량과 제갈량, 이적의 문안이 마을 안정과 공지 합류로 이어집니다.' },
|
|
{ label: '협곡 전열', classKeys: ['infantry', 'spearman'], reason: '좁은 길의 보병 전열은 튼튼한 돌파 장수가 받아내야 합니다.' },
|
|
{ label: '능선 차단', classKeys: ['cavalry'], reason: '기동 장수가 척후와 기병을 끊어야 산길 목표를 놓치지 않습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 28, role: 'reserve', unitId: 'liu-bei', label: '군율 중심' },
|
|
{ x: 6, y: 28, role: 'front', unitId: 'zhang-fei', label: '협곡 돌파' },
|
|
{ x: 8, y: 26, role: 'flank', unitId: 'zhao-yun', label: '능선 차단' },
|
|
{ x: 5, y: 29, role: 'support', unitId: 'zhuge-liang', label: '산길 계책' },
|
|
{ x: 7, y: 31, role: 'support', unitId: 'ma-liang', label: '마을 기록' },
|
|
{ x: 9, y: 28, role: 'support', unitId: 'yi-ji', label: '서약 문안' }
|
|
],
|
|
note: '무릉 산길 확보전은 마량과 이적의 형주 지식이 처음 함께 쓰이는 전장입니다. 산길 입구를 열고, 마을과 창고를 안정시킨 뒤 김선을 물러나게 하십시오.'
|
|
},
|
|
'twenty-sixth-battle-changsha-veteran': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '장사 항복 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'guan-yu', role: 'front', reason: '황충과 무인의 예를 나누며 전열을 받아낼 주력입니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '서문 보병 전열을 밀어내고 성문 압박을 유지하는 돌파 역할입니다.' },
|
|
{ unitId: 'zhao-yun', role: 'flank', reason: '장사 성문 주변 기병과 척후를 빠르게 정리하기 좋습니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '한현의 의심과 성문 방어선을 끊는 작전 핵심입니다.' },
|
|
{ unitId: 'gong-zhi', role: 'support', reason: '무릉에서 얻은 현지 정보로 장사 접근로와 성 안 소문을 읽습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '항복 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 장사를 예로 대한다는 명분이 유지됩니다.' },
|
|
{ label: '예법 문사', classKeys: ['strategist'], reason: '공지와 이적, 제갈량이 황충 예우와 항복 조건을 연결합니다.' },
|
|
{ label: '노장 전열', classKeys: ['infantry', 'spearman'], reason: '황충의 전열은 힘만이 아니라 무인의 예로 받아내야 합니다.' },
|
|
{ label: '동문 차단', classKeys: ['cavalry'], reason: '기동 장수가 동문 기병과 척후를 끊어 성문 압박을 안정시킵니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 27, role: 'reserve', unitId: 'liu-bei', label: '항복 명분' },
|
|
{ x: 6, y: 25, role: 'front', unitId: 'guan-yu', label: '노장 전열' },
|
|
{ x: 6, y: 29, role: 'front', unitId: 'zhang-fei', label: '서문 돌파' },
|
|
{ x: 8, y: 25, role: 'flank', unitId: 'zhao-yun', label: '동문 차단' },
|
|
{ x: 5, y: 27, role: 'support', unitId: 'zhuge-liang', label: '의심 해소' },
|
|
{ x: 9, y: 30, role: 'support', unitId: 'gong-zhi', label: '장사 길' }
|
|
],
|
|
note: '장사 노장 대면전은 공지의 현지 정보가 처음 강하게 추천되는 전장입니다. 서문 마을을 안정시키고, 황충 전열을 예로 받아낸 뒤 한현의 의심선을 압박하십시오.'
|
|
},
|
|
'twenty-seventh-battle-yizhou-relief-road': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '익주 원군 요청의 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '산길 전열과 관문 압박 순서를 함께 잡는 핵심 책사입니다.' },
|
|
{ unitId: 'huang-zhong', reason: '산비탈 궁병과 성루 궁병을 안정적으로 견제할 새 원거리 주력입니다.' },
|
|
{ unitId: 'wei-yan', reason: '좁은 관문 전열을 빠르게 흔들어 보급선을 여는 돌파 역할입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '원군로의 척후와 기병을 먼저 끊어 후방 혼란을 막습니다.' },
|
|
{ unitId: 'mi-zhu', reason: '마을 보급과 회복 도구를 안정시켜 원군 명분을 지키기 좋습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '원군 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 익주 원군 요청이라는 명분이 유지됩니다.' },
|
|
{ label: '산길 책략', classKeys: ['strategist'], reason: '제갈량과 문관이 산길 순서와 관문 압박을 읽어야 합니다.' },
|
|
{ label: '관문 돌파', classKeys: ['infantry', 'spearman'], reason: '위연 같은 돌파 장수가 좁은 관문 전열을 흔들어야 합니다.' },
|
|
{ label: '척후 차단', classKeys: ['cavalry'], reason: '기동 장수가 산길 척후와 기병을 먼저 끊어 후방을 지켜야 합니다.' },
|
|
{ label: '산비탈 견제', classKeys: ['archer'], reason: '황충의 원거리 견제가 성루 궁병과 산비탈 궁병을 안정적으로 눌러 줍니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 30, role: 'reserve', unitId: 'liu-bei', label: '원군 명분' },
|
|
{ x: 5, y: 30, role: 'support', unitId: 'zhuge-liang', label: '산길 계책' },
|
|
{ x: 7, y: 31, role: 'support', unitId: 'huang-zhong', label: '산비탈 견제' },
|
|
{ x: 9, y: 29, role: 'front', unitId: 'wei-yan', label: '관문 돌파' },
|
|
{ x: 8, y: 30, role: 'flank', unitId: 'zhao-yun', label: '척후 차단' },
|
|
{ x: 4, y: 34, role: 'support', unitId: 'mi-zhu', label: '보급 안정' }
|
|
],
|
|
note: '익주 원군로는 황충과 위연이 합류 후 바로 실전 선택지로 느껴지는 전장입니다. 마을 보급을 해치지 않고 산길 척후와 관문 전열을 차례로 끊어 방통의 신뢰를 얻으십시오.'
|
|
},
|
|
'twenty-eighth-battle-fu-pass-entry': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '부수관 진입 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'pang-tong', reason: '성루 사격선과 관문 창고를 흔드는 이번 전투의 새 기책 축입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '방통의 기책을 전체 전열과 보급선 판단으로 연결합니다.' },
|
|
{ unitId: 'wei-yan', reason: '좁은 관문 보병 전열을 빠르게 열어 책사들의 압박 시간을 벌어 줍니다.' },
|
|
{ unitId: 'huang-zhong', reason: '성루 궁병과 산루 궁병을 안정적으로 견제합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '협곡 기병과 복병을 빠르게 끊어 후방을 지킵니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '관문 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 부수관 진입의 명분이 유지됩니다.' },
|
|
{ label: '와룡봉추', classKeys: ['strategist'], reason: '제갈량과 방통을 함께 세우면 관문 압박과 계책 성공률이 안정됩니다.' },
|
|
{ label: '협곡 돌파', classKeys: ['infantry', 'spearman'], reason: '위연 같은 돌파 장수가 좁은 길의 보병 전열을 열어야 합니다.' },
|
|
{ label: '기병 차단', classKeys: ['cavalry'], reason: '기동 장수가 협곡 기병과 복병을 끊어 책사를 보호합니다.' },
|
|
{ label: '성루 견제', classKeys: ['archer'], reason: '황충의 원거리 견제가 성루 궁병을 억제합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 32, role: 'reserve', unitId: 'liu-bei', label: '관문 명분' },
|
|
{ x: 5, y: 32, role: 'support', unitId: 'zhuge-liang', label: '전열 판단' },
|
|
{ x: 9, y: 33, role: 'support', unitId: 'pang-tong', label: '성루 기책' },
|
|
{ x: 10, y: 31, role: 'front', unitId: 'wei-yan', label: '협곡 돌파' },
|
|
{ x: 7, y: 33, role: 'support', unitId: 'huang-zhong', label: '성루 견제' },
|
|
{ x: 8, y: 32, role: 'flank', unitId: 'zhao-yun', label: '기병 차단' }
|
|
],
|
|
note: '부수관 진입전은 방통을 처음 실전 편성으로 시험하는 전장입니다. 와룡과 봉추를 같이 세우면 관문 압박은 강해지지만, 전열과 보급 선택을 더 신중히 해야 합니다.'
|
|
},
|
|
'twenty-ninth-battle-luo-outer-wall': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '낙성 외곽 마을을 안정시키는 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '오의 설득과 성문 장수들의 속내를 읽는 이번 전투의 새 정보 축입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '법정의 내부 정보를 전체 방어선과 다음 낙봉파 판단으로 연결합니다.' },
|
|
{ unitId: 'wei-yan', reason: '외곽 보병 전열과 고개 척후를 빠르게 열어 책사들의 시간을 벌어 줍니다.' },
|
|
{ unitId: 'huang-zhong', reason: '성벽 궁병과 산루 궁병을 안정적으로 견제합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '넓어진 외곽 맵에서 기병과 복병을 빠르게 차단합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '외곽 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 마을 위무와 오의 설득의 명분이 유지됩니다.' },
|
|
{ label: '익주 정보', classKeys: ['strategist'], reason: '제갈량과 법정이 함께 있으면 성문 정보와 전열 판단이 안정됩니다.' },
|
|
{ label: '고개 돌파', classKeys: ['infantry', 'spearman'], reason: '위연 같은 전열 무장이 길목 보병을 열어야 책사가 움직일 수 있습니다.' },
|
|
{ label: '성벽 견제', classKeys: ['archer'], reason: '황충의 원거리 견제가 성벽 궁병과 산루 궁병을 억제합니다.' },
|
|
{ label: '기병 차단', classKeys: ['cavalry'], reason: '기동 장수가 넓은 외곽의 기병과 복병을 끊어 후방을 지킵니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 34, role: 'reserve', unitId: 'liu-bei', label: '마을 위무' },
|
|
{ x: 5, y: 34, role: 'support', unitId: 'zhuge-liang', label: '방어선 판단' },
|
|
{ x: 11, y: 36, role: 'support', unitId: 'fa-zheng', label: '성문 정보' },
|
|
{ x: 10, y: 33, role: 'front', unitId: 'wei-yan', label: '고개 돌파' },
|
|
{ x: 7, y: 35, role: 'support', unitId: 'huang-zhong', label: '성벽 견제' },
|
|
{ x: 8, y: 34, role: 'flank', unitId: 'zhao-yun', label: '기병 차단' }
|
|
],
|
|
note: '낙성 외곽전은 열다섯 명 중 여섯 명만 출전합니다. 법정과 제갈량을 함께 세우면 설득과 정보가 강해지지만, 방통까지 넣을지 전열 무장을 세울지 선택이 더 중요해집니다.'
|
|
},
|
|
'thirtieth-battle-luofeng-ambush': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'pang-tong'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '낙봉파 돌파 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'pang-tong', reason: '매복을 읽는 핵심 책사이며 반드시 살려야 하는 생존 조건입니다.' },
|
|
{ unitId: 'wu-yi', reason: '낙봉파 샛길과 고지 매복 위치를 아는 새 합류 안내역입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '장임의 방어 습관과 익주 장수들의 심리를 읽어 매복 대응을 돕습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '후미 기병과 복병을 빠르게 차단해 방통의 퇴로를 지킵니다.' },
|
|
{ unitId: 'huang-zhong', reason: '고지 궁병과 성루 궁병을 안정적으로 견제합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '돌파 명분', classKeys: ['lord'], reason: '유비가 살아 남아야 낙봉파 돌파의 명분이 유지됩니다.' },
|
|
{ label: '매복 판독', classKeys: ['strategist'], reason: '방통과 법정이 있으면 매복 경고와 적 심리 판단이 안정됩니다.' },
|
|
{ label: '현지 안내', classKeys: ['infantry', 'spearman'], reason: '오의 같은 현지 장수가 샛길과 퇴로 확보를 돕습니다.' },
|
|
{ label: '후미 차단', classKeys: ['cavalry'], reason: '기동 장수가 후미 기병과 복병을 끊어 방통을 지킵니다.' },
|
|
{ label: '고지 견제', classKeys: ['archer'], reason: '황충의 원거리 견제가 고지 궁병의 압박을 줄입니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 36, role: 'reserve', unitId: 'liu-bei', label: '돌파 명분' },
|
|
{ x: 9, y: 36, role: 'support', unitId: 'pang-tong', label: '매복 판독' },
|
|
{ x: 12, y: 36, role: 'flank', unitId: 'wu-yi', label: '길 안내' },
|
|
{ x: 11, y: 38, role: 'support', unitId: 'fa-zheng', label: '속내 읽기' },
|
|
{ x: 8, y: 36, role: 'flank', unitId: 'zhao-yun', label: '후미 차단' },
|
|
{ x: 7, y: 37, role: 'support', unitId: 'huang-zhong', label: '고지 견제' }
|
|
],
|
|
note: '낙봉파 매복전은 열여섯 명 중 여섯 명만 출전합니다. 오의를 데려가면 길 안내가 강해지지만 전열 무장이 줄어드므로, 후미 차단과 고지 견제를 누가 맡을지 먼저 정해야 합니다.'
|
|
},
|
|
'thirty-first-battle-luo-main-gate': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'pang-tong'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '낙성 항복 명분이자 패배 조건의 중심입니다.' },
|
|
{ unitId: 'pang-tong', role: 'support', reason: '성문 돌파 순서와 퇴로를 함께 계산하는 핵심 책사입니다.' },
|
|
{ unitId: 'fa-zheng', role: 'support', reason: '엄안 설득과 익주 장수 심리 판단을 맡습니다.' },
|
|
{ unitId: 'wu-yi', role: 'flank', reason: '낙성 내부 지형과 성문 방어 습관을 아는 새 합류 장수입니다.' },
|
|
{ unitId: 'huang-zhong', role: 'support', reason: '성벽 궁병과 노장 엄안의 원거리 압박에 대응하기 좋습니다.' },
|
|
{ unitId: 'zhang-fei', role: 'front', reason: '성문 앞 친위대를 짧게 무너뜨리는 정면 돌파력이 필요합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '항복 명분', classKeys: ['lord'], reason: '유비가 살아 있어야 성문 개방 이후 항복 명분이 유지됩니다.' },
|
|
{ label: '성문 설계', classKeys: ['strategist'], reason: '방통과 법정이 성문 돌파와 엄안 설득을 안정시킵니다.' },
|
|
{ label: '정면 돌파', classKeys: ['infantry', 'spearman'], reason: '성문 친위대를 밀어내려면 굵은 전열이 필요합니다.' },
|
|
{ label: '성벽 견제', classKeys: ['archer'], reason: '황충 같은 궁병이 성벽 사격선을 끊어 줍니다.' },
|
|
{ label: '측면 기동', classKeys: ['cavalry'], reason: '성문 앞 기병과 복병을 빠르게 묶어야 합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 39, role: 'reserve', unitId: 'liu-bei', label: '항복 명분' },
|
|
{ x: 9, y: 38, role: 'support', unitId: 'pang-tong', label: '성문 설계' },
|
|
{ x: 11, y: 40, role: 'support', unitId: 'fa-zheng', label: '엄안 설득' },
|
|
{ x: 12, y: 38, role: 'flank', unitId: 'wu-yi', label: '내성 길' },
|
|
{ x: 7, y: 39, role: 'support', unitId: 'huang-zhong', label: '성벽 견제' },
|
|
{ x: 4, y: 41, role: 'front', unitId: 'zhang-fei', label: '성문 돌파' }
|
|
],
|
|
note: '낙성 본성 공략은 성문을 여는 힘과 항복을 받아낼 명분을 함께 봐야 합니다. 법정·오의를 세우면 엄안 설득이 선명해지고, 장비·황충을 세우면 성문 돌파가 안정됩니다.'
|
|
},
|
|
'thirty-second-battle-mianzhu-gate': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'yan-yan'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '성도 항복 권고와 패배 조건의 중심입니다.' },
|
|
{ unitId: 'yan-yan', role: 'support', reason: '엄안의 첫 출전입니다. 이엄에게 익주 장수 예우를 증명합니다.' },
|
|
{ unitId: 'fa-zheng', role: 'support', reason: '성도 수비장들의 심리와 이엄의 판단을 읽는 책사입니다.' },
|
|
{ unitId: 'wu-yi', role: 'flank', reason: '면죽관 관문과 보급 창고 위치를 아는 현지 장수입니다.' },
|
|
{ unitId: 'pang-tong', role: 'support', reason: '무리한 돌파 대신 관문 압박과 퇴로 확보를 동시에 계산합니다.' },
|
|
{ unitId: 'huang-zhong', role: 'support', reason: '관문 궁병과 고지 수비를 안정적으로 견제합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '항복 명분', classKeys: ['lord'], reason: '유비가 살아 있어야 성도 항복 권고의 명분이 유지됩니다.' },
|
|
{ label: '익주 증언', classKeys: ['archer'], reason: '엄안과 황충이 관문 궁병을 견제하며 예우의 증거를 보여 줍니다.' },
|
|
{ label: '관문 설계', classKeys: ['strategist'], reason: '법정과 방통이 이엄의 심리와 관문 압박 순서를 잡습니다.' },
|
|
{ label: '현지 안내', classKeys: ['infantry', 'spearman'], reason: '오의 같은 익주 장수가 마을과 창고 위치를 짚어 줍니다.' },
|
|
{ label: '측면 기동', classKeys: ['cavalry'], reason: '관문 앞 기병과 창고 차단 병력을 빠르게 묶을 수 있습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 40, role: 'reserve', unitId: 'liu-bei', label: '항복 명분' },
|
|
{ x: 11, y: 37, role: 'support', unitId: 'yan-yan', label: '익주 증언' },
|
|
{ x: 11, y: 41, role: 'support', unitId: 'fa-zheng', label: '이엄 판단' },
|
|
{ x: 12, y: 39, role: 'flank', unitId: 'wu-yi', label: '창고 길' },
|
|
{ x: 9, y: 39, role: 'support', unitId: 'pang-tong', label: '관문 설계' },
|
|
{ x: 7, y: 40, role: 'support', unitId: 'huang-zhong', label: '궁병 견제' }
|
|
],
|
|
note: '면죽관 압박전은 엄안의 첫 출전을 설득의 증거로 쓰는 전투입니다. 관문을 무너뜨리는 힘보다 외곽 마을과 보급 창고를 안정시키는 명분이 중요합니다.'
|
|
},
|
|
'thirty-third-battle-chengdu-surrender': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'li-yan'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', role: 'reserve', reason: '성도 항복 권고와 익주 확보 명분의 중심입니다.' },
|
|
{ unitId: 'li-yan', role: 'support', reason: '이엄의 첫 출전입니다. 면죽관에서 본 군율을 증언합니다.' },
|
|
{ unitId: 'zhuge-liang', role: 'support', reason: '항복 조건과 익주 수습책을 동시에 설계하는 핵심 책사입니다.' },
|
|
{ unitId: 'fa-zheng', role: 'support', reason: '황권의 신중한 판단과 유장 진영의 속내를 읽습니다.' },
|
|
{ unitId: 'yan-yan', role: 'support', reason: '엄안의 항복 선례가 황권과 익주 장수들의 마음을 흔듭니다.' },
|
|
{ unitId: 'wu-yi', role: 'flank', reason: '성도 외곽 길과 창고 위치를 아는 현지 장수입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '항복 명분', classKeys: ['lord'], reason: '유비가 살아 있어야 성도 항복 권고가 힘을 얻습니다.' },
|
|
{ label: '익주 증언', classKeys: ['infantry', 'spearman'], reason: '이엄과 오의가 군율과 현지 사정을 증언합니다.' },
|
|
{ label: '권고 설계', classKeys: ['strategist'], reason: '제갈량과 법정이 항복 조건과 황권의 심리를 정리합니다.' },
|
|
{ label: '노장 선례', classKeys: ['archer'], reason: '엄안이 익주 노장의 항복 선례로 설득력을 보탭니다.' },
|
|
{ label: '측면 차단', classKeys: ['cavalry'], reason: '성도 기병의 남문 반격과 창고 차단을 빠르게 묶을 수 있습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 42, role: 'reserve', unitId: 'liu-bei', label: '항복 명분' },
|
|
{ x: 13, y: 40, role: 'support', unitId: 'li-yan', label: '익주 증언' },
|
|
{ x: 6, y: 40, role: 'support', unitId: 'zhuge-liang', label: '권고 설계' },
|
|
{ x: 11, y: 43, role: 'support', unitId: 'fa-zheng', label: '황권 판단' },
|
|
{ x: 11, y: 39, role: 'support', unitId: 'yan-yan', label: '노장 선례' },
|
|
{ x: 12, y: 41, role: 'flank', unitId: 'wu-yi', label: '창고 길' }
|
|
],
|
|
note: '성도 항복 권고전은 성문을 부수기보다 항복 조건을 믿게 만드는 전투입니다. 이엄을 지키고, 마을과 창고를 안정시키며, 황권이 물러설 명분을 만들어야 합니다.'
|
|
},
|
|
'thirty-fourth-battle-jiameng-pass': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'huang-quan'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '가맹관에서 마초를 받아들일 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'huang-quan', reason: '황권의 첫 출전입니다. 익주 북문과 한중 길을 해석합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '서량 기병의 돌격을 기동으로 받아 내고 후속을 끊습니다.' },
|
|
{ unitId: 'zhang-fei', reason: '마초의 정면 돌격을 받아 내는 강한 전열 압박입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '마초가 장로에게 마음을 다 주지 못한 틈을 읽는 책사입니다.' },
|
|
{ unitId: 'huang-zhong', reason: '산허리 궁병과 기병 돌격을 안정적으로 견제합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '포용 명분', classKeys: ['lord'], reason: '유비가 살아 있어야 마초 합류의 명분이 유지됩니다.' },
|
|
{ label: '길 해석', classKeys: ['strategist'], reason: '황권과 법정이 가맹관 길과 마초의 속내를 읽습니다.' },
|
|
{ label: '돌격 저지', classKeys: ['spearman', 'infantry'], reason: '장비 같은 전열 무장이 서량 기병의 첫 충돌을 받아 냅니다.' },
|
|
{ label: '기병 회수', classKeys: ['cavalry'], reason: '조운이 깊게 들어온 기병의 후속과 퇴로를 끊습니다.' },
|
|
{ label: '사격 견제', classKeys: ['archer'], reason: '황충이 관문 궁병과 고지 사격선을 누릅니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 44, role: 'reserve', unitId: 'liu-bei', label: '포용 명분' },
|
|
{ x: 14, y: 44, role: 'support', unitId: 'huang-quan', label: '길 해석' },
|
|
{ x: 8, y: 43, role: 'flank', unitId: 'zhao-yun', label: '기병 회수' },
|
|
{ x: 4, y: 46, role: 'front', unitId: 'zhang-fei', label: '돌격 저지' },
|
|
{ x: 11, y: 45, role: 'support', unitId: 'fa-zheng', label: '설득 책략' },
|
|
{ x: 7, y: 44, role: 'support', unitId: 'huang-zhong', label: '사격 견제' }
|
|
],
|
|
note: '가맹관 마초 대면전은 서량 기병을 완전히 몰살하는 싸움이 아니라, 첫 돌격을 막고 보급로와 말길을 보전해 마초가 유비군을 인정하게 만드는 전투입니다.'
|
|
},
|
|
'thirty-fifth-battle-yangping-scout': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'ma-chao'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '한중 전선의 첫 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'ma-chao', reason: '마초의 첫 아군 출전입니다. 서량 기병으로 양평관 선봉을 흔들 수 있습니다.' },
|
|
{ unitId: 'huang-quan', reason: '양평관 지형과 창고 확보 목표를 읽는 현지 참모입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '한중 본전으로 이어질 전선 설계와 적 책사 견제가 필요합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '마초가 너무 깊게 들어가지 않도록 기병 후속을 안정시킵니다.' },
|
|
{ unitId: 'fa-zheng', reason: '장로군의 내부 동요와 염포의 책략을 읽는 데 강합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '정찰 명분', classKeys: ['lord'], reason: '유비가 살아 있어야 한중 정찰이 다음 전선으로 이어집니다.' },
|
|
{ label: '서량 선봉', classKeys: ['cavalry'], reason: '마초와 조운이 돌파와 회수를 나누면 기병 운용이 안정됩니다.' },
|
|
{ label: '창고 기록', classKeys: ['strategist'], reason: '황권과 제갈량, 법정이 창고와 산길 정보를 전장 보상으로 연결합니다.' },
|
|
{ label: '관문 압박', classKeys: ['infantry', 'spearman'], reason: '보병 전열이 관문 앞에서 기병이 돌아올 시간을 벌어 줍니다.' },
|
|
{ label: '고지 견제', classKeys: ['archer'], reason: '궁병은 관문 궁병과 창고 주변 사격선을 억제합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 46, role: 'reserve', unitId: 'liu-bei', label: '정찰 명분' },
|
|
{ x: 13, y: 42, role: 'flank', unitId: 'ma-chao', label: '서량 선봉' },
|
|
{ x: 14, y: 46, role: 'support', unitId: 'huang-quan', label: '창고 기록' },
|
|
{ x: 6, y: 44, role: 'support', unitId: 'zhuge-liang', label: '전선 설계' },
|
|
{ x: 8, y: 45, role: 'flank', unitId: 'zhao-yun', label: '기병 회수' },
|
|
{ x: 11, y: 45, role: 'support', unitId: 'fa-zheng', label: '책략 판단' }
|
|
],
|
|
note: '양평관 정찰전은 마초가 처음 아군 선봉으로 나서는 전투입니다. 마초를 앞세우되 황권이 창고와 산길을 기록할 시간을 벌어, 마대 합류와 한중 본전 준비로 연결하십시오.'
|
|
},
|
|
'thirty-sixth-battle-dingjun-vanguard': {
|
|
maxUnits: 6,
|
|
requiredUnitIds: ['liu-bei', 'huang-zhong'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '정군산 전선의 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'huang-zhong', reason: '정군산 전초전의 핵심 장수입니다. 고지 압박과 생존 목표가 맞물립니다.' },
|
|
{ unitId: 'fa-zheng', reason: '황충의 고지 돌파 타이밍과 하후연 선봉의 허점을 읽는 책사입니다.' },
|
|
{ unitId: 'ma-chao', reason: '서쪽 능선을 흔들어 하후연의 시선을 분산시킬 수 있습니다.' },
|
|
{ unitId: 'ma-dai', reason: '마초의 첫 돌파 뒤 후속 기병과 퇴로를 안정시키는 새 합류 장수입니다.' },
|
|
{ unitId: 'mi-zhu', reason: '정군산 장기전에서 회복 도구와 보급 운용을 안정시키는 선택지입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '고지 돌파', classKeys: ['archer', 'infantry'], reason: '황충과 보병 전열이 능선 위 사격선을 밀어냅니다.' },
|
|
{ label: '책략 타이밍', classKeys: ['strategist'], reason: '법정이 하후연 선봉의 허점과 고지 책략대를 끊어 줍니다.' },
|
|
{ label: '서쪽 우회', classKeys: ['cavalry'], reason: '마초와 마대가 적 기병의 시선을 갈라 황충의 돌파 길을 엽니다.' },
|
|
{ label: '보급 안정', classKeys: ['quartermaster'], reason: '미축과 보급 담당은 황충 생존 목표를 안정시키는 역할입니다.' },
|
|
{ label: '군주 생존', classKeys: ['lord'], reason: '유비가 살아 있어야 왕평 귀순과 한중 본전으로 이어집니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 48, role: 'reserve', unitId: 'liu-bei', label: '한중 명분' },
|
|
{ x: 8, y: 47, role: 'front', unitId: 'huang-zhong', label: '고지 돌파' },
|
|
{ x: 10, y: 49, role: 'support', unitId: 'fa-zheng', label: '책략 타이밍' },
|
|
{ x: 13, y: 44, role: 'flank', unitId: 'ma-chao', label: '서쪽 우회' },
|
|
{ x: 15, y: 45, role: 'flank', unitId: 'ma-dai', label: '퇴로 회수' },
|
|
{ x: 6, y: 50, role: 'support', unitId: 'mi-zhu', label: '보급 안정' }
|
|
],
|
|
note: '정군산 전초전은 스물한 명 중 여섯 명만 출전합니다. 황충은 필수이자 패배 조건이므로 법정의 책략, 마초·마대의 우회, 미축의 보급 중 무엇을 빼면 전열이 흔들리는지 바로 보이게 구성했습니다.'
|
|
},
|
|
'thirty-seventh-battle-hanzhong-decisive': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'wang-ping'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '한중을 얻는 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'huang-zhong', reason: '정군산의 여세를 이어 고지 압박을 맡는 핵심 장수입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '조조군 본진을 흔드는 유인책과 고지 운용의 핵심 참모입니다.' },
|
|
{ unitId: 'wang-ping', reason: '한중 산길과 보급로를 잇는 길잡이이며 생존 목표와 맞물립니다.' },
|
|
{ unitId: 'ma-chao', reason: '북쪽 기병대를 묶고 퇴로를 흔드는 기동 축입니다.' },
|
|
{ unitId: 'ma-dai', reason: '마초의 돌격을 회수하고 좁은 산길 기병 운용을 보조합니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '한중 승리 이후 한중왕 즉위와 촉한 건국 흐름까지 설계합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '한중 명분', classKeys: ['lord'], reason: '유비가 살아 있어야 한중왕 즉위 준비가 이어집니다.' },
|
|
{ label: '산길 안내', classKeys: ['spearman'], reason: '왕평은 새로 합류한 길잡이이며 창고 확보와 생존 목표가 맞물립니다.' },
|
|
{ label: '고지 압박', classKeys: ['archer', 'infantry'], reason: '황충과 전열 병과가 중앙 창고와 고지를 열어 줍니다.' },
|
|
{ label: '본진 유인', classKeys: ['strategist'], reason: '법정과 제갈량이 조조 본진을 무리 없이 끌어냅니다.' },
|
|
{ label: '퇴로 차단', classKeys: ['cavalry'], reason: '마초와 마대가 북쪽 기병을 묶고 조조군의 뒤를 흔듭니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 4, y: 50, role: 'reserve', unitId: 'liu-bei', label: '한중 명분' },
|
|
{ x: 16, y: 49, role: 'support', unitId: 'wang-ping', label: '산길 안내' },
|
|
{ x: 7, y: 50, role: 'front', unitId: 'huang-zhong', label: '고지 압박' },
|
|
{ x: 11, y: 51, role: 'support', unitId: 'fa-zheng', label: '본진 유인' },
|
|
{ x: 13, y: 46, role: 'flank', unitId: 'ma-chao', label: '퇴로 압박' },
|
|
{ x: 15, y: 47, role: 'flank', unitId: 'ma-dai', label: '기병 회수' },
|
|
{ x: 6, y: 48, role: 'support', unitId: 'zhuge-liang', label: '건국 설계' }
|
|
],
|
|
note: '한중 결전은 스물두 명 중 일곱 명만 출전합니다. 왕평이 필수 길잡이로 합류했으므로, 중앙 창고와 산길을 열며 조조 본진을 압박하는 편성이 가장 안정적입니다.'
|
|
},
|
|
'thirty-eighth-battle-jing-defense': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '촉한의 이름을 세운 뒤 형주 방위 명분을 직접 확인합니다.' },
|
|
{ unitId: 'guan-yu', reason: '형주 방위 전초전의 핵심 장수이며 생존 목표입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '관우의 전열 압박을 보조하며 강한 정면 대응을 맡습니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 민심과 봉화대 확보를 읽는 현지 책사입니다.' },
|
|
{ unitId: 'mi-zhu', reason: '강릉과 나루 사이 보급을 안정시키는 군량관입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '촉한 건국 직후 형주 전선의 정치적 위험을 해석합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '강변 기동대와 기병 대응을 안정적으로 맡습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '형주 명분', classKeys: ['lord'], reason: '유비가 전장에 있어야 형주 방위 명분이 흔들리지 않습니다.' },
|
|
{ label: '북문 전열', classKeys: ['spearman', 'infantry'], reason: '관우와 장비가 북쪽 성채 압박을 받아 냅니다.' },
|
|
{ label: '강변 기동', classKeys: ['cavalry'], reason: '조운이 나루와 강변 척후를 빠르게 끊어 줍니다.' },
|
|
{ label: '민심 판단', classKeys: ['strategist'], reason: '마량과 제갈량이 봉화대·민심·정치적 위험을 읽습니다.' },
|
|
{ label: '보급 안정', classKeys: ['infantry', 'strategist'], reason: '미축이 강릉과 나루 사이 보급선을 지켜 줍니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 5, y: 51, role: 'reserve', unitId: 'liu-bei', label: '형주 명분' },
|
|
{ x: 9, y: 46, role: 'front', unitId: 'guan-yu', label: '북문 전열' },
|
|
{ x: 6, y: 53, role: 'front', unitId: 'zhang-fei', label: '남안 압박' },
|
|
{ x: 10, y: 51, role: 'support', unitId: 'ma-liang', label: '봉화 판단' },
|
|
{ x: 8, y: 52, role: 'support', unitId: 'mi-zhu', label: '보급 안정' },
|
|
{ x: 8, y: 48, role: 'support', unitId: 'zhuge-liang', label: '양면 해석' },
|
|
{ x: 11, y: 49, role: 'flank', unitId: 'zhao-yun', label: '강변 기동' }
|
|
],
|
|
note: '형주 방위 전초전은 스물두 명 중 일곱 명만 출전합니다. 관우를 전열의 중심에 세우고, 마량·미축·제갈량의 지원과 조운의 강변 기동으로 봉화대와 나루를 안정시키십시오.'
|
|
},
|
|
'thirty-ninth-battle-fan-castle-vanguard': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '번성 전선의 큰 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'guan-yu', reason: '번성 외곽 압박전의 핵심 장수이며 생존 목표입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '방덕 기병의 돌파를 정면에서 받아치는 강한 전열입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '강변과 보루 사이 기동 대응을 안정적으로 맡습니다.' },
|
|
{ unitId: 'huang-zhong', reason: '외곽 보루의 궁병과 성벽 압박에 강한 원거리 축입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '한수 물길과 제방을 읽어 다음 수공 준비와 맞물립니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 민심과 보급선을 안정시키며 관우 전선을 보조합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '번성 명분', classKeys: ['lord'], reason: '유비가 있어야 형주 전선의 공세 명분이 흔들리지 않습니다.' },
|
|
{ label: '보루 전열', classKeys: ['spearman', 'infantry'], reason: '관우와 장비가 방덕 기병 돌파를 받아 냅니다.' },
|
|
{ label: '기동 견제', classKeys: ['cavalry'], reason: '조운이 강변과 보루 사이 빈틈을 메웁니다.' },
|
|
{ label: '성벽 압박', classKeys: ['archer'], reason: '황충이 보루 궁병과 성벽 압박을 안정적으로 맡습니다.' },
|
|
{ label: '수공 준비', classKeys: ['strategist'], reason: '법정과 마량이 제방·수문·보급선을 다음 전투로 연결합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 5, y: 54, role: 'reserve', unitId: 'liu-bei', label: '번성 명분' },
|
|
{ x: 10, y: 49, role: 'front', unitId: 'guan-yu', label: '보루 전열' },
|
|
{ x: 6, y: 56, role: 'front', unitId: 'zhang-fei', label: '기병 수비' },
|
|
{ x: 13, y: 52, role: 'flank', unitId: 'zhao-yun', label: '강변 기동' },
|
|
{ x: 16, y: 50, role: 'support', unitId: 'huang-zhong', label: '성벽 압박' },
|
|
{ x: 17, y: 54, role: 'support', unitId: 'fa-zheng', label: '수공 준비' },
|
|
{ x: 12, y: 54, role: 'support', unitId: 'ma-liang', label: '보급 안정' }
|
|
],
|
|
note: '번성 외곽 압박전은 관우가 방어에서 공세로 전환하는 첫 전장입니다. 외곽 보루를 확보해 방덕 기병을 고립시키고, 법정·마량으로 제방과 보급 정보를 챙겨 다음 한수 수공을 준비하십시오.'
|
|
},
|
|
'fortieth-battle-han-river-flood': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '칠군을 흔드는 대의명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'guan-yu', reason: '한수 수공의 전선 지휘관이며 생존 목표입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '수위와 둑길을 읽어 수공의 타이밍을 잡는 핵심 참모입니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 현지의 제방과 민심을 함께 살피는 보조 책사입니다.' },
|
|
{ unitId: 'huang-quan', reason: '물길과 퇴로를 신중하게 판정해 포위 안정성을 높입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '제방과 나루 사이를 빠르게 보강하는 기동 축입니다.' },
|
|
{ unitId: 'ma-chao', reason: '방덕 기병의 돌파를 맞받아치는 기병 대응 카드입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '수공 명분', classKeys: ['lord'], reason: '유비가 있어야 칠군 항복 이후 처리 명분이 분명해집니다.' },
|
|
{ label: '둑길 전열', classKeys: ['spearman', 'infantry'], reason: '관우가 둑길과 수문 사이 전선을 붙잡습니다.' },
|
|
{ label: '물길 판단', classKeys: ['strategist'], reason: '법정·마량·황권이 수위, 퇴로, 포로 처리를 나눠 봅니다.' },
|
|
{ label: '기병 봉쇄', classKeys: ['cavalry'], reason: '조운과 마초가 방덕 기병을 물가로 몰아넣습니다.' },
|
|
{ label: '고지 지원', classKeys: ['archer'], reason: '둑 위 궁병 대응이 있으면 본대 압박이 안정됩니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 5, y: 56, role: 'reserve', unitId: 'liu-bei', label: '수공 명분' },
|
|
{ x: 11, y: 51, role: 'front', unitId: 'guan-yu', label: '둑길 전열' },
|
|
{ x: 18, y: 56, role: 'support', unitId: 'fa-zheng', label: '수위 판단' },
|
|
{ x: 13, y: 56, role: 'support', unitId: 'ma-liang', label: '제방 민심' },
|
|
{ x: 21, y: 57, role: 'support', unitId: 'huang-quan', label: '퇴로 판정' },
|
|
{ x: 14, y: 54, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' },
|
|
{ x: 22, y: 51, role: 'flank', unitId: 'ma-chao', label: '기병 봉쇄' }
|
|
],
|
|
note: '한수 수공은 둑길과 수문을 잡아 우금 본대를 강과 진흙 사이에 묶는 전투입니다. 법정·마량·황권으로 물길을 읽고, 조운·마초로 방덕 기병의 돌파를 막으며 관우의 전열을 유지하십시오.'
|
|
},
|
|
'forty-first-battle-fan-castle-siege': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '번성 포위의 큰 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'guan-yu', reason: '성문을 압박하는 핵심 장수이며 생존 목표입니다.' },
|
|
{ unitId: 'huang-zhong', reason: '성벽 궁병과 외성 보루를 안정적으로 견제합니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 민심과 강동의 움직임을 함께 읽는 현지 참모입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '조인의 수성전과 방덕의 결사 돌격을 읽는 책략 축입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '성문과 나루 사이를 빠르게 보강하는 기동 축입니다.' },
|
|
{ unitId: 'ma-chao', reason: '방덕 기병과 조조군 북로 기병을 맞받아치는 강한 기병 대응 카드입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '포위 명분', classKeys: ['lord'], reason: '유비가 있어야 번성 포위와 항복 처리의 명분이 유지됩니다.' },
|
|
{ label: '성문 전열', classKeys: ['spearman', 'infantry'], reason: '관우가 외성 보루와 성문 앞 압박을 붙잡습니다.' },
|
|
{ label: '성벽 사거리', classKeys: ['archer'], reason: '황충이 성벽 궁병과 보루 위 적을 먼저 누릅니다.' },
|
|
{ label: '수성전 판단', classKeys: ['strategist'], reason: '법정과 마량이 조인의 버티기와 강동 나루의 징후를 읽습니다.' },
|
|
{ label: '기병 차단', classKeys: ['cavalry'], reason: '조운과 마초가 방덕 결사 기병과 남서 나루 척후를 묶습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 5, y: 58, role: 'reserve', unitId: 'liu-bei', label: '포위 명분' },
|
|
{ x: 12, y: 53, role: 'front', unitId: 'guan-yu', label: '성문 압박' },
|
|
{ x: 18, y: 54, role: 'support', unitId: 'huang-zhong', label: '성벽 견제' },
|
|
{ x: 14, y: 58, role: 'support', unitId: 'ma-liang', label: '형주 후방' },
|
|
{ x: 19, y: 58, role: 'support', unitId: 'fa-zheng', label: '수성전 판단' },
|
|
{ x: 15, y: 56, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' },
|
|
{ x: 23, y: 53, role: 'flank', unitId: 'ma-chao', label: '방덕 봉쇄' }
|
|
],
|
|
note: '번성 공성전은 성문 압박과 후방 나루 경계를 함께 보는 전투입니다. 관우·황충으로 외성 보루를 밀고, 조운·마초로 방덕과 강동 척후의 길목을 막으며, 마량·법정으로 다음 형주 위기의 징후를 읽으십시오.'
|
|
},
|
|
'forty-second-battle-jing-rear-crisis': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '형주 후방 명분과 패배 조건의 중심입니다.' },
|
|
{ unitId: 'guan-yu', reason: '번성 전선과 후방 경계를 함께 묶습니다.' },
|
|
{ unitId: 'ma-liang', reason: '강릉 민심과 강동 말투를 읽는 참모입니다.' },
|
|
{ unitId: 'mi-zhu', reason: '창고 장부와 군량선을 확인합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '나루와 창고 사이 빈틈을 빠르게 막습니다.' },
|
|
{ unitId: 'huang-quan', reason: '포로와 장부의 흔들림을 신중히 판정합니다.' },
|
|
{ unitId: 'wang-ping', reason: '숲길과 강변 기습을 읽어 차단합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '후방 명분', classKeys: ['lord'], reason: '유비가 있어야 후방 병사와 민심을 다독일 명분이 살아납니다.' },
|
|
{ label: '나루 전열', classKeys: ['spearman', 'infantry'], reason: '관우가 창고와 나루 사이의 전열을 붙잡습니다.' },
|
|
{ label: '창고 장부', classKeys: ['strategist'], reason: '마량과 황권이 강동 말투, 포로, 장부의 흔들림을 읽습니다.' },
|
|
{ label: '보급 관리', classKeys: ['lord', 'strategist'], reason: '미축이 군량선과 창고를 확인해 목표 피드백을 살립니다.' },
|
|
{ label: '기습 차단', classKeys: ['cavalry', 'infantry'], reason: '조운과 왕평이 숲길과 강변 기습을 빠르게 막습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 6, y: 59, role: 'reserve', unitId: 'liu-bei', label: '후방 명분' },
|
|
{ x: 13, y: 54, role: 'front', unitId: 'guan-yu', label: '나루 전열' },
|
|
{ x: 15, y: 59, role: 'support', unitId: 'ma-liang', label: '민심 판정' },
|
|
{ x: 9, y: 60, role: 'support', unitId: 'mi-zhu', label: '창고 장부' },
|
|
{ x: 16, y: 57, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' },
|
|
{ x: 23, y: 60, role: 'support', unitId: 'huang-quan', label: '장부 판정' },
|
|
{ x: 23, y: 52, role: 'flank', unitId: 'wang-ping', label: '숲길 경계' }
|
|
],
|
|
note: '강릉 나루 경계전은 적을 모두 격파하기보다 창고와 나루가 흔들리지 않게 지키는 전투입니다. 미축·마량·황권으로 후방 장부를 읽고, 조운·왕평으로 강동 척후와 숲길 기습을 끊으십시오.'
|
|
},
|
|
'forty-third-battle-gongan-collapse': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '공안 병사에게 형주의 명분을 보여 줍니다.' },
|
|
{ unitId: 'guan-yu', reason: '번성 전선과 후방 성문을 함께 묶습니다.' },
|
|
{ unitId: 'ma-liang', reason: '수비대 민심과 강동 설객을 읽습니다.' },
|
|
{ unitId: 'mi-zhu', reason: '창고 장부와 미방의 흔들림을 추적합니다.' },
|
|
{ unitId: 'huang-quan', reason: '부사인 군령을 판정해 내분을 줄입니다.' },
|
|
{ unitId: 'wang-ping', reason: '성문과 숲길 복병을 읽어 막습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '나루와 성문 사이 백의 복병을 차단합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '형주 명분', classKeys: ['lord'], reason: '유비가 있어야 공안 병사들이 완전히 돌아서지 않습니다.' },
|
|
{ label: '성문 전열', classKeys: ['spearman', 'infantry'], reason: '관우가 외성 성문을 붙들어 여몽 본대를 막습니다.' },
|
|
{ label: '군령 판정', classKeys: ['strategist'], reason: '마량과 황권이 설객, 군령서, 수비대 동요를 가려냅니다.' },
|
|
{ label: '장부 수습', classKeys: ['lord', 'strategist'], reason: '미축이 창고와 미방의 침묵을 전투 목표로 연결합니다.' },
|
|
{ label: '복병 차단', classKeys: ['cavalry', 'infantry'], reason: '조운과 왕평이 나루, 숲길, 성문 옆길을 빠르게 막습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 8, y: 61, role: 'reserve', unitId: 'liu-bei', label: '형주 명분' },
|
|
{ x: 15, y: 56, role: 'front', unitId: 'guan-yu', label: '성문 전열' },
|
|
{ x: 17, y: 61, role: 'support', unitId: 'ma-liang', label: '민심 판정' },
|
|
{ x: 11, y: 61, role: 'support', unitId: 'mi-zhu', label: '창고 장부' },
|
|
{ x: 26, y: 62, role: 'support', unitId: 'huang-quan', label: '군령 판정' },
|
|
{ x: 25, y: 52, role: 'flank', unitId: 'wang-ping', label: '숲길 경계' },
|
|
{ x: 18, y: 58, role: 'flank', unitId: 'zhao-yun', label: '나루 차단' }
|
|
],
|
|
note: '공안 성문 변고는 여몽 본대를 막는 동시에 성문과 장부를 다시 붙드는 전투입니다. 마량·황권으로 군령의 진위를 읽고, 미축으로 창고 기록을 확인하며, 조운·왕평으로 강변 복병을 끊으십시오.'
|
|
},
|
|
'forty-fourth-battle-maicheng-isolation': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '관우군을 버리지 않는 명분의 중심입니다.' },
|
|
{ unitId: 'guan-yu', reason: '맥성 돌파 전열의 핵심 장수입니다.' },
|
|
{ unitId: 'wang-ping', reason: '북문과 산길의 좁은 통로를 읽습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '갈대 물목과 숲길 추격대를 끊습니다.' },
|
|
{ unitId: 'huang-zhong', reason: '성벽 너머 궁병을 견제합니다.' },
|
|
{ unitId: 'ma-dai', reason: '위군 기병의 측면 차단에 대응합니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 민심과 병사 사기를 붙듭니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '퇴로 명분', classKeys: ['lord'], reason: '유비가 있어야 관우군을 버리지 않는 선택이 설득됩니다.' },
|
|
{ label: '북문 전열', classKeys: ['spearman', 'infantry'], reason: '관우와 황충이 북문 앞 좁은 전열을 지탱합니다.' },
|
|
{ label: '지형 판정', classKeys: ['infantry', 'strategist'], reason: '왕평과 마량이 산길, 숲길, 갈대 물목의 위험을 읽습니다.' },
|
|
{ label: '기동 차단', classKeys: ['cavalry'], reason: '조운과 마대가 양쪽 추격대를 빠르게 끊습니다.' },
|
|
{ label: '원거리 견제', classKeys: ['archer'], reason: '황충이 물목 너머 궁병과 봉쇄선을 눌러 줍니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 24, y: 45, role: 'reserve', unitId: 'liu-bei', label: '퇴로 명분' },
|
|
{ x: 30, y: 38, role: 'front', unitId: 'guan-yu', label: '북문 전열' },
|
|
{ x: 37, y: 34, role: 'flank', unitId: 'wang-ping', label: '산길 판단' },
|
|
{ x: 33, y: 42, role: 'flank', unitId: 'zhao-yun', label: '물목 기동' },
|
|
{ x: 35, y: 36, role: 'front', unitId: 'huang-zhong', label: '궁병 견제' },
|
|
{ x: 43, y: 39, role: 'flank', unitId: 'ma-dai', label: '기병 차단' },
|
|
{ x: 31, y: 45, role: 'support', unitId: 'ma-liang', label: '사기 수습' }
|
|
],
|
|
note: '맥성 고립전은 성 안에서 오래 버티는 전투가 아니라 북문 밖 퇴로를 열어 관우군을 빼내는 전투입니다. 왕평으로 산길을 읽고, 조운·마대로 추격대를 끊으며, 황충으로 갈대 물목의 궁병을 눌러 주십시오.'
|
|
},
|
|
'forty-fifth-battle-yiling-vanguard': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '형주 상실 뒤 이릉으로 향하는 복수전의 주장입니다.' },
|
|
{ unitId: 'zhang-fei', reason: '중앙 숲길을 빠르게 밀어 여는 강한 선봉입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '강변과 숲길 사이를 오가며 늘어진 진영을 보강합니다.' },
|
|
{ unitId: 'ma-chao', reason: '오군 기병과 동쪽 전초선을 강하게 맞받아칩니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '육손의 기다림과 화공 위험을 먼저 읽습니다.' },
|
|
{ unitId: 'huang-quan', reason: '긴 보급로와 강변 진영의 안전을 계산합니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 잔병과 현지 민심을 다독이는 연결 축입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '복수전 주장', classKeys: ['lord'], reason: '유비 생존과 후방 진영 유지가 패배 조건을 안정시킵니다.' },
|
|
{ label: '숲길 돌파', classKeys: ['infantry', 'spearman'], reason: '중앙 숲길 전초선을 빠르게 밀어야 전선이 늘어지지 않습니다.' },
|
|
{ label: '강변 기동', classKeys: ['cavalry'], reason: '강변과 동쪽 전초선 사이의 긴 거리를 빠르게 메웁니다.' },
|
|
{ label: '원거리 견제', classKeys: ['archer'], reason: '숲그늘 궁병과 나루 수군을 안전하게 눌러 줍니다.' },
|
|
{ label: '보급 판단', classKeys: ['strategist'], reason: '보급점, 나루, 화공 위험을 읽어 다음 전투의 피해를 줄입니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 14, y: 36, role: 'reserve', unitId: 'liu-bei', label: '복수전 주장' },
|
|
{ x: 16, y: 39, role: 'front', unitId: 'zhang-fei', label: '숲길 선봉' },
|
|
{ x: 18, y: 34, role: 'flank', unitId: 'zhao-yun', label: '강변 기동' },
|
|
{ x: 23, y: 34, role: 'flank', unitId: 'ma-chao', label: '동쪽 압박' },
|
|
{ x: 8, y: 38, role: 'support', unitId: 'zhuge-liang', label: '화공 대비' },
|
|
{ x: 8, y: 33, role: 'support', unitId: 'huang-quan', label: '보급 계산' },
|
|
{ x: 13, y: 32, role: 'support', unitId: 'ma-liang', label: '잔병 수습' }
|
|
],
|
|
note: '이릉 진격로는 이기는 것보다 진영을 너무 길게 흩지 않는 것이 핵심입니다. 선봉으로 중앙 숲길을 열고, 기동 병종으로 강변과 동쪽 보급점을 이어 주십시오.'
|
|
},
|
|
'forty-sixth-battle-yiling-fire': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'liu-bei', reason: '불길 속에서 병사를 버리지 않고 퇴로를 여는 주장입니다.' },
|
|
{ unitId: 'huang-quan', reason: '군막 간격, 보급 장부, 서쪽 퇴로를 계산하는 핵심 참모입니다.' },
|
|
{ unitId: 'zhuge-liang', reason: '육손의 화공 의도와 다음 백제성 수습까지 읽습니다.' },
|
|
{ unitId: 'ma-liang', reason: '형주 잔병과 현지 민심을 모아 불길 속 사기를 붙잡습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '끊어진 군막 사이를 빠르게 오가며 흩어진 아군을 회수합니다.' },
|
|
{ unitId: 'wang-ping', reason: '숲길과 강변 퇴로의 지형 판단으로 빠져나갈 길을 찾습니다.' },
|
|
{ unitId: 'ma-chao', reason: '오군 추격 기병과 불길 바깥 차단선을 강하게 밀어냅니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '퇴로 주장', classKeys: ['lord'], reason: '유비를 중심으로 병사를 모아 패배 조건을 안정시킵니다.' },
|
|
{ label: '화공 판단', classKeys: ['strategist'], reason: '불길, 군막, 보급로를 동시에 읽어야 피해가 줄어듭니다.' },
|
|
{ label: '퇴로 기동', classKeys: ['cavalry'], reason: '끊어진 군막과 서쪽 퇴로 사이를 빠르게 이어 줍니다.' },
|
|
{ label: '불길 돌파', classKeys: ['infantry', 'spearman'], reason: '중앙 군막을 수습하고 화공 지휘선까지 버틸 전열이 필요합니다.' },
|
|
{ label: '강변 견제', classKeys: ['archer'], reason: '강변 수군과 숲그늘 궁병을 멀리서 눌러 줍니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 18, y: 41, role: 'reserve', unitId: 'liu-bei', label: '퇴로 주장' },
|
|
{ x: 10, y: 37, role: 'support', unitId: 'huang-quan', label: '보급 계산' },
|
|
{ x: 12, y: 43, role: 'support', unitId: 'zhuge-liang', label: '화공 판독' },
|
|
{ x: 17, y: 35, role: 'support', unitId: 'ma-liang', label: '사기 수습' },
|
|
{ x: 24, y: 38, role: 'flank', unitId: 'zhao-yun', label: '군막 회수' },
|
|
{ x: 24, y: 34, role: 'flank', unitId: 'wang-ping', label: '퇴로 판단' },
|
|
{ x: 31, y: 39, role: 'front', unitId: 'ma-chao', label: '차단선 돌파' }
|
|
],
|
|
note: '이릉 화공전은 불길을 모두 끄는 전투가 아닙니다. 유비를 살리고, 중앙 군막을 수습한 뒤, 서쪽 퇴로로 병사를 모아 백제성 방향을 여십시오.'
|
|
},
|
|
'forty-seventh-battle-nanzhong-stabilization': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '백제성 이후 촉한을 이끄는 섭정이며 패배 조건의 중심입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '서쪽 산길 선봉과 후위 회수에 안정적인 기동 축입니다.' },
|
|
{ unitId: 'wang-ping', reason: '남중 숲길과 강변 지형을 읽어 매복을 줄이는 길잡이입니다.' },
|
|
{ unitId: 'ma-liang', reason: '마을 회유와 남중 민심 판단으로 평정 명분을 세웁니다.' },
|
|
{ unitId: 'huang-quan', reason: '창고와 보급 장부를 안정시켜 다음 전선의 군량을 남깁니다.' },
|
|
{ unitId: 'huang-zhong', reason: '밀림 궁병과 고지 방어선을 견제하는 원거리 축입니다.' },
|
|
{ unitId: 'ma-chao', reason: '남중 기병과 빠른 차단선에 맞서는 강한 측면 압박입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '섭정 지휘', classKeys: ['strategist'], reason: '제갈량 생존과 회유 방침이 전투의 중심입니다.' },
|
|
{ label: '산길 선봉', classKeys: ['infantry', 'spearman'], reason: '숲길과 마을 입구를 차근차근 열어야 합니다.' },
|
|
{ label: '길잡이 기동', classKeys: ['cavalry'], reason: '계곡과 강변 사이의 넓은 거리를 빠르게 이어 줍니다.' },
|
|
{ label: '고지 견제', classKeys: ['archer'], reason: '밀림 궁병과 고지 방어선을 먼저 눌러 마을 진입을 돕습니다.' },
|
|
{ label: '회유 보급', classKeys: ['strategist'], reason: '마을과 창고를 약탈하지 않고 안정시키는 판단이 필요합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 12, y: 47, role: 'support', unitId: 'zhuge-liang', label: '섭정 지휘' },
|
|
{ x: 20, y: 46, role: 'front', unitId: 'zhao-yun', label: '산길 선봉' },
|
|
{ x: 24, y: 43, role: 'flank', unitId: 'wang-ping', label: '길잡이' },
|
|
{ x: 16, y: 44, role: 'support', unitId: 'ma-liang', label: '마을 회유' },
|
|
{ x: 10, y: 45, role: 'support', unitId: 'huang-quan', label: '창고 장부' },
|
|
{ x: 23, y: 48, role: 'flank', unitId: 'huang-zhong', label: '고지 견제' },
|
|
{ x: 31, y: 47, role: 'front', unitId: 'ma-chao', label: '기병 압박' }
|
|
],
|
|
note: '남중 안정로는 적을 전부 베는 전투가 아닙니다. 제갈량을 지키며 산길, 마을, 창고를 순서대로 안정시키고 맹획 본대전으로 이어질 회유 명분을 남기십시오.'
|
|
},
|
|
'forty-eighth-battle-meng-huo-main-force': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '맹획 생포와 회유 방침을 직접 지휘하는 필수 장수입니다.' },
|
|
{ unitId: 'ma-liang', reason: '회유 문서와 마을 설득의 핵심 참모입니다.' },
|
|
{ unitId: 'wang-ping', reason: '늪지와 산채 길을 읽어 복병과 포로 수용소를 안정시킵니다.' },
|
|
{ unitId: 'huang-quan', reason: '포로 명부와 군량 장부를 나누어 약탈 없는 전후 처리를 돕습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '맹수 기병의 첫 돌격을 받아 내고 후위를 회수합니다.' },
|
|
{ unitId: 'ma-chao', reason: '강한 기병 압박으로 맹획의 측면 돌파를 묶습니다.' },
|
|
{ unitId: 'huang-zhong', reason: '고지 궁병과 주술사를 안정적으로 견제합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '회유 지휘', classKeys: ['strategist'], reason: '생포와 석방 방침을 유지하는 전투의 중심입니다.' },
|
|
{ label: '생포망 전열', classKeys: ['infantry', 'spearman'], reason: '맹획 본대가 빠져나가지 못하게 전열을 닫아야 합니다.' },
|
|
{ label: '맹수 대응', classKeys: ['cavalry'], reason: '맹수 기병과 측면 돌파를 빠르게 받아칩니다.' },
|
|
{ label: '고지 견제', classKeys: ['archer'], reason: '고지 궁병과 주술사를 눌러 포로장 피해를 줄입니다.' },
|
|
{ label: '포로 장부', classKeys: ['strategist'], reason: '포로와 군량을 구분해 약탈 없는 전후 처리를 보여 줍니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 10, y: 54, role: 'support', unitId: 'zhuge-liang', label: '회유 지휘' },
|
|
{ x: 14, y: 51, role: 'support', unitId: 'ma-liang', label: '마을 설득' },
|
|
{ x: 25, y: 50, role: 'flank', unitId: 'wang-ping', label: '늪길 판독' },
|
|
{ x: 8, y: 52, role: 'support', unitId: 'huang-quan', label: '포로 장부' },
|
|
{ x: 20, y: 53, role: 'front', unitId: 'zhao-yun', label: '생포망 선봉' },
|
|
{ x: 34, y: 53, role: 'front', unitId: 'ma-chao', label: '맹수 대응' },
|
|
{ x: 23, y: 55, role: 'flank', unitId: 'huang-zhong', label: '고지 견제' }
|
|
],
|
|
note: '맹획 본대전은 적장을 베는 전투가 아닙니다. 마을과 포로 수용소를 지킨 뒤 맹획을 생포해 풀어 주어, 다음 칠종칠금 전투의 설득 흐름을 만드십시오.'
|
|
},
|
|
'forty-ninth-battle-meng-huo-second-capture': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '두 번째 생포와 석방 명분을 유지하는 필수 지휘관입니다.' },
|
|
{ unitId: 'huang-quan', reason: '포로 안심 장부와 군율 유지로 회유의 신뢰를 쌓습니다.' },
|
|
{ unitId: 'wang-ping', reason: '늪지와 강가 우회로를 읽어 맹획의 재매복을 끊습니다.' },
|
|
{ unitId: 'ma-liang', reason: '마을 사람들에게 두 번째 석방의 뜻을 설명하는 연결 축입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '맹수 재돌격을 받아 내고 생포망의 옆구리를 지킵니다.' },
|
|
{ unitId: 'huang-zhong', reason: '고지 궁병과 주술사를 억눌러 포위망이 흐트러지지 않게 합니다.' },
|
|
{ unitId: 'ma-chao', reason: '맹획의 측면 돌파와 재집결 기병을 강하게 묶어 둡니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '재석방 지휘', classKeys: ['strategist'], reason: '두 번째 생포 뒤에도 회유 명분이 무너지지 않게 합니다.' },
|
|
{ label: '포로장 장부', classKeys: ['strategist'], reason: '포로와 군량 기록을 지켜 군율을 증명합니다.' },
|
|
{ label: '늪길 판독', classKeys: ['infantry', 'spearman'], reason: '늪지와 강가 우회로의 매복을 줄입니다.' },
|
|
{ label: '맹수 재돌격', classKeys: ['cavalry'], reason: '재집결 기병과 맹수 돌파를 빠르게 받아칩니다.' },
|
|
{ label: '고지 억제', classKeys: ['archer'], reason: '고지 궁병과 주술사를 눌러 생포망을 안정시킵니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 11, y: 56, role: 'support', unitId: 'zhuge-liang', label: '재석방 지휘' },
|
|
{ x: 8, y: 54, role: 'support', unitId: 'huang-quan', label: '포로 장부' },
|
|
{ x: 27, y: 52, role: 'flank', unitId: 'wang-ping', label: '늪길 판독' },
|
|
{ x: 15, y: 53, role: 'support', unitId: 'ma-liang', label: '마을 설명' },
|
|
{ x: 22, y: 56, role: 'front', unitId: 'zhao-yun', label: '재돌격 저지' },
|
|
{ x: 25, y: 57, role: 'flank', unitId: 'huang-zhong', label: '고지 억제' },
|
|
{ x: 37, y: 56, role: 'front', unitId: 'ma-chao', label: '측면 봉쇄' }
|
|
],
|
|
note: '칠종칠금 2차전은 같은 생포를 반복하더라도 신뢰를 잃지 않는 전투입니다. 포로장 장부와 회유 마을을 지키며 맹획을 다시 잡아 풀어 보낼 명분을 남기십시오.'
|
|
},
|
|
'fiftieth-battle-meng-huo-third-capture': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '세 번째 생포와 호족 회유 명분을 세우는 필수 지휘관입니다.' },
|
|
{ unitId: 'ma-liang', reason: '흔들리는 호족에게 회유 문장을 전하는 핵심 참모입니다.' },
|
|
{ unitId: 'huang-quan', reason: '포로와 군량 장부를 안정시켜 촉한군의 군율을 증명합니다.' },
|
|
{ unitId: 'wang-ping', reason: '깊은 산채와 늪지 길목을 읽어 호족 매복을 끊습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '맹수 돌격을 받아 내며 생포망의 중심을 지킵니다.' },
|
|
{ unitId: 'ma-chao', reason: '남만 기병과 축융 선봉의 측면 돌파를 강하게 묶습니다.' },
|
|
{ unitId: 'huang-zhong', reason: '고지 궁병과 독천 주술사를 안전하게 견제합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '호족 회유', classKeys: ['strategist'], reason: '흔들리는 호족에게 물러설 명분을 줍니다.' },
|
|
{ label: '장부 신뢰', classKeys: ['strategist'], reason: '포로와 군량 기록을 지켜 촉한군의 군율을 보여 줍니다.' },
|
|
{ label: '늪지 매복 차단', classKeys: ['infantry', 'spearman'], reason: '마을 뒤 길목으로 돌아드는 매복을 줄입니다.' },
|
|
{ label: '축융 선봉 억제', classKeys: ['cavalry'], reason: '불창 선봉과 맹수대를 빠르게 받아칩니다.' },
|
|
{ label: '독천 주술 견제', classKeys: ['archer'], reason: '고지 궁병과 주술사를 눌러 회유 목표를 지킵니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 12, y: 58, role: 'support', unitId: 'zhuge-liang', label: '호족 회유 지휘' },
|
|
{ x: 16, y: 55, role: 'support', unitId: 'ma-liang', label: '회유 문장' },
|
|
{ x: 9, y: 56, role: 'support', unitId: 'huang-quan', label: '포로 장부' },
|
|
{ x: 28, y: 54, role: 'flank', unitId: 'wang-ping', label: '늪길 차단' },
|
|
{ x: 23, y: 58, role: 'front', unitId: 'zhao-yun', label: '생포망 중심' },
|
|
{ x: 38, y: 58, role: 'front', unitId: 'ma-chao', label: '선봉 억제' },
|
|
{ x: 26, y: 59, role: 'flank', unitId: 'huang-zhong', label: '독천 견제' }
|
|
],
|
|
note: '칠종칠금 3차전은 맹획뿐 아니라 그를 따르는 호족들이 보는 싸움입니다. 마을 회유, 포로장 신뢰, 늪지 차단을 먼저 세우고 맹획을 세 번째로 생포하십시오.'
|
|
},
|
|
'fifty-first-battle-meng-huo-fourth-capture': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '네 번째 생포와 회유 퇴로 유지의 필수 지휘관입니다.' },
|
|
{ unitId: 'ma-liang', reason: '세 번째 생포 뒤 만든 호족 설득문을 전장 안에서 이어 갑니다.' },
|
|
{ unitId: 'huang-quan', reason: '보증 장부로 회유 퇴로와 포로 귀환 약속을 지킵니다.' },
|
|
{ unitId: 'wang-ping', reason: '열어 둔 산길과 독천 우회로를 읽어 호족 퇴로를 확보합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '등갑병과 맹수대가 회유로를 덮치지 못하게 중심을 지킵니다.' },
|
|
{ unitId: 'ma-chao', reason: '남만 기병을 강하게 압박하되 추격선을 넘지 않게 합니다.' },
|
|
{ unitId: 'ma-dai', reason: '마초와 함께 회유 퇴로 주변의 빠른 차단과 회수를 맡습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '회유 퇴로 지휘', classKeys: ['strategist'], reason: '물러설 호족에게 남길 말과 길을 유지합니다.' },
|
|
{ label: '보증 장부', classKeys: ['strategist'], reason: '포로 귀환과 회유 퇴로 약속을 기록으로 지킵니다.' },
|
|
{ label: '독천 우회로', classKeys: ['infantry', 'spearman'], reason: '퇴로 뒤 매복과 감시병을 끊습니다.' },
|
|
{ label: '등갑 추격선', classKeys: ['cavalry'], reason: '등갑병과 맹수대의 추격을 빠르게 조절합니다.' },
|
|
{ label: '화덕 견제', classKeys: ['archer'], reason: '화덕 요새 주변 제사장과 궁병을 안전하게 누릅니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 13, y: 60, role: 'support', unitId: 'zhuge-liang', label: '퇴로 지휘' },
|
|
{ x: 17, y: 57, role: 'support', unitId: 'ma-liang', label: '호족 문서' },
|
|
{ x: 10, y: 58, role: 'support', unitId: 'huang-quan', label: '보증 장부' },
|
|
{ x: 29, y: 56, role: 'flank', unitId: 'wang-ping', label: '독천 우회' },
|
|
{ x: 24, y: 60, role: 'front', unitId: 'zhao-yun', label: '퇴로 중심' },
|
|
{ x: 39, y: 60, role: 'front', unitId: 'ma-chao', label: '등갑 압박' },
|
|
{ x: 42, y: 65, role: 'flank', unitId: 'ma-dai', label: '추격선 회수' }
|
|
],
|
|
note: '칠종칠금 4차전은 생포만큼이나 회유 퇴로와 보증 장부가 중요합니다. 화덕 요새를 누르되 퇴로를 막지 않는 편성을 고르십시오.'
|
|
},
|
|
'fifty-second-battle-meng-huo-fifth-capture': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '다섯 번째 생포와 강경파 분리 명분을 직접 지휘하는 필수 장수입니다.' },
|
|
{ unitId: 'huang-quan', reason: '강경파와 회유파를 장부로 분리해 군율과 약속을 동시에 지킵니다.' },
|
|
{ unitId: 'ma-liang', reason: '물러설 호족에게 남길 말을 정리해 마을 회유 목표를 돕습니다.' },
|
|
{ unitId: 'wang-ping', reason: '죽림과 협곡의 분리로를 읽어 회유 퇴로를 확보합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '맹수 추격대가 회유로를 짓밟지 못하게 중심을 받쳐 줍니다.' },
|
|
{ unitId: 'huang-zhong', reason: '고지 궁병과 제사장을 억제해 강경파 요새 공략을 안정시킵니다.' },
|
|
{ unitId: 'ma-dai', reason: '마초와 다른 빠른 우회로를 맡아 분리로 주변 추격선을 끊습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '강경파 분리', classKeys: ['strategist'], reason: '싸울 자와 물러설 자를 장부와 말로 구분합니다.' },
|
|
{ label: '회유 마을 보호', classKeys: ['strategist', 'archer'], reason: '마을 보증을 지키고 고지 사격을 억제합니다.' },
|
|
{ label: '죽림 분리로', classKeys: ['infantry', 'spearman'], reason: '협곡과 죽림 사이 매복을 걷어 냅니다.' },
|
|
{ label: '맹수 추격 차단', classKeys: ['cavalry'], reason: '빠른 추격대가 회유로를 덮치지 못하게 끊습니다.' },
|
|
{ label: '요새 견제', classKeys: ['archer', 'spearman'], reason: '강경파 요새의 궁병과 제사장을 안정적으로 누릅니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 14, y: 62, role: 'support', unitId: 'zhuge-liang', label: '분리 지휘' },
|
|
{ x: 11, y: 60, role: 'support', unitId: 'huang-quan', label: '장부 구분' },
|
|
{ x: 18, y: 59, role: 'support', unitId: 'ma-liang', label: '마을 설득' },
|
|
{ x: 30, y: 58, role: 'flank', unitId: 'wang-ping', label: '죽림 분리' },
|
|
{ x: 25, y: 62, role: 'front', unitId: 'zhao-yun', label: '중앙 회유로' },
|
|
{ x: 28, y: 63, role: 'front', unitId: 'huang-zhong', label: '고지 견제' },
|
|
{ x: 43, y: 67, role: 'flank', unitId: 'ma-dai', label: '추격 차단' }
|
|
],
|
|
note: '칠종칠금 5차전은 맹획 생포에 더해 강경파 호족을 본대에서 떼어 내는 전투입니다. 마을 회유, 장부 분리, 퇴로 유지, 추격 억제를 맡을 장수를 균형 있게 고르십시오.'
|
|
},
|
|
'fifty-third-battle-meng-huo-sixth-capture': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '제갈량은 필수 출전입니다. 맹획의 자존심을 꺾되 남중 전체를 꺾지 않는 지휘가 필요합니다.' },
|
|
{ unitId: 'ma-liang', reason: '마량은 마을 신뢰와 촌장 설득을 맡아 여섯 번째 생포의 명분을 세웁니다.' },
|
|
{ unitId: 'huang-quan', reason: '황권은 신뢰 장부를 정리해 처벌과 약속의 선을 분명히 합니다.' },
|
|
{ unitId: 'wang-ping', reason: '왕평은 깊은 계곡길을 읽어 퇴로와 추격 차단선을 동시에 잡습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '조운은 빠르게 움직여 잔당의 마을 습격을 끊을 수 있습니다.' },
|
|
{ unitId: 'wei-yan', reason: '위연은 거친 돌파로 강경파 잔당의 요새선을 흔듭니다.' },
|
|
{ unitId: 'ma-dai', reason: '마대는 남중 기동대의 움직임을 따라잡고 계곡 측면을 막습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '마을 신뢰', classKeys: ['strategist'], reason: '촌장 설득과 신뢰 장부를 전장 안에서 유지합니다.' },
|
|
{ label: '계곡 퇴로', classKeys: ['infantry', 'spearman'], reason: '절벽과 물길 사이 퇴로를 끊기지 않게 합니다.' },
|
|
{ label: '잔당 요새 돌파', classKeys: ['spearman', 'cavalry'], reason: '강경파 잔당의 요새선을 빠르게 흔듭니다.' },
|
|
{ label: '마을 습격 차단', classKeys: ['cavalry'], reason: '맹수대와 기동대가 마을로 번지기 전에 끊습니다.' },
|
|
{ label: '절벽 사격 억제', classKeys: ['archer'], reason: '고지 궁병과 제사장의 시야를 안정적으로 누릅니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 15, y: 64, role: 'support', unitId: 'zhuge-liang', label: '신뢰 지휘' },
|
|
{ x: 19, y: 61, role: 'support', unitId: 'ma-liang', label: '촌장 설득' },
|
|
{ x: 12, y: 62, role: 'support', unitId: 'huang-quan', label: '신뢰 장부' },
|
|
{ x: 31, y: 60, role: 'flank', unitId: 'wang-ping', label: '계곡 퇴로' },
|
|
{ x: 26, y: 64, role: 'front', unitId: 'zhao-yun', label: '마을 차단' },
|
|
{ x: 34, y: 69, role: 'front', unitId: 'wei-yan', label: '요새 돌파' },
|
|
{ x: 44, y: 69, role: 'flank', unitId: 'ma-dai', label: '측면 회수' }
|
|
],
|
|
note: '칠종칠금 6차전은 맹획의 남은 자존심을 마주하는 전투입니다. 공격력만 높이기보다 마을 신뢰, 신뢰 장부, 계곡 퇴로, 잔당 차단 역할을 균형 있게 고르십시오.'
|
|
},
|
|
'fifty-fourth-battle-meng-huo-final-capture': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '제갈량은 필수 출전입니다. 일곱 번째 생포를 항복과 남중 평정으로 완성해야 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '황권은 일곱 번의 약속을 장부로 남겨 항복 조건을 분명히 합니다.' },
|
|
{ unitId: 'ma-liang', reason: '마량은 촌장들이 보는 회의장에서 남중이 받아들일 말을 고릅니다.' },
|
|
{ unitId: 'wang-ping', reason: '왕평은 회의장 길과 마을길을 나누어 군율이 흔들리지 않게 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '조운은 빠른 기동으로 항복로를 해치려는 잔당을 끊습니다.' },
|
|
{ unitId: 'ma-dai', reason: '마대는 남중 기동대를 따라잡되 항복로를 밟지 않는 절제가 필요합니다.' },
|
|
{ unitId: 'huang-zhong', reason: '황충은 고지 궁병과 주술사를 멀리서 억제해 회의장 압박을 줄입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '항복 회의장', classKeys: ['strategist'], reason: '회의장 길과 항복 조건을 지휘합니다.' },
|
|
{ label: '약속 장부', classKeys: ['strategist'], reason: '일곱 번의 석방과 보증을 기록으로 남깁니다.' },
|
|
{ label: '촌장 증언 마을', classKeys: ['infantry', 'archer'], reason: '마을이 전투에 휘말리지 않게 보호합니다.' },
|
|
{ label: '항복로 차단선', classKeys: ['cavalry'], reason: '잔당 기동대가 회의장 길을 흔들기 전에 끊습니다.' },
|
|
{ label: '최후 요새 견제', classKeys: ['archer', 'spearman'], reason: '잔당 요새의 사격과 주술을 안정적으로 누릅니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 16, y: 66, role: 'support', unitId: 'zhuge-liang', label: '회의 지휘' },
|
|
{ x: 13, y: 64, role: 'support', unitId: 'huang-quan', label: '약속 장부' },
|
|
{ x: 20, y: 63, role: 'support', unitId: 'ma-liang', label: '촌장 증언' },
|
|
{ x: 32, y: 62, role: 'flank', unitId: 'wang-ping', label: '회의장 길' },
|
|
{ x: 27, y: 66, role: 'front', unitId: 'zhao-yun', label: '항복로 차단' },
|
|
{ x: 45, y: 71, role: 'flank', unitId: 'ma-dai', label: '측면 절제' },
|
|
{ x: 30, y: 67, role: 'front', unitId: 'huang-zhong', label: '요새 견제' }
|
|
],
|
|
note: '칠종칠금 7차전은 남중 평정의 결말입니다. 맹획을 마지막으로 생포하되, 촌장 증언 마을, 항복 회의장 길, 최후 잔당 요새를 함께 관리할 편성을 고르십시오.'
|
|
},
|
|
'fifty-fifth-battle-northern-qishan-road': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '북벌 첫 실전의 총지휘관입니다. 출진로, 보급, 민심 목표를 동시에 판단해야 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '기산 산길의 빠른 정찰과 위군 기병 차단에 어울리는 기동 담당입니다.' },
|
|
{ unitId: 'huang-quan', reason: '한중 보급 캠프와 장부 목표를 안정시키는 보급 판단 담당입니다.' },
|
|
{ unitId: 'ma-liang', reason: '서쪽 마을 안정과 민심 수습 목표에서 보조 효과가 큽니다.' },
|
|
{ unitId: 'wang-ping', reason: '산길과 능선 지형을 읽어 적의 우회 공격을 막는 데 좋습니다.' },
|
|
{ unitId: 'ma-dai', reason: '마초군 계열의 빠른 기동으로 위군 추격 기병을 끊을 수 있습니다.' },
|
|
{ unitId: 'wei-yan', reason: '좁은 길에서 돌파력이 필요할 때 전초 보병과 요새 수비를 흔들 수 있습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '북벌 총지휘', classKeys: ['strategist'], reason: '제갈량이 후방에서 산길, 보급, 민심 목표를 묶어 봅니다.' },
|
|
{ label: '산길 돌파', classKeys: ['infantry', 'spearman'], reason: '능선 보병과 요새 수비를 차례로 밀어 올립니다.' },
|
|
{ label: '기병 차단', classKeys: ['cavalry'], reason: '위군 추격 기병이 보급 캠프를 돌아치기 전에 끊습니다.' },
|
|
{ label: '보급 안정', classKeys: ['strategist'], reason: '한중 보급 캠프와 장부 목표를 흔들리지 않게 관리합니다.' },
|
|
{ label: '마을 민심', classKeys: ['archer', 'strategist'], reason: '서쪽 마을 주변을 정리해 북벌군의 명분을 세웁니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 17, y: 72, role: 'support', unitId: 'zhuge-liang', label: '북벌 지휘' },
|
|
{ x: 13, y: 70, role: 'support', unitId: 'huang-quan', label: '보급 장부' },
|
|
{ x: 21, y: 69, role: 'support', unitId: 'ma-liang', label: '마을 안정' },
|
|
{ x: 27, y: 72, role: 'front', unitId: 'zhao-yun', label: '산길 정찰' },
|
|
{ x: 34, y: 68, role: 'flank', unitId: 'wang-ping', label: '능선 전열' },
|
|
{ x: 49, y: 78, role: 'flank', unitId: 'ma-dai', label: '기병 차단' },
|
|
{ x: 38, y: 76, role: 'front', unitId: 'wei-yan', label: '전초 돌파' }
|
|
],
|
|
note: '북벌부터는 유비가 후방에 남고 제갈량이 직접 출진을 지휘합니다. 새로 합류한 무장들 중 기동, 보급, 산길 수비, 민심 수습 역할을 골라 7명을 구성하십시오.'
|
|
},
|
|
'fifty-sixth-battle-tianshui-advance': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '천수 진군과 강유의 움직임을 직접 판단해야 하는 총지휘관입니다.' },
|
|
{ unitId: 'wang-ping', reason: '가정으로 이어지는 고개와 물길이 먼 지형을 읽는 핵심 장수입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '강유와 위군 기병의 빠른 움직임에 대응할 기동 담당입니다.' },
|
|
{ unitId: 'huang-quan', reason: '천수 길목의 보급선과 물길 확보 목표에서 안정적인 판단을 제공합니다.' },
|
|
{ unitId: 'ma-liang', reason: '천수 마을과 강유 접촉의 명분을 부드럽게 정리할 문사입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '위군 방어선의 빈틈과 장수들의 속내를 읽는 책략 담당입니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 감각으로 우회 기병과 물목 차단에 대응하기 좋습니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '천수 총지휘', classKeys: ['strategist'], reason: '제갈량이 물길, 고개, 강유 관찰 목표를 함께 판단합니다.' },
|
|
{ label: '가정 고개 확인', classKeys: ['infantry', 'spearman'], reason: '가정으로 이어지는 고개를 무리 없이 밀고 퇴로를 남깁니다.' },
|
|
{ label: '강유 기동 대응', classKeys: ['cavalry'], reason: '강유와 위군 기병의 빠른 압박을 따라잡되 깊게 물리지 않습니다.' },
|
|
{ label: '물길 마을 안정', classKeys: ['strategist', 'archer'], reason: '마을 주변을 안정시켜 북벌군의 명분과 보급을 지킵니다.' },
|
|
{ label: '방어선 책략', classKeys: ['strategist'], reason: '곽회의 방어선과 군리의 빈틈을 읽어 진군로를 엽니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 36, y: 74, role: 'support', unitId: 'zhuge-liang', label: '천수 지휘' },
|
|
{ x: 45, y: 69, role: 'front', unitId: 'wang-ping', label: '고개 판단' },
|
|
{ x: 42, y: 72, role: 'front', unitId: 'zhao-yun', label: '강유 대응' },
|
|
{ x: 35, y: 71, role: 'support', unitId: 'huang-quan', label: '물길 보급' },
|
|
{ x: 39, y: 68, role: 'support', unitId: 'ma-liang', label: '마을 명분' },
|
|
{ x: 25, y: 70, role: 'support', unitId: 'fa-zheng', label: '방어선 파악' },
|
|
{ x: 48, y: 72, role: 'flank', unitId: 'ma-dai', label: '우회 차단' }
|
|
],
|
|
note: '천수 진군로는 길을 뚫는 전투이면서 강유의 재능을 처음 확인하는 전장입니다. 제갈량을 중심으로 지형, 기동, 보급, 설득 역할을 7명 안에 담으십시오.'
|
|
},
|
|
'fifty-seventh-battle-jieting-crisis': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '가정 배치 위기의 총지휘관입니다. 고지와 물길 판단을 끝까지 조율해야 합니다.' },
|
|
{ unitId: 'wang-ping', reason: '가정의 물길과 고지 배치를 읽는 핵심 장수입니다. 왕평 생존 목표도 직접 걸려 있습니다.' },
|
|
{ unitId: 'ma-liang', reason: '마속의 고집과 강유 접촉을 말로 풀어 다음 전선의 명분을 지킵니다.' },
|
|
{ unitId: 'huang-quan', reason: '물길과 수레 길 보급을 장부로 붙잡아 장기전 붕괴를 막습니다.' },
|
|
{ unitId: 'fa-zheng', reason: '장합의 차단선과 위군 군리의 빈틈을 읽는 책략 담당입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '위군 기병의 우회와 빠른 압박을 끊고 퇴로를 회수합니다.' },
|
|
{ unitId: 'ma-dai', reason: '좁은 고개에서 기병을 지나치게 깊이 보내지 않고 되돌리는 역할에 어울립니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '가정 총지휘', classKeys: ['strategist'], reason: '제갈량이 물길, 고지, 강유 접촉 목표를 함께 판단합니다.' },
|
|
{ label: '물길 판단', classKeys: ['infantry', 'spearman'], reason: '왕평처럼 버틸 수 있는 전열이 물길과 진영 사이를 잡아야 합니다.' },
|
|
{ label: '퇴로 회수', classKeys: ['cavalry'], reason: '장합 기병을 깊이 추격하지 않고 끊어 내는 기동 역할이 필요합니다.' },
|
|
{ label: '군량 장부', classKeys: ['strategist', 'archer'], reason: '수레 길과 후방 진영을 안정시켜 장기전 붕괴를 막습니다.' },
|
|
{ label: '접촉 명분', classKeys: ['strategist'], reason: '강유가 돌아설 수 있도록 백성과 물길을 해치지 않는 전투 흐름을 만듭니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 38, y: 79, role: 'support', unitId: 'zhuge-liang', label: '가정 지휘' },
|
|
{ x: 58, y: 72, role: 'front', unitId: 'wang-ping', label: '물길 판단' },
|
|
{ x: 42, y: 80, role: 'front', unitId: 'zhao-yun', label: '퇴로 회수' },
|
|
{ x: 38, y: 81, role: 'support', unitId: 'huang-quan', label: '수레 장부' },
|
|
{ x: 39, y: 76, role: 'support', unitId: 'ma-liang', label: '강유 접촉' },
|
|
{ x: 26, y: 74, role: 'support', unitId: 'fa-zheng', label: '차단선 분석' },
|
|
{ x: 55, y: 80, role: 'flank', unitId: 'ma-dai', label: '측면 회수' }
|
|
],
|
|
note: '가정 전투는 공격력이 높은 장수만으로는 버티기 어렵습니다. 제갈량과 왕평을 중심으로 물길, 수레 길, 설득, 기동 회수 역할을 7명 안에 담으십시오.'
|
|
},
|
|
'fifty-eighth-battle-qishan-retreat': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '기산 후퇴로의 총지휘관입니다. 무리한 추격보다 군량 수레와 병사를 살리는 판단이 필요합니다.' },
|
|
{ unitId: 'jiang-wei', reason: '합류 후 첫 출전 후보입니다. 천수의 낮은 길을 알아 퇴각로 확보와 새 공명 조합을 만듭니다.' },
|
|
{ unitId: 'wang-ping', reason: '물길과 수레 길을 함께 보는 장수입니다. 후퇴전에서 보급선 붕괴를 막는 핵심입니다.' },
|
|
{ unitId: 'zhao-yun', reason: '장합의 빠른 추격을 끊고 흩어진 병사를 회수하는 기동 담당입니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 감각으로 좁은 길을 우회하는 위군 기병을 늦출 수 있습니다.' },
|
|
{ unitId: 'huang-quan', reason: '군량 장부와 후방 진영 수습 목표를 안정시키는 보급 판단 담당입니다.' },
|
|
{ unitId: 'ma-liang', reason: '강유의 첫 출전과 후퇴 명분을 병사들에게 설득력 있게 정리합니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '후퇴 총지휘', classKeys: ['strategist'], reason: '제갈량이 전선을 길게 늘이지 않고 퇴로 목표를 우선합니다.' },
|
|
{ label: '낮은 길 안내', classKeys: ['infantry', 'spearman'], reason: '강유와 왕평이 수레 길과 물길 사이의 안정된 길을 찾습니다.' },
|
|
{ label: '추격 차단', classKeys: ['cavalry'], reason: '장합 기병을 늦추되 깊이 추격하지 않는 회수 기동이 필요합니다.' },
|
|
{ label: '군량 보호', classKeys: ['strategist', 'archer'], reason: '수레 길 주변을 안정시키고 후방 진영을 무너뜨리지 않습니다.' },
|
|
{ label: '후방 수습', classKeys: ['strategist'], reason: '병사와 장부를 살려 다음 북벌을 준비하는 역할입니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 18, y: 81, role: 'support', unitId: 'zhuge-liang', label: '후퇴 지휘' },
|
|
{ x: 33, y: 78, role: 'front', unitId: 'jiang-wei', label: '낮은 길' },
|
|
{ x: 40, y: 75, role: 'front', unitId: 'wang-ping', label: '물길 판단' },
|
|
{ x: 32, y: 82, role: 'front', unitId: 'zhao-yun', label: '추격 차단' },
|
|
{ x: 56, y: 87, role: 'flank', unitId: 'ma-dai', label: '측면 회수' },
|
|
{ x: 12, y: 80, role: 'support', unitId: 'huang-quan', label: '군량 장부' },
|
|
{ x: 23, y: 78, role: 'support', unitId: 'ma-liang', label: '병사 수습' }
|
|
],
|
|
note: '기산 후퇴로는 강유를 처음 편성에 넣는 전투입니다. 제갈량을 중심으로 강유, 왕평, 조운, 마대, 보급·문서 담당 중 7명을 골라 퇴로를 지키십시오.'
|
|
},
|
|
'fifty-ninth-battle-chencang-siege': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '진창 공성전의 총지휘관입니다. 성벽 압박과 보급 철수 시점을 함께 계산해야 합니다.' },
|
|
{ unitId: 'jiang-wei', reason: '진창 길과 천수 방면 지형을 잇는 새 길잡이입니다. 성문보다 보급로를 먼저 읽습니다.' },
|
|
{ unitId: 'wang-ping', reason: '공성 진영과 후방 표식을 안정시키는 장수입니다. 오래 끄는 성채전에서 진영 붕괴를 막습니다.' },
|
|
{ unitId: 'wei-yan', reason: '성문 압박을 맡길 돌파 장수입니다. 다만 무리한 전진을 보급 판단과 함께 제어해야 합니다.' },
|
|
{ unitId: 'ma-dai', reason: '왕쌍의 기병 추격과 우회를 늦추는 기병 담당입니다. 성벽보다 보급로 측면을 먼저 지킵니다.' },
|
|
{ unitId: 'huang-quan', reason: '진창 공성전의 군량 제한을 계산하는 보급 담당입니다. 장기전의 위험을 줄입니다.' },
|
|
{ unitId: 'fa-zheng', reason: '학소의 성벽 대응과 위군 군리의 빈틈을 읽는 책략 담당입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '공성 총지휘', classKeys: ['strategist'], reason: '제갈량이 성문 압박, 보급, 철수 시점을 한 번에 조율합니다.' },
|
|
{ label: '성문 압박', classKeys: ['infantry', 'spearman'], reason: '위연과 강유가 앞길을 열되 성벽 아래에서 고립되지 않게 버팁니다.' },
|
|
{ label: '추격 차단', classKeys: ['cavalry'], reason: '왕쌍의 기병이 군량 수레 길을 찌르기 전에 측면에서 늦춥니다.' },
|
|
{ label: '진영 유지', classKeys: ['infantry', 'archer'], reason: '왕평이 공성 진영과 후방 표식을 붙들어 전열이 길어지는 것을 막습니다.' },
|
|
{ label: '군량 계책', classKeys: ['strategist'], reason: '황권과 법정이 장기 공성의 시간을 계산하고 무리한 돌파를 제어합니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 44, y: 82, role: 'support', unitId: 'zhuge-liang', label: '공성 지휘' },
|
|
{ x: 48, y: 80, role: 'front', unitId: 'jiang-wei', label: '진창 길' },
|
|
{ x: 50, y: 78, role: 'front', unitId: 'wang-ping', label: '진영 표식' },
|
|
{ x: 52, y: 82, role: 'front', unitId: 'wei-yan', label: '성문 압박' },
|
|
{ x: 56, y: 86, role: 'flank', unitId: 'ma-dai', label: '왕쌍 차단' },
|
|
{ x: 44, y: 84, role: 'support', unitId: 'huang-quan', label: '군량 계산' },
|
|
{ x: 46, y: 78, role: 'support', unitId: 'fa-zheng', label: '성벽 계책' }
|
|
],
|
|
note: '진창 공성전은 성문을 치는 장수만 고르면 전열이 길어집니다. 강유와 왕평으로 길을 잡고, 위연의 돌파와 마대의 추격 차단, 황권·법정의 보급 판단을 7명 안에 담으십시오.'
|
|
},
|
|
'sixtieth-battle-wudu-yinping': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '무도와 음평 두 길을 동시에 조율하는 총지휘관입니다. 두 고을을 얻어도 보급이 끊기면 북벌은 좁아집니다.' },
|
|
{ unitId: 'jiang-wei', reason: '천수와 무도 사이 낮은 길을 잇는 길잡이입니다. 곽회의 구원로를 먼저 읽어야 합니다.' },
|
|
{ unitId: 'wei-yan', reason: '무도 고개를 빠르게 밀어붙일 돌파 장수입니다. 다만 깊이 들어간 뒤 돌아올 길을 함께 계산해야 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '구원 기병을 늦추고 앞선 부대를 회수하는 안정된 기동 담당입니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 우회 감각으로 음평 물목과 곽회 기병의 측면을 견제합니다.' },
|
|
{ unitId: 'wang-ping', reason: '산길 진영과 정지선을 표시해 두 갈래 전선이 흩어지지 않게 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '무도와 음평으로 갈라지는 군량 장부를 맞춰 장기전 붕괴를 막습니다.' },
|
|
{ unitId: 'ma-liang', reason: '새로 확보할 고을의 백성에게 승리보다 약속을 먼저 설명할 문서 담당입니다.' }
|
|
],
|
|
recommendedClasses: [
|
|
{ label: '두 길 총지휘', classKeys: ['strategist'], reason: '제갈량이 무도와 음평 양쪽 전선을 동시에 정리합니다.' },
|
|
{ label: '무도 고개 돌파', classKeys: ['infantry', 'spearman'], reason: '강유와 위연이 고개 수비를 밀되 길이 너무 길어지지 않게 버팁니다.' },
|
|
{ label: '음평 물목 견제', classKeys: ['cavalry'], reason: '마대와 조운이 물목과 수레 길을 오가는 구원 기병을 늦춥니다.' },
|
|
{ label: '산길 진영 유지', classKeys: ['infantry', 'archer'], reason: '왕평이 두 갈래 진영의 표식을 유지해 회수선을 잃지 않게 합니다.' },
|
|
{ label: '군량·민심 안정', classKeys: ['strategist'], reason: '황권과 마량이 새 고을을 보급선으로 바꾸는 뒤쪽 역할을 맡습니다.' }
|
|
],
|
|
deploymentSlots: [
|
|
{ x: 17, y: 87, role: 'support', unitId: 'zhuge-liang', label: '두 길 지휘' },
|
|
{ x: 33, y: 84, role: 'front', unitId: 'jiang-wei', label: '낮은 길' },
|
|
{ x: 43, y: 88, role: 'front', unitId: 'wei-yan', label: '무도 돌파' },
|
|
{ x: 31, y: 88, role: 'front', unitId: 'zhao-yun', label: '회수선' },
|
|
{ x: 56, y: 92, role: 'flank', unitId: 'ma-dai', label: '음평 물목' },
|
|
{ x: 40, y: 81, role: 'front', unitId: 'wang-ping', label: '산길 표식' },
|
|
{ x: 12, y: 86, role: 'support', unitId: 'huang-quan', label: '군량 장부' }
|
|
],
|
|
note: '무도·음평 확보전은 추천 무장이 출전 제한보다 많습니다. 강유·위연의 전진, 조운·마대의 회수, 왕평·황권·마량의 안정 중 무엇을 군영에 남길지 정해야 합니다.'
|
|
},
|
|
'sixty-first-battle-hanzhong-rain-defense': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '한중 북문과 무도·음평 보급선을 동시에 조율해야 하는 총지휘관입니다. 비가 길을 끊기 전에 전선을 나누어야 합니다.' },
|
|
{ unitId: 'jiang-wei', reason: '새 고을과 한중 사이의 낮은 길을 읽어 사마의 별동대가 봉화를 잇기 전에 움직일 수 있습니다.' },
|
|
{ unitId: 'wang-ping', reason: '빗길 진영 표식과 물길 기준을 세워 병사들이 후퇴선과 보급선을 혼동하지 않게 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '무도와 음평으로 갈라지는 군량 장부를 한중 창고와 맞춰 방어전의 장기 붕괴를 막습니다.' },
|
|
{ unitId: 'li-yan', reason: '수레와 운송 책임을 맡아 새 고을을 얻은 뒤 생긴 실제 부담을 전장에서 감당합니다.' },
|
|
{ unitId: 'ma-liang', reason: '새 고을의 고시문과 백성 안심 약속을 정리해 방어전이 점령군처럼 보이지 않게 합니다.' },
|
|
{ unitId: 'wei-yan', reason: '조진 본대가 깊이 들어오는 순간 짧게 반격할 돌파 장수입니다. 다만 비 오는 길에서는 제동 기준이 필요합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '넓어진 전선에서 예비대를 회수하고 빈 고개를 메우는 안정적 기동 담당입니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 우회 감각으로 음평 물목과 조진 기병의 측면 압박을 견제합니다.' }
|
|
],
|
|
note: '한중 우로 방어전은 추천 무장이 9명으로 출전 제한보다 둘 많습니다. 새 고을 보급을 지킬 장부 담당, 빗길 표식, 반격 돌파, 기병 회수 중 무엇을 뒤에 남길지 정해야 합니다.'
|
|
},
|
|
'sixty-second-battle-qishan-renewed-offensive': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '노성 앞 지연선을 상대하며 전진과 회수, 보급과 민심을 동시에 나누어야 하는 총지휘관입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '지켜 낸 무도·음평 길을 다시 공격로로 바꾸는 길잡이입니다. 사마의의 지연선을 먼저 읽어야 합니다.' },
|
|
{ unitId: 'wei-yan', reason: '노성 앞 지연선을 짧게 흔들 돌파 장수입니다. 다만 장합의 추격과 돌아올 길을 함께 계산해야 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '깊이 들어간 부대를 회수하고 예비대를 묶어 장합 기병의 추격을 늦춥니다.' },
|
|
{ unitId: 'wang-ping', reason: '기산 산길의 표식과 회수선을 세워 공격로가 퇴로를 잃지 않게 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '한중 우로 장부를 공세 가능량으로 환산해 보급 붕괴를 막습니다.' },
|
|
{ unitId: 'ma-liang', reason: '노성 앞 마을과 새 보급로의 고시문을 정리해 북벌 명분을 실제 약속으로 보이게 합니다.' },
|
|
{ unitId: 'li-yan', reason: '수레와 운송 지연을 후방에서 관리합니다. 전장에 데려가면 보급 위기는 줄지만 기동 선택이 줄어듭니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 측면 감각으로 곽회의 차단로와 동쪽 물목을 견제합니다.' }
|
|
],
|
|
note: '기산 재출정전은 추천 무장이 9명입니다. 공격로를 열 장수, 돌아올 표식을 세울 장수, 후방 장부를 맡길 장수 중 둘을 반드시 군영에 남겨야 합니다.'
|
|
},
|
|
'sixty-third-battle-lucheng-pursuit': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '노성 추격로와 회수선, 보급 장부를 동시에 조율해야 하는 총지휘관입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '낮은 길을 읽어 추격로가 함정이 되기 전에 먼저 방향을 잡습니다.' },
|
|
{ unitId: 'zhao-yun', reason: '장합 기병이 뒤를 물 때 예비대를 회수선으로 묶는 안정적 기동 담당입니다.' },
|
|
{ unitId: 'wei-yan', reason: '사마의가 물러난 틈을 놓치지 않고 노성 앞 방어선을 흔들 돌파 장수입니다.' },
|
|
{ unitId: 'wang-ping', reason: '회수 표식과 정지선을 세워 추격전이 퇴로 없는 돌파가 되지 않게 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '추격로가 길어질수록 수레와 군량 장부를 전장 기준으로 맞춰야 합니다.' },
|
|
{ unitId: 'li-yan', reason: '운송 지연과 예비 수레 배정을 현장에서 감당할 수 있습니다. 데려가면 보급 안정성이 커집니다.' },
|
|
{ unitId: 'ma-liang', reason: '노성 주변 마을의 안심 약속과 고시문을 추격 전에 준비합니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 측면 감각으로 곽회의 봉화 차단로와 우회 기병을 견제합니다.' }
|
|
],
|
|
note: '노성 추격전은 추천 무장이 9명입니다. 빠른 추격, 안전한 회수, 수레 운송, 마을 안심 중 둘을 군영에 남기는 선택이 필요합니다.'
|
|
},
|
|
'sixty-fourth-battle-weishui-camps': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '위수 진영과 물목, 봉화선을 동시에 조율해야 하는 총지휘관입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '강가 낮은 길과 물목을 읽어 장합의 추격로를 먼저 예측합니다.' },
|
|
{ unitId: 'wang-ping', reason: '진영 정지선과 회수 표식을 세워 위수 남안 확보가 퇴로 없는 돌파가 되지 않게 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '강가 장기전에서 수레와 군량 장부를 전장 기준으로 맞춰야 합니다.' },
|
|
{ unitId: 'li-yan', reason: '운송 지연과 예비 수레 배정을 현장에서 감당해 강안 보급 붕괴를 막습니다.' },
|
|
{ unitId: 'wei-yan', reason: '위군 진영 사이를 짧게 흔들 돌파 장수입니다. 다만 장합의 물목 추격을 함께 계산해야 합니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 측면 감각으로 곽회의 봉화로와 우회 기병을 견제합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '넓어진 강안 전선에서 예비대를 회수선으로 묶는 안정적 기동 담당입니다.' },
|
|
{ unitId: 'ma-liang', reason: '강가 마을의 고시문과 안심 약속을 정리해 북벌 명분을 실제 약속으로 이어 줍니다.' }
|
|
],
|
|
note: '위수 진영전은 추천 무장이 9명입니다. 물목 판단, 진영 확보, 수레 운송, 봉화 차단, 마을 안심 중 둘을 군영에 남겨야 합니다.'
|
|
},
|
|
'sixty-fifth-battle-weishui-northbank': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '북안 지연선과 전진 진영, 회수선의 폭을 동시에 조율해야 하는 총지휘관입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '강북 낮은 길과 장합의 재추격로를 읽어 먼저 끊어야 합니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 측면 감각으로 곽회의 봉화로와 장합의 우회 기병을 견제합니다.' },
|
|
{ unitId: 'wang-ping', reason: '북안 전진 진영마다 정지선과 회수 표식을 세워 돌파가 고립으로 바뀌지 않게 합니다.' },
|
|
{ unitId: 'huang-quan', reason: '강북 진영을 얻은 뒤 버틸 날짜와 물릴 날짜를 장부로 계산합니다.' },
|
|
{ unitId: 'li-yan', reason: '강북 수레선과 예비 운송 기준을 현장에서 맞춰 보급 붕괴를 막습니다.' },
|
|
{ unitId: 'wei-yan', reason: '북안 진영 사이를 짧게 흔들 돌파 장수입니다. 조운의 회수선과 함께 계산해야 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '넓어진 강북 전선에서 예비대를 회수선으로 묶는 안정적 기동 담당입니다.' },
|
|
{ unitId: 'ma-liang', reason: '강북 마을의 고시문과 안심 약속을 정리해 북벌 명분을 실제 약속으로 이어 줍니다.' }
|
|
],
|
|
note: '위수 북안 압박전은 추천 무장이 9명입니다. 낮은 길 판단, 측면 견제, 장부와 수레, 돌파와 회수 중 둘을 군영에 남겨야 합니다.'
|
|
},
|
|
'sixty-sixth-battle-wuzhang-final': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '오장원 본영과 귀환 장부를 동시에 지휘해야 하는 최종전의 중심입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '제갈량의 작전도를 이어 받아 다음 길을 세우는 계승 축입니다.' },
|
|
{ unitId: 'wang-ping', reason: '퇴로 질서와 후위 방어를 맡겨 장합의 마지막 추격을 흔들 수 있습니다.' },
|
|
{ unitId: 'ma-dai', reason: '측면 기병을 끊고 곽회의 봉화로 이어지는 우회로를 견제합니다.' },
|
|
{ unitId: 'huang-quan', reason: '귀환 장부와 군량 계산을 안정시켜 긴 전투 후에도 부대가 흩어지지 않게 합니다.' },
|
|
{ unitId: 'li-yan', reason: '보급선과 예비 수레를 붙들어 오장원 본영의 지속력을 높입니다.' },
|
|
{ unitId: 'wei-yan', reason: '사마의 본대의 지연선을 흔드는 돌파 역할에 강합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '흩어진 전선을 다시 묶는 안정적인 기동 예비대입니다.' },
|
|
{ unitId: 'ma-liang', reason: '마을 안심과 군문 고시문을 정리해 귀환로의 혼란을 줄입니다.' }
|
|
],
|
|
note: '오장원 최종전은 추천 무장 9명 중 7명을 고르는 마지막 편성입니다. 본영 방어, 퇴로 유지, 장부 계승을 함께 고려하십시오.'
|
|
}
|
|
};
|
|
|
|
const sortieRulesByCampaignStep: Partial<Record<CampaignStep, SortieRuleDefinition>> = {
|
|
'northern-campaign-prep-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '북벌 준비 회의의 중심입니다. 한중 창고와 전군 편성을 직접 조율해야 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '기산 출진 전 조운의 기동대 운용과 우회로 정찰이 필요합니다.' },
|
|
{ unitId: 'huang-quan', reason: '한중과 익주의 군량 장부를 맞춰 장기전 보급선을 안정시킵니다.' },
|
|
{ unitId: 'ma-liang', reason: '문서와 민심을 정리해 북벌 명분과 후방 행정의 빈틈을 줄입니다.' },
|
|
{ unitId: 'wang-ping', reason: '산길과 진영 배치를 읽어 다음 북방 전장의 지형 선택을 돕습니다.' },
|
|
{ unitId: 'ma-dai', reason: '기병 정찰과 추격 차단을 맡아 출진로의 측면 위험을 줄입니다.' },
|
|
{ unitId: 'wei-yan', reason: '거친 돌파력을 어디까지 전방에 둘지 미리 판단해야 할 장수입니다.' }
|
|
],
|
|
note: '북벌 준비 회의는 전투가 아니지만 다음 장기전의 편성을 정하는 단계입니다. 제갈량을 중심으로 보급, 산길, 기동, 돌파 역할을 균형 있게 고르십시오.'
|
|
},
|
|
'fifty-eighth-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '기산 후퇴 이후 북벌 재정비의 중심입니다. 다음 출정을 무리 없이 준비해야 합니다.' },
|
|
{ unitId: 'jiang-wei', reason: '첫 출전 평가가 필요한 새 장수입니다. 천수의 낮은 길을 다음 북벌 계획에 녹여야 합니다.' },
|
|
{ unitId: 'wang-ping', reason: '가정과 기산에서 배운 물길 원칙을 다음 진영 배치 기준으로 정리합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '오래 지켜 온 회수 기동으로 후퇴전의 교훈을 병사들에게 훈련시킵니다.' },
|
|
{ unitId: 'ma-dai', reason: '서량 기병의 우회와 귀환선을 강유의 낮은 길과 비교해 다음 기병 운용을 다듬습니다.' },
|
|
{ unitId: 'huang-quan', reason: '장부와 보급을 재점검해 다음 북벌의 무리한 진군을 막습니다.' },
|
|
{ unitId: 'ma-liang', reason: '강유의 첫 출전과 후퇴 명분을 군영의 말과 기록으로 정리합니다.' }
|
|
],
|
|
note: '기산 후퇴 이후는 엔딩이 아니라 북벌 재정비 단계입니다. 제갈량과 강유를 중심으로 다음 출정에 남길 교훈을 정리하십시오.'
|
|
},
|
|
'fifty-ninth-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '진창 공성 이후 북방 계산을 다시 세워야 하는 중심입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '진창 길과 천수 길을 비교해 다음 북벌에서 자신이 맡을 전선을 정리해야 합니다.' },
|
|
{ unitId: 'wei-yan', reason: '성채 앞 돌파 판단을 평가해야 합니다. 다음 전선에서 무리한 전진을 어떻게 쓸지 결정합니다.' },
|
|
{ unitId: 'wang-ping', reason: '공성 진영 표식과 후퇴 기준을 다음 북벌 군령으로 정리합니다.' },
|
|
{ unitId: 'ma-dai', reason: '왕쌍 추격 차단의 기병 운용을 다음 출정의 우회 기준으로 남깁니다.' },
|
|
{ unitId: 'huang-quan', reason: '오래 끈 공성전의 군량 손익을 장부로 다시 계산합니다.' },
|
|
{ unitId: 'fa-zheng', reason: '학소의 수비와 위군 대응을 계책 관점에서 정리합니다.' }
|
|
],
|
|
note: '진창 공성전 이후도 엔딩이 아니라 북벌 보급 재계산 단계입니다. 강유와 위연의 역할, 왕평과 황권의 제동 장치를 함께 보십시오.'
|
|
},
|
|
'sixtieth-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '두 고을 확보 뒤 다음 북벌 계산을 다시 세워야 하는 중심입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '무도와 음평의 새 길을 다음 전선의 기준으로 정리해야 합니다.' },
|
|
{ unitId: 'wei-yan', reason: '돌파가 실제 성과를 냈을 때 어디까지 허용할지 평가해야 합니다.' },
|
|
{ unitId: 'wang-ping', reason: '산길 진영 표식과 정지선을 다음 출정 군령으로 정리합니다.' },
|
|
{ unitId: 'huang-quan', reason: '두 고을을 얻은 뒤 늘어난 보급 책임을 장부로 다시 계산합니다.' },
|
|
{ unitId: 'ma-liang', reason: '새 고을의 민심과 고시문을 정리해 점령이 약탈처럼 보이지 않게 합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '깊어진 전선에서 회수 기동 기준을 다시 훈련시킵니다.' },
|
|
{ unitId: 'ma-dai', reason: '음평 물목과 서량 기병 운용을 다음 우회 전선 기준으로 남깁니다.' }
|
|
],
|
|
note: '무도·음평 확보 이후도 엔딩이 아니라 더 넓어진 북벌 책임입니다. 새 고을 안정과 다음 전진 기준을 함께 점검하십시오.'
|
|
},
|
|
'sixty-first-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '한중 우로 방어 이후 다음 북벌의 공격로와 후방 방어선을 다시 나누어야 합니다.' },
|
|
{ unitId: 'jiang-wei', reason: '지켜 낸 무도·음평 길을 다음 공격로로 바꿀 수 있는지 판단해야 합니다.' },
|
|
{ unitId: 'wang-ping', reason: '빗길 표식과 진영 기준을 다음 출정의 후퇴선으로 정리합니다.' },
|
|
{ unitId: 'huang-quan', reason: '한중 창고와 새 고을 군량 흐름을 다음 출정 가능량으로 환산합니다.' },
|
|
{ unitId: 'li-yan', reason: '운송 책임과 지체 위험을 군영 단계에서 먼저 계산해야 합니다.' },
|
|
{ unitId: 'ma-liang', reason: '새 고을의 민심 문서를 다음 북벌 명분과 약속으로 이어 줍니다.' },
|
|
{ unitId: 'wei-yan', reason: '반격 돌파가 가능했던 지점과 더 기다려야 했던 지점을 구분합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '예비대 회수 기준을 다음 넓은 전선의 안전장치로 남깁니다.' },
|
|
{ unitId: 'ma-dai', reason: '음평 물목의 기병 우회로를 다음 측면 대응 기준으로 정리합니다.' }
|
|
],
|
|
note: '한중 우로 방어 이후도 엔딩이 아니라 방어 책임이 넓어진 다음 북벌 준비입니다. 지킨 길을 공격로로 바꾸려면 누가 전방에 나가고 누가 보급을 붙들지 다시 골라야 합니다.'
|
|
},
|
|
'sixty-second-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '기산 재출정 이후 더 깊은 북방 계산을 다시 세워야 하는 중심입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '노성 앞에서 확인한 위군 지연선을 다음 공격로 지도로 정리합니다.' },
|
|
{ unitId: 'wei-yan', reason: '돌파가 성과를 냈을 때 어디까지 허용할지 다음 군령으로 남깁니다.' },
|
|
{ unitId: 'zhao-yun', reason: '장합 추격을 견딘 회수 기동을 예비대 훈련 기준으로 정리합니다.' },
|
|
{ unitId: 'wang-ping', reason: '기산 회수 표식과 진영 정지선을 다음 전선의 안전장치로 남깁니다.' },
|
|
{ unitId: 'huang-quan', reason: '노성 앞 공세가 실제로 소모한 군량과 남은 장부를 계산합니다.' },
|
|
{ unitId: 'ma-liang', reason: '전방 마을 안심 약속과 고시문을 다음 북벌 명분으로 이어 줍니다.' },
|
|
{ unitId: 'li-yan', reason: '운송 지연과 예비 수레 배정을 후방 기준으로 다시 정리합니다.' },
|
|
{ unitId: 'ma-dai', reason: '곽회 차단로 대응과 서량 기병 우회 기준을 다음 측면 운용에 반영합니다.' }
|
|
],
|
|
note: '기산 재출정 이후도 엔딩이 아니라 더 깊어진 북방 전선 준비입니다. 이긴 길을 지킬 사람과 더 깊이 들어갈 사람을 나누어 보십시오.'
|
|
},
|
|
'sixty-third-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '노성 추격 이후 얻은 길과 잃지 말아야 할 나라의 숨을 다시 계산해야 합니다.' },
|
|
{ unitId: 'jiang-wei', reason: '노성 낮은 길과 봉화 차단 경험을 다음 북벌 공격로로 정리합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '회수선 훈련을 예비대 기준으로 남겨 넓어진 전선을 안전하게 묶습니다.' },
|
|
{ unitId: 'wei-yan', reason: '돌파가 성과를 낸 지점과 멈춰야 했던 지점을 다음 군령으로 남깁니다.' },
|
|
{ unitId: 'wang-ping', reason: '노성 표식과 정지선을 다음 진영 배치의 안전장치로 정리합니다.' },
|
|
{ unitId: 'huang-quan', reason: '추격전에서 소모된 군량과 남은 수레를 다음 출정 가능량으로 환산합니다.' },
|
|
{ unitId: 'li-yan', reason: '운송 지연과 예비 수레 배정을 후방 기준으로 다시 세웁니다.' },
|
|
{ unitId: 'ma-liang', reason: '노성 주변 마을의 고시문을 다음 북벌 명분과 실제 약속으로 이어 줍니다.' },
|
|
{ unitId: 'ma-dai', reason: '곽회 봉화로와 서량 기병 우회 기준을 다음 측면 운용에 반영합니다.' }
|
|
],
|
|
note: '노성 추격 이후도 엔딩이 아니라 더 깊은 북방 전선의 재정비입니다. 이긴 길을 지킬 사람과 더 나아갈 사람을 다시 나누어 보십시오.'
|
|
},
|
|
'sixty-fourth-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '위수 진영 이후 강가 장기전의 다음 계산을 다시 세워야 하는 중심입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '위수 낮은 길과 물목 대응 경험을 다음 북방 공격로로 정리합니다.' },
|
|
{ unitId: 'wang-ping', reason: '위수 진영 표식과 정지선을 다음 진영 배치의 안전장치로 남깁니다.' },
|
|
{ unitId: 'huang-quan', reason: '강가 장기전에서 소모된 군량과 남은 수레를 다음 출정 가능량으로 환산합니다.' },
|
|
{ unitId: 'li-yan', reason: '강안 운송 지연과 예비 수레 배정을 후방 기준으로 다시 세웁니다.' },
|
|
{ unitId: 'wei-yan', reason: '진영 사이 돌파가 성과를 낸 지점과 멈춰야 했던 지점을 다음 군령으로 남깁니다.' },
|
|
{ unitId: 'ma-dai', reason: '곽회 봉화로와 장합 물목 기병 대응을 다음 측면 운용에 반영합니다.' },
|
|
{ unitId: 'zhao-yun', reason: '예비대 회수 기준을 더 넓어진 강안 전선의 안전장치로 남깁니다.' },
|
|
{ unitId: 'ma-liang', reason: '강가 마을의 고시문을 다음 북벌 명분과 실제 약속으로 이어 줍니다.' }
|
|
],
|
|
note: '위수 진영 이후도 엔딩이 아니라 더 깊은 북방 전선의 재정비입니다. 강을 끼고 오래 싸울 사람과 더 밀고 들어갈 사람을 다시 나누어 보십시오.'
|
|
},
|
|
'sixty-fifth-camp': {
|
|
maxUnits: 7,
|
|
requiredUnitIds: ['zhuge-liang'],
|
|
excludedUnitIds: ['liu-bei'],
|
|
recommended: [
|
|
{ unitId: 'zhuge-liang', reason: '북안 지연선 이후 다음 전선의 폭을 다시 줄이고 늘릴 중심입니다.' },
|
|
{ unitId: 'jiang-wei', reason: '강북 낮은 길과 장합 재추격 대응 경험을 다음 북방 공격로로 정리합니다.' },
|
|
{ unitId: 'ma-dai', reason: '곽회 봉화로와 서량 기병 우회 기준을 다음 측면 운용에 반영합니다.' },
|
|
{ unitId: 'wang-ping', reason: '북안 전진 진영의 정지선과 회수 표식을 다음 배치의 안전장치로 남깁니다.' },
|
|
{ unitId: 'huang-quan', reason: '강북에서 소모된 군량과 남은 수레를 다음 출정 가능량으로 환산합니다.' },
|
|
{ unitId: 'li-yan', reason: '강북 운송 지연과 예비 수레 배정을 후방 기준으로 다시 세웁니다.' },
|
|
{ unitId: 'wei-yan', reason: '북안 진영 사이 돌파가 성과를 낸 지점과 멈춰야 했던 지점을 다음 군령으로 남깁니다.' },
|
|
{ unitId: 'zhao-yun', reason: '예비대 회수 기준을 더 넓어진 강북 전선의 안전장치로 남깁니다.' },
|
|
{ unitId: 'ma-liang', reason: '강북 마을의 고시문을 다음 북벌 명분과 실제 약속으로 이어 줍니다.' }
|
|
],
|
|
note: '위수 북안 이후도 엔딩이 아니라 더 깊은 북방 전선의 재정비입니다. 지킬 진영과 물릴 진영, 다시 밀고 들어갈 길을 나누어 보십시오.'
|
|
}
|
|
};
|
|
|
|
const campDialogues: CampDialogue[] = [
|
|
{
|
|
id: 'liu-guan-after-first-battle',
|
|
title: '의리를 다지는 밤',
|
|
availableAfterBattleIds: [campBattleIds.first],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 14,
|
|
lines: [
|
|
'유비: 운장, 오늘 그대가 앞을 막아 주지 않았다면 백성들을 지키지 못했을 것이오.',
|
|
'관우: 형님의 뜻이 분명했기에 칼을 들 수 있었습니다. 다음 싸움에서도 길을 열겠습니다.',
|
|
'유비: 우리의 맹세가 전장에서 더 단단해지는구려.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-vanguard',
|
|
label: '앞길을 맡긴다',
|
|
response: '관우는 말없이 고개를 숙였다. 다음 전장에서도 형님의 길을 열겠다는 결의가 깊어졌다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'protect-people',
|
|
label: '백성 보호를 당부한다',
|
|
response: '관우는 칼끝보다 백성의 숨결을 먼저 살피겠다고 답했다. 두 사람의 뜻이 한층 가까워졌다.',
|
|
rewardExp: 4
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-first-battle',
|
|
title: '거친 용기를 붙잡다',
|
|
availableAfterBattleIds: [campBattleIds.first],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 14,
|
|
lines: [
|
|
'장비: 형님, 다음엔 제가 먼저 뛰쳐나가 두령 놈을 단번에 꺾겠습니다!',
|
|
'유비: 익덕의 용맹은 든든하나, 백성이 뒤에 있음을 잊지 말아야 하오.',
|
|
'장비: 하하! 형님 말이라면 참아 보겠습니다. 대신 싸울 때는 크게 치겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'temper-courage',
|
|
label: '용맹을 다독인다',
|
|
response: '장비는 답답하다는 듯 웃었지만, 형님의 말에 맞춰 창을 거두는 법도 익히겠다고 했다.',
|
|
rewardExp: 5
|
|
},
|
|
{
|
|
id: 'lead-charge',
|
|
label: '선봉을 약속한다',
|
|
response: '장비의 눈빛이 밝아졌다. 믿고 맡긴다는 말은 그에게 무엇보다 큰 격려였다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-first-battle',
|
|
title: '칼과 창의 약속',
|
|
availableAfterBattleIds: [campBattleIds.first],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 12,
|
|
lines: [
|
|
'관우: 익덕, 적이 흩어질 때 너무 깊이 들어가면 형님이 걱정하신다.',
|
|
'장비: 운장 형님이 옆을 받쳐 준다면 걱정할 일이 없지 않습니까?',
|
|
'관우: 좋다. 다음 전장에서는 서로의 빈틈을 먼저 살피자.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'cover-flanks',
|
|
label: '서로의 측면을 맡긴다',
|
|
response: '칼과 창이 같은 호흡으로 움직일 길을 찾았다. 두 맹장의 공명이 단단해졌다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'compete-boldly',
|
|
label: '누가 먼저 베는지 겨룬다',
|
|
response: '장비가 크게 웃고 관우도 미소를 감추지 못했다. 경쟁심마저 전장의 힘이 되었다.',
|
|
rewardExp: 4
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-pursuit',
|
|
title: '추격 뒤의 절제',
|
|
availableAfterBattleIds: [campBattleIds.second],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 15,
|
|
lines: [
|
|
'유비: 잔당을 쫓는 싸움일수록 칼끝이 흐려지기 쉽소. 오늘 운장의 판단이 군을 지켰소.',
|
|
'관우: 형님이 백성의 길을 먼저 살피라 하셨기에 무리한 추격을 멈출 수 있었습니다.',
|
|
'유비: 이기되 어지럽히지 않는 것, 그것이 우리가 지켜야 할 싸움이오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'steady-line',
|
|
label: '전열의 절제를 칭찬한다',
|
|
response: '관우는 전공보다 전열의 안정이 먼저라는 말을 마음에 새겼다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'entrust-village',
|
|
label: '마을 수비를 맡긴다',
|
|
response: '관우는 다음 싸움에서도 백성이 있는 곳을 먼저 지키겠다고 답했다.',
|
|
rewardExp: 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-pursuit',
|
|
title: '성급함과 선봉',
|
|
availableAfterBattleIds: [campBattleIds.second],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 15,
|
|
lines: [
|
|
'장비: 형님, 추격전에서는 한 발만 더 빨랐다면 놈들을 몽땅 묶어 둘 수 있었습니다.',
|
|
'유비: 익덕의 빠른 창은 큰 힘이오. 다만 혼자 먼저 닿으면 뒤따르는 병사들이 흔들리오.',
|
|
'장비: 좋소. 다음엔 앞서 달리되, 형님 깃발이 보이는 거리 안에서 치겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'controlled-charge',
|
|
label: '통제된 돌격을 약속받는다',
|
|
response: '장비는 선봉의 힘과 군율이 함께 가야 한다는 말을 받아들였다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'praise-speed',
|
|
label: '빠른 결단을 칭찬한다',
|
|
response: '믿음을 받은 장비는 다음 전장에서 더 날카로운 선봉이 되겠다고 웃었다.',
|
|
rewardExp: 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-pursuit',
|
|
title: '길목을 나누다',
|
|
availableAfterBattleIds: [campBattleIds.second],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 13,
|
|
lines: [
|
|
'관우: 익덕, 네가 길목을 막아 준 덕에 궁병들이 숨을 돌렸다.',
|
|
'장비: 운장 형님이 옆을 눌러 주니 제가 마음 놓고 앞을 칠 수 있었지요.',
|
|
'관우: 다음에도 누가 먼저 나서든, 남은 한 사람이 뒤를 끊어 주면 된다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'split-roads',
|
|
label: '길목 분담을 정한다',
|
|
response: '두 사람은 좁은 길에서 서로의 창끝과 칼끝이 부딪히지 않을 간격을 익혔다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'bold-competition',
|
|
label: '전공을 겨루게 둔다',
|
|
response: '가벼운 경쟁심이 두 맹장의 호흡을 더 뜨겁게 만들었다.',
|
|
rewardExp: 4
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-guangzong-road',
|
|
title: '광종 길목의 침묵',
|
|
availableAfterBattleIds: [campBattleIds.third],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 16,
|
|
lines: [
|
|
'유비: 광종으로 가는 길은 좁고, 적은 지형을 믿고 버티고 있소.',
|
|
'관우: 험한 길에서는 말보다 마음이 먼저 흔들립니다. 제가 앞에서 병사들의 숨을 고르겠습니다.',
|
|
'유비: 운장이 앞을 잡아 준다면, 나도 뒤에서 군의 뜻을 잃지 않게 하겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'lead-narrow-road',
|
|
label: '좁은 길의 선봉을 맡긴다',
|
|
response: '관우는 험지를 뚫는 임무를 조용히 받아들였다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'keep-discipline',
|
|
label: '군율 유지를 당부한다',
|
|
response: '관우는 승리보다 흐트러지지 않는 행군이 더 중요하다고 답했다.',
|
|
rewardExp: 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-guangzong-road',
|
|
title: '강가의 큰소리',
|
|
availableAfterBattleIds: [campBattleIds.third],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 16,
|
|
lines: [
|
|
'장비: 강가 길목이 답답해서 창을 휘두를 맛이 덜했습니다.',
|
|
'유비: 답답한 싸움에서도 익덕이 버텨 주니 병사들이 물러서지 않았소.',
|
|
'장비: 다음엔 답답한 길도 제가 먼저 웃으며 열어 보이겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'laugh-through',
|
|
label: '호방함으로 사기를 올린다',
|
|
response: '장비의 큰 웃음이 군영의 무거운 공기를 풀었다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'guard-bridgehead',
|
|
label: '교두보 수비를 맡긴다',
|
|
response: '장비는 한 발도 물러서지 않는 수비 역시 선봉의 일이라며 고개를 끄덕였다.',
|
|
rewardExp: 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-guangzong-road',
|
|
title: '험지의 호흡',
|
|
availableAfterBattleIds: [campBattleIds.third],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 14,
|
|
lines: [
|
|
'관우: 숲과 언덕에서는 한 걸음 늦어도 전열 전체가 늦어진다.',
|
|
'장비: 그럴 땐 형님이 길을 재고, 제가 길을 넓히면 되지 않겠습니까?',
|
|
'관우: 좋은 말이다. 칼이 재고 창이 연다면 험지도 길이 된다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'measure-and-break',
|
|
label: '관우가 재고 장비가 뚫는다',
|
|
response: '두 사람은 험지 돌파의 역할을 명확히 나누었다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'watch-each-other',
|
|
label: '서로의 후방을 살핀다',
|
|
response: '성급한 돌파보다 빈틈을 지우는 호흡이 깊어졌다.',
|
|
rewardExp: 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-zhang-jue',
|
|
title: '황건의 깃발이 꺾인 밤',
|
|
availableAfterBattleIds: [campBattleIds.fourth],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'유비: 장각이 쓰러졌으나 천하가 바로 맑아진 것은 아니오.',
|
|
'관우: 큰 난이 끝난 뒤에도 작은 탐욕은 남습니다. 형님의 뜻이 흔들리지 않게 제가 곁에 있겠습니다.',
|
|
'유비: 운장과 익덕이 있으니, 더 큰 혼란에도 나아갈 용기가 생기오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-larger-chaos',
|
|
label: '더 큰 혼란을 준비한다',
|
|
response: '관우는 반동탁의 소문을 들으며 다시 칼을 정비했다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'honor-fallen-people',
|
|
label: '희생된 백성을 기린다',
|
|
response: '두 사람은 전공보다 백성의 상처를 먼저 기억하기로 했다.',
|
|
rewardExp: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-zhang-jue',
|
|
title: '끝난 싸움, 남은 분노',
|
|
availableAfterBattleIds: [campBattleIds.fourth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'장비: 장각을 꺾었는데도 속이 시원하지만은 않습니다. 아직 백성을 괴롭히는 놈들이 많습니다.',
|
|
'유비: 그 분노가 백성을 향하지 않고 악한 자에게만 향하도록 붙잡아야 하오.',
|
|
'장비: 형님이 길을 가리키면, 제 창은 그 길 밖으로 나가지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'temper-anger',
|
|
label: '분노를 다스리게 한다',
|
|
response: '장비는 분노를 창끝에만 싣고 마음에는 남기지 않겠다고 했다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'promise-next-vanguard',
|
|
label: '다음 선봉을 약속한다',
|
|
response: '장비는 반동탁의 길에서도 가장 먼저 적진을 흔들겠다고 웃었다.',
|
|
rewardExp: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-zhang-jue',
|
|
title: '맹장들의 전공',
|
|
availableAfterBattleIds: [campBattleIds.fourth],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 16,
|
|
lines: [
|
|
'장비: 운장 형님, 오늘은 제가 더 크게 적진을 흔든 것 같습니다.',
|
|
'관우: 전공을 다투는 마음이 백성을 구하는 데 쓰인다면 나쁠 것이 없다.',
|
|
'장비: 그럼 다음 전장에서도 누가 더 크게 이름을 떨치는지 봅시다!'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'clean-rivalry',
|
|
label: '깨끗한 경쟁을 인정한다',
|
|
response: '두 맹장은 웃으며 전공 경쟁을 다음 전장의 활력으로 삼았다.',
|
|
rewardExp: 6
|
|
},
|
|
{
|
|
id: 'brothers-before-glory',
|
|
label: '명성보다 형제를 앞세운다',
|
|
response: '관우와 장비는 전공보다 서로의 생존을 먼저 지키기로 했다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-sishui',
|
|
title: '공손찬의 깃발 아래',
|
|
availableAfterBattleIds: [campBattleIds.fifth],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'유비: 공손찬의 진영은 크고, 제후들의 생각은 저마다 다르오. 우리 뜻을 잃지 않는 것이 더 어려워졌소.',
|
|
'관우: 큰 깃발 아래 설수록 작은 의리를 더 단단히 붙잡아야 합니다.',
|
|
'유비: 운장의 말이 옳소. 남의 군영에 있어도 우리가 지킬 길은 변하지 않소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'hold-oath-in-coalition',
|
|
label: '연합 속에서도 맹세를 지킨다',
|
|
response: '관우는 도원에서 맺은 뜻을 낯선 군영에서도 흐리지 않겠다고 답했다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'observe-warlords',
|
|
label: '제후들의 속내를 살핀다',
|
|
response: '관우는 싸움만이 아니라 사람의 뜻을 살피는 눈도 필요하다고 받아들였다.',
|
|
rewardExp: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-sishui',
|
|
title: '낯선 군영의 술잔',
|
|
availableAfterBattleIds: [campBattleIds.fifth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'장비: 형님, 공손찬 군영엔 말도 많고 술도 많고, 괜히 시비 걸 놈들도 많습니다.',
|
|
'유비: 익덕의 기개는 좋으나, 지금은 싸워야 할 적과 참아야 할 말을 구분해야 하오.',
|
|
'장비: 하하, 형님이 그렇게 말하면 술잔은 들되 주먹은 참아 보겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'restrain-in-camp',
|
|
label: '군영 안의 절제를 당부한다',
|
|
response: '장비는 시비보다 형님의 체면을 먼저 지키겠다고 약속했다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'trust-his-honesty',
|
|
label: '솔직함을 믿어 준다',
|
|
response: '믿음을 받은 장비는 낯선 제후들 사이에서도 형님의 편에 서겠다고 웃었다.',
|
|
rewardExp: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-sishui',
|
|
title: '연합군의 두 맹장',
|
|
availableAfterBattleIds: [campBattleIds.fifth],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 16,
|
|
lines: [
|
|
'관우: 이곳에는 이름난 장수도 많고, 허세뿐인 자도 많다.',
|
|
'장비: 그럼 우리가 전장에서 보여 주면 되지요. 말보다 칼과 창이 빠릅니다.',
|
|
'관우: 다만 형님의 이름이 먼저 서야 한다. 우리의 무공은 그 뜻을 받치는 것이니.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'raise-liu-bei-name',
|
|
label: '유비의 이름을 세운다',
|
|
response: '두 사람은 자신의 전공보다 유비의 명성을 먼저 드러내기로 뜻을 모았다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'test-famous-generals',
|
|
label: '이름난 장수들을 의식한다',
|
|
response: '장비의 승부욕과 관우의 침착함이 묘한 균형을 이루었다.',
|
|
rewardExp: 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-jieqiao',
|
|
title: '서주로 향하는 뜻',
|
|
availableAfterBattleIds: [campBattleIds.sixth],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'유비: 공손찬 장군의 은혜는 크나, 서주의 백성이 위태롭다는 소식을 들으니 마음이 편치 않소.',
|
|
'관우: 의탁은 몸을 잠시 맡기는 일이고, 의리는 마음이 향하는 곳을 따르는 일입니다.',
|
|
'유비: 운장의 말이 나를 깨우는구려. 도겸 공의 청을 외면하지 않겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'ask-permission',
|
|
label: '공손찬에게 허락을 구한다',
|
|
response: '관우는 예를 갖추어 떠나는 것이 유비의 이름을 더 높인다고 답했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'put-people-first',
|
|
label: '백성을 먼저 생각한다',
|
|
response: '두 사람은 새 주인의 눈치보다 고통받는 백성을 먼저 보겠다고 뜻을 모았다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-jieqiao',
|
|
title: '떠나는 술잔',
|
|
availableAfterBattleIds: [campBattleIds.sixth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'장비: 형님, 북방 군영도 나쁘진 않았지만 서주 백성이 부른다면 가야지요.',
|
|
'유비: 익덕은 떠나는 길에서도 성급히 다투지 말아야 하오. 우리는 도움을 주러 가는 것이오.',
|
|
'장비: 알겠습니다. 술잔은 비우고, 창은 백성을 괴롭히는 자에게만 겨누겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'steady-departure',
|
|
label: '차분한 출발을 당부한다',
|
|
response: '장비는 떠나는 순간의 소란도 형님의 이름에 흠이 될 수 있음을 받아들였다.',
|
|
rewardExp: 7
|
|
},
|
|
{
|
|
id: 'promise-xuzhou-vanguard',
|
|
label: '서주 선봉을 맡긴다',
|
|
response: '장비는 서주 길에서도 가장 앞에서 백성을 지키겠다며 창을 움켜쥐었다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-jieqiao',
|
|
title: '새 길의 두 기둥',
|
|
availableAfterBattleIds: [campBattleIds.sixth],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'관우: 익덕, 서주로 가면 우리 힘만으로는 감당하기 어려운 일도 많을 것이다.',
|
|
'장비: 그러니 더더욱 형님 곁을 단단히 지켜야지요. 운장 형님이 오른쪽이면 저는 왼쪽입니다.',
|
|
'관우: 좋다. 길이 바뀌어도 형님을 받치는 두 기둥은 흔들리지 않는다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'two-pillars',
|
|
label: '좌우의 역할을 정한다',
|
|
response: '두 사람은 유비의 좌우에서 전선과 군심을 함께 지키기로 했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'guard-the-oath',
|
|
label: '도원 맹세를 되새긴다',
|
|
response: '낯선 서주 길 앞에서도 도원의 맹세가 세 사람의 중심이 되었다.',
|
|
rewardExp: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-xuzhou',
|
|
title: '서주의 무게',
|
|
availableAfterBattleIds: [campBattleIds.seventh],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'유비: 도겸 공은 서주를 맡아 달라 하나, 내 그릇이 백성을 감당할 수 있을지 두렵소.',
|
|
'관우: 형님께서 두려워하시는 까닭은 땅이 아니라 백성을 먼저 보기 때문입니다. 그래서 맡을 수 있는 일입니다.',
|
|
'유비: 운장의 말처럼 두려움이 의를 잊지 않게 하는 끈이라면, 그 끈을 놓지 않겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'accept-responsibility',
|
|
label: '책임을 피하지 않는다',
|
|
response: '관우는 유비가 명분보다 책임을 먼저 말하는 것을 보고 깊이 고개를 끄덕였다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'listen-to-xuzhou',
|
|
label: '서주 사람들의 뜻을 듣는다',
|
|
response: '두 사람은 성급히 자리를 받기보다 백성의 마음을 먼저 살피기로 했다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-xuzhou',
|
|
title: '성급함을 다스리다',
|
|
availableAfterBattleIds: [campBattleIds.seventh],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'장비: 형님, 서주가 우리를 부른다면 시원하게 맡으면 되지 않습니까?',
|
|
'유비: 익덕, 기뻐할 일만은 아니오. 성 하나를 맡는다는 것은 그 성의 굶주림과 두려움도 함께 맡는 일이오.',
|
|
'장비: 그렇다면 제가 먼저 군량과 길목을 지키겠습니다. 백성이 굶는 일부터 막아야겠지요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'guard-supplies',
|
|
label: '군량 길목을 맡긴다',
|
|
response: '장비는 싸움보다 지키는 일이 더 어렵다는 말을 마음에 새겼다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'calm-the-temper',
|
|
label: '분노보다 절제를 당부한다',
|
|
response: '유비의 당부에 장비는 주먹을 풀고, 백성을 놀라게 하지 않겠다고 답했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-xuzhou',
|
|
title: '새 군영의 좌우',
|
|
availableAfterBattleIds: [campBattleIds.seventh],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'관우: 서주는 넓고 사람의 마음은 아직 흩어져 있다. 우리가 먼저 흔들리면 형님의 짐이 더 무거워질 것이다.',
|
|
'장비: 걱정 마시오. 운장 형님이 군율을 세우면, 나는 군심을 붙잡겠습니다.',
|
|
'관우: 좋다. 이제 우리는 전장에서뿐 아니라 군영에서도 형님의 좌우가 되어야 한다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'divide-camp-duties',
|
|
label: '군율과 군심을 나눈다',
|
|
response: '관우와 장비는 서로의 강점을 인정하며 새 군영을 안정시키기로 했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'train-new-recruits',
|
|
label: '새 병사를 함께 훈련한다',
|
|
response: '두 장수의 훈련 방식은 달랐지만, 병사들은 그 사이에서 빠르게 자리를 잡았다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-xuzhou',
|
|
title: '오랜 벗의 조언',
|
|
availableAfterBattleIds: [campBattleIds.seventh],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'간옹: 현덕, 서주를 맡는 일은 칼을 드는 일보다 더 무겁네. 백성은 말보다 먼저 밥과 길을 보네.',
|
|
'유비: 헌화, 내가 그 무게를 감당할 수 있을까 두렵소.',
|
|
'간옹: 두려움을 아는 사람이면 적어도 함부로 빼앗지는 않겠지. 그러니 먼저 사람을 살피게.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'listen-before-rule',
|
|
label: '먼저 백성의 말을 듣는다',
|
|
response: '간옹은 유비가 급히 명분을 세우기보다 서주의 목소리를 먼저 듣겠다고 하자 고개를 끄덕였다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'appoint-practical-aide',
|
|
label: '실무를 맡아 달라 한다',
|
|
response: '간옹은 투덜대면서도 군영 장부와 사람들의 청원을 살피겠다고 나섰다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-zhu-after-xuzhou',
|
|
title: '서주의 군량',
|
|
availableAfterBattleIds: [campBattleIds.seventh],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'미축: 유 장군, 서주의 창고는 아직 온전하지만 오래 버티려면 상인과 호족의 마음을 함께 묶어야 합니다.',
|
|
'유비: 백성에게 더 짐을 지우고 싶지는 않소. 다른 길이 있겠소?',
|
|
'미축: 사사로운 이익을 줄이고 공적인 신뢰를 세우면 길은 열립니다. 제가 그 일을 맡겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'protect-grain',
|
|
label: '군량을 백성과 나눈다',
|
|
response: '미축은 군량을 무기로 삼지 않겠다는 유비의 뜻을 확인하고 서주의 상단을 설득하기로 했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'secure-supply-route',
|
|
label: '보급로 정비를 부탁한다',
|
|
response: '미축은 곧장 장부를 펼쳐 길목과 창고를 다시 묶는 계획을 세웠다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-zhu-after-xiaopei',
|
|
title: '지켜낸 보급로',
|
|
availableAfterBattleIds: [campBattleIds.eighth],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'미축: 소패 보급로는 지켰지만, 서주는 아직 안정되었다고 보기 어렵습니다. 창고를 지켜도 사람의 마음이 흩어지면 다시 흔들립니다.',
|
|
'유비: 군량은 칼보다 조용하지만, 백성을 살리는 힘이 있소. 그 일을 계속 맡아 주시오.',
|
|
'미축: 맡겨 주신다면 상단과 호족을 설득해 서주의 숨통을 조금 더 넓히겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-supply-network',
|
|
label: '보급망을 믿고 맡긴다',
|
|
response: '미축은 유비가 실무를 믿고 맡기자 서주의 창고와 상단을 더 단단히 묶겠다고 답했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'share-grain-openly',
|
|
label: '군량 배분을 투명하게 한다',
|
|
response: '유비가 백성 앞에서 군량 배분을 숨기지 않겠다고 하자 미축의 표정이 한결 밝아졌다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-xiaopei',
|
|
title: '여포를 맞을 것인가',
|
|
availableAfterBattleIds: [campBattleIds.eighth],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'간옹: 여포를 받아들이면 의롭다는 말은 들을 걸세. 하지만 사람들은 그 뒤에 칼집이 몇 개인지도 세어 볼 거야.',
|
|
'유비: 곤궁한 이를 외면할 수는 없소. 그러나 서주 백성의 근심도 가볍게 여길 수 없소.',
|
|
'간옹: 그래서 선택에는 늘 값을 치르는 법이지. 적어도 그 값을 알고 받아들이게.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'accept-with-guard',
|
|
label: '경계를 세우고 맞는다',
|
|
response: '간옹은 유비가 의와 경계를 함께 세우겠다고 하자, 그나마 현실적인 대답이라고 웃었다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'ask-people-first',
|
|
label: '백성의 불안을 먼저 살핀다',
|
|
response: '간옹은 서주 사람들의 두려움을 먼저 살피겠다는 말에 고개를 끄덕였다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhang-after-xiaopei',
|
|
title: '성문을 지키는 두 장수',
|
|
availableAfterBattleIds: [campBattleIds.eighth],
|
|
unitIds: ['guan-yu', 'zhang-fei'],
|
|
bondId: 'guan-yu__zhang-fei',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'관우: 여포가 온다면 군율이 먼저 흔들릴 것이다. 성문과 술자리를 모두 조심해야 한다.',
|
|
'장비: 왜 나를 보며 말하시오? 하지만 알겠소. 이번에는 먼저 소리치기보다 성문부터 붙잡겠소.',
|
|
'관우: 형님의 뜻을 지키려면 우리부터 흔들리지 않아야 한다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'guard-gates',
|
|
label: '성문 경계를 강화한다',
|
|
response: '관우와 장비는 성문 근무와 순찰을 나누어 맡기로 했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'restrain-temper',
|
|
label: '장비의 성급함을 다잡는다',
|
|
response: '장비는 툴툴대면서도 이번만큼은 술보다 군율을 먼저 보겠다고 약속했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-xuzhou-gate',
|
|
title: '성문 뒤의 후회',
|
|
availableAfterBattleIds: [campBattleIds.ninth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'장비: 형님, 성문은 지켰지만 조표 같은 자들이 움직인 것 자체가 제 불찰입니다.',
|
|
'유비: 익덕, 성급함은 네 강점이자 약점이다. 하지만 후회로 멈추면 다음 성문도 지키지 못한다.',
|
|
'장비: 다음에는 술도, 분노도, 먼저 앞세우지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-reform',
|
|
label: '다시 맡기되 군율을 세운다',
|
|
response: '장비는 꾸짖음보다 믿음을 받은 것에 더 무겁게 고개를 숙였다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'share-command',
|
|
label: '관우와 함께 성문을 맡긴다',
|
|
response: '장비는 운장 형님과 함께라면 이번 실수를 씻어 보이겠다고 답했다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-xuzhou-gate',
|
|
title: '의와 현실 사이',
|
|
availableAfterBattleIds: [campBattleIds.ninth],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'간옹: 현덕, 여포를 들인 일은 이제 말 한마디로 되돌릴 수 없네. 사람들은 이미 네 의와 그의 칼을 함께 보고 있어.',
|
|
'유비: 의로 시작한 일이 백성을 위태롭게 한다면, 나는 무엇을 지켜야 하오?',
|
|
'간옹: 그래서 더더욱 눈을 뜨고 있어야지. 의도 잠들면 남의 칼집이 된다네.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'watch-lu-bu',
|
|
label: '여포의 움직임을 감시한다',
|
|
response: '간옹은 마침내 현실적인 지시가 나왔다며 사람을 붙이겠다고 했다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'protect-civilians-first',
|
|
label: '백성 피난로를 먼저 정한다',
|
|
response: '간옹은 서주를 잃더라도 사람을 잃지 않겠다는 말에 한숨 섞인 미소를 지었다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-after-xuzhou-gate',
|
|
title: '성문과 창고',
|
|
availableAfterBattleIds: [campBattleIds.ninth],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'미축: 성문이 흔들리면 창고도 오래 버티지 못합니다. 군량을 지키는 길과 성문을 지키는 길은 다르지 않습니다.',
|
|
'유비: 그렇다면 창고의 문서와 수레를 성문 수비와 함께 움직입시다. 길이 끊겨도 백성에게 갈 몫은 남겨 두어야 하오.',
|
|
'미축: 그 뜻이면 충분합니다. 다음 혼란에 대비하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'move-grain-carts',
|
|
label: '군량 수레를 재배치한다',
|
|
response: '미축은 군량 수레를 성문 안쪽 길목으로 옮겨 비상시에 대비했다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'guard-ledgers',
|
|
label: '창고 장부를 보호한다',
|
|
response: '관우는 장부를 지키는 일도 전열을 지키는 일이라며 병사를 배치했다.',
|
|
rewardExp: 7
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-xuzhou-loss',
|
|
title: '잃은 성 앞에서',
|
|
availableAfterBattleIds: [campBattleIds.tenth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'장비: 형님, 서주를 잃은 죄는 모두 제게 있습니다. 제 목을 베어 군율을 세우십시오.',
|
|
'유비: 익덕의 목을 베면 서주가 돌아오느냐. 살아서 다시 지키는 것이 네 벌이다.',
|
|
'장비: 다시는 술과 분노가 창끝보다 먼저 나가지 않게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'forgive-and-command',
|
|
label: '용서하되 군율을 맡긴다',
|
|
response: '장비는 꾸지람보다 무거운 신뢰를 받고, 다음 전장에서는 먼저 자신을 다스리겠다고 맹세했다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'make-amends-through-action',
|
|
label: '공으로 갚게 한다',
|
|
response: '장비는 서주를 잃은 분노를 다음 싸움의 방패로 삼겠다고 답했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-xuzhou-loss',
|
|
title: '흩어지지 않는 전열',
|
|
availableAfterBattleIds: [campBattleIds.tenth],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'관우: 성은 잃었으나 형님의 깃발 아래 모인 마음은 아직 남았습니다.',
|
|
'유비: 운장, 내가 의를 앞세워 사람을 들였고 그 의가 백성을 흔들었다.',
|
|
'관우: 의는 버릴 것이 아니라 더 단단히 세울 것입니다. 이번 패배가 그 기둥이 되게 하십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'hold-the-banner',
|
|
label: '깃발을 다시 세운다',
|
|
response: '관우는 흩어진 병사들이 볼 수 있도록 유비군의 깃발을 다시 높이 세웠다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'protect-the-people',
|
|
label: '피난민을 먼저 모은다',
|
|
response: '관우는 패전의 행군에서도 백성을 버리지 않는 것이 형님의 길이라며 고개를 끄덕였다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-xuzhou-loss',
|
|
title: '허도로 가는 말',
|
|
availableAfterBattleIds: [campBattleIds.tenth],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'간옹: 현덕, 조조에게 몸을 의탁하는 일은 달갑지 않겠지. 하지만 지금은 그 그늘을 지나야 다음 길이 열린다네.',
|
|
'유비: 조조의 그늘이 너무 짙어 내 뜻까지 가리면 어찌하오?',
|
|
'간옹: 그때를 대비해 사람과 말을 잃지 말아야지. 뜻도 살아 있는 사람에게 붙는 법이네.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'send-envoys',
|
|
label: '먼저 사자를 보낸다',
|
|
response: '간옹은 허도로 향할 말을 고르고, 유비군이 굴복이 아닌 임시 의탁임을 전할 문장을 다듬었다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'hide-strength',
|
|
label: '병력 규모를 감춘다',
|
|
response: '간옹은 조조가 보는 숫자와 실제로 보존할 숫자를 나누어 생각해야 한다고 조언했다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-cao-cao-refuge',
|
|
title: '남의 깃발 아래',
|
|
availableAfterBattleIds: [campBattleIds.eleventh],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'관우: 조조의 깃발 아래 서 있으니 병사들의 눈빛이 조심스러워졌습니다.',
|
|
'유비: 운장, 깃발은 잠시 빌릴 수 있어도 뜻은 빌릴 수 없다. 우리가 지켜야 할 선을 잊지 맙시다.',
|
|
'관우: 형님의 뜻이 흔들리지 않는다면, 남의 진영에서도 우리의 칼은 길을 잃지 않을 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'keep-oath-visible',
|
|
label: '도원의 뜻을 분명히 한다',
|
|
response: '관우는 병사들에게 조조의 명을 따르되 유비군의 군율을 잃지 말라고 전했다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'watch-cao-cao-camp',
|
|
label: '조조 진영의 움직임을 살핀다',
|
|
response: '관우는 지나친 경계가 오히려 의심을 부른다며 조용한 감시를 맡았다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-cao-cao-refuge',
|
|
title: '웃음 뒤의 문장',
|
|
availableAfterBattleIds: [campBattleIds.eleventh],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'간옹: 조조는 웃으며 맞았지만, 그 웃음은 사람을 묶는 줄이기도 하네.',
|
|
'유비: 그 줄을 끊으면 목이 조이고, 그대로 두면 발이 묶일 것이오.',
|
|
'간옹: 그래서 말과 문서를 조심해야지. 지금은 충심을 보이되, 떠날 길의 이름을 잊지 않는 때라네.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-carefully',
|
|
label: '문서를 조심스럽게 정리한다',
|
|
response: '간옹은 조조에게 보일 문서와 유비군 내부 문서를 따로 정리했다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'hear-rumors',
|
|
label: '허도 소문을 모은다',
|
|
response: '간옹은 말 많은 술자리와 시장길에서 조조 진영의 분위기를 살피기로 했다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-after-cao-cao-refuge',
|
|
title: '빌린 창고',
|
|
availableAfterBattleIds: [campBattleIds.eleventh],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'미축: 조조군의 창고를 빌리더라도 우리 병사의 식량 장부는 따로 남겨 두겠습니다.',
|
|
'유비: 군량 하나에도 마음이 묶이는 법이오. 빌린 것은 빌린 것이라 분명히 해야 하오.',
|
|
'미축: 그렇다면 보급은 받되, 유비군이 스스로 버틸 양도 함께 모아 두겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-ledgers',
|
|
label: '장부를 분리한다',
|
|
response: '미축은 조조군 보급과 유비군 예비 군량을 따로 기록해 훗날의 발목을 줄였다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'save-refugee-grain',
|
|
label: '피난민 군량을 남긴다',
|
|
response: '미축은 병사 몫을 줄이지 않으면서도 피난민에게 갈 곡식을 별도로 숨겨 두었다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-xiapi-outer',
|
|
title: '창끝을 누르는 법',
|
|
availableAfterBattleIds: [campBattleIds.twelfth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'장비: 형님, 여포 놈의 깃발이 눈앞에 있는데도 창을 참는 일이 쉽지 않았습니다.',
|
|
'유비: 참았기에 포위망이 무너지지 않았다. 익덕의 창은 분노보다 늦게 나갈 때 더 무겁소.',
|
|
'장비: 다음에는 여포의 말발굽보다 먼저 제 마음을 묶어 두겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'praise-restraint',
|
|
label: '참은 공을 칭찬한다',
|
|
response: '장비는 호통보다 뜻밖의 칭찬에 더 깊이 고개를 숙였고, 다음 싸움에서도 전열을 지키겠다고 다짐했다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'assign-dike-front',
|
|
label: '둑길 전열을 맡긴다',
|
|
response: '장비는 자신이 앞에서 버티면 여포군 기병도 쉽게 뚫지 못할 것이라며 창대를 움켜쥐었다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-xiapi-outer',
|
|
title: '공적을 쓰는 붓',
|
|
availableAfterBattleIds: [campBattleIds.twelfth],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'간옹: 오늘 공적은 조조의 장부에도 오르겠지만, 그 장부가 우리 목줄이 되지 않게 써야 하네.',
|
|
'유비: 공을 세워도 빚이 되고, 물러서도 의심이 되는 형국이오.',
|
|
'간옹: 그래서 말의 끝을 남겨야지. 우리는 조조를 위해서만 싸운 것이 아니라 서주 백성을 위해 싸웠다고.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'record-people-first',
|
|
label: '백성 보호를 먼저 기록한다',
|
|
response: '간옹은 전공 보고의 첫 줄에 피난민과 보급로 보호를 적어 유비군의 명분을 분명히 했다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'keep-private-notes',
|
|
label: '별도 기록을 남긴다',
|
|
response: '간옹은 조조에게 보일 보고와 훗날 유비군이 기억할 기록을 따로 남겼다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-after-xiapi-outer',
|
|
title: '물가의 보급선',
|
|
availableAfterBattleIds: [campBattleIds.twelfth],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'미축: 강가 포위전은 싸움보다 군량이 먼저 흔들립니다. 조조군 배급만 믿으면 우리 병사 마음도 함께 묶입니다.',
|
|
'유비: 독자 보급이 곧 독자적인 마음이오. 작은 양이라도 우리가 스스로 마련할 길을 남깁시다.',
|
|
'미축: 그렇다면 하비 외곽에서 거둔 전리품은 공용과 예비로 나누어 두겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'split-siege-supplies',
|
|
label: '보급을 둘로 나눈다',
|
|
response: '미축은 조조군 공용 보급과 유비군 예비 보급을 나누어, 훗날 떠날 수 있는 여지를 남겼다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'secure-river-cart',
|
|
label: '강가 수레를 확보한다',
|
|
response: '미축은 강가에서 쓸 수레와 배를 따로 챙겨 다음 포위전의 군량 흐름을 안정시켰다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-lu-bu-fall',
|
|
title: '무용과 믿음',
|
|
availableAfterBattleIds: [campBattleIds.thirteenth],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'관우: 여포의 무용은 천하가 알았으나, 끝내 누구도 오래 믿게 하지 못했습니다.',
|
|
'유비: 힘만으로 길을 열 수는 있어도 사람 마음을 머물게 하지는 못하오.',
|
|
'관우: 그렇다면 형님의 길은 더 무거워질 것입니다. 저 또한 그 길을 지키겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'honor-trust-over-force',
|
|
label: '믿음의 무게를 말한다',
|
|
response: '관우는 여포의 말로를 힘의 패배가 아니라 믿음의 패배로 새기며 결의를 다졌다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'prepare-for-cao-watch',
|
|
label: '조조의 시선을 경계한다',
|
|
response: '관우는 칼을 거두는 때도 전투라 여기며 조조의 경계를 살피겠다고 답했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-yong-after-lu-bu-fall',
|
|
title: '말 한마디의 무게',
|
|
availableAfterBattleIds: [campBattleIds.thirteenth],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'간옹: 현덕, 여포가 사라졌으니 조조는 이제 우리를 더 오래 바라볼 걸세.',
|
|
'유비: 오늘 한 말이 내일의 의심이 될 수 있겠군.',
|
|
'간옹: 그러니 말은 짧게, 뜻은 길게 남기세. 떠날 명분은 오늘부터 쌓아야 하네.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'keep-words-measured',
|
|
label: '말을 아끼게 한다',
|
|
response: '간옹은 조조 앞에서 드러낼 말과 속으로 남길 뜻을 분리해 두었다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'collect-leaving-cause',
|
|
label: '떠날 명분을 모은다',
|
|
response: '간옹은 조조 휘하에서 겪는 작은 어긋남도 훗날의 기록으로 남기기 시작했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-after-lu-bu-fall',
|
|
title: '남겨 둘 군량',
|
|
availableAfterBattleIds: [campBattleIds.thirteenth],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'미축: 하비의 보급품은 조조군 장부로 들어가겠지만, 우리 병사들이 떠날 때 쓸 몫도 필요합니다.',
|
|
'유비: 떠날 길을 생각하는 것이 불충으로 보일 수도 있소.',
|
|
'미축: 뜻을 지키려면 길도 있어야 합니다. 군량은 의리를 움직이게 하는 바퀴입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'set-aside-march-grain',
|
|
label: '행군 군량을 남긴다',
|
|
response: '미축은 장부 밖으로 새지 않는 선에서 유비군의 독자 행군 물자를 확보했다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'protect-xu-refugees',
|
|
label: '서주 피난민을 챙긴다',
|
|
response: '미축은 하비 뒤편의 피난민에게 갈 식량을 남겨 유비군의 명분을 조용히 지켰다.',
|
|
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
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-jian-after-yuan-refuge',
|
|
title: '큰 깃발 아래의 말',
|
|
availableAfterBattleIds: [campBattleIds.fifteenth],
|
|
unitIds: ['liu-bei', 'jian-yong'],
|
|
bondId: 'liu-bei__jian-yong',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'간옹: 원소의 장막은 크지만, 그 안의 사람들 말도 그만큼 큽니다. 주공의 이름이 오래 머물수록 여러 귀에 다르게 들릴 것입니다.',
|
|
'유비: 우리가 의탁한 것은 몸이지 뜻이 아니오. 그러나 그 뜻을 지키려면 지금은 말 한마디도 조심해야 하겠지.',
|
|
'간옹: 그렇다면 제가 먼저 가벼운 말로 무거운 시선을 흩뜨리겠습니다. 주공께서는 백성을 향한 뜻만 흐리지 마십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'measure-words-in-yuan-camp',
|
|
label: '말을 낮추고 뜻을 숨긴다',
|
|
response: '간옹은 원소 진영의 술자리와 군막 사이를 오가며 유비군의 속내가 쉽게 드러나지 않도록 말을 다듬었다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'keep-liu-bei-name-clean',
|
|
label: '유비의 명분을 조용히 남긴다',
|
|
response: '간옹은 크게 외치지 않는 방식으로 유비가 조조를 떠난 이유와 백성을 살핀 행적을 진영 곳곳에 흘렸다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-sun-after-yuan-refuge',
|
|
title: '의탁의 문서',
|
|
availableAfterBattleIds: [campBattleIds.fifteenth],
|
|
unitIds: ['liu-bei', 'sun-qian'],
|
|
bondId: 'liu-bei__sun-qian',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'손건: 원소에게 보낼 문서는 이미 통과했지만, 머무는 동안의 문서가 더 중요합니다. 작은 약속이 나중에 큰 족쇄가 될 수 있습니다.',
|
|
'유비: 지금 얻은 쉼터가 훗날 발목을 잡아서는 안 되겠지. 우리가 받은 은혜는 잊지 않되, 길을 잃지는 말아야 하오.',
|
|
'손건: 그 뜻을 문서마다 남기겠습니다. 주공의 군은 원소의 사병이 아니라, 난세를 건너는 의로운 객군으로 남아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-guest-army-status',
|
|
label: '객군의 지위를 분명히 한다',
|
|
response: '손건은 유비군의 지위를 원소 진영의 객군으로 적어, 명령 체계에 휩쓸릴 여지를 줄였다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'record-cao-pursuit-proof',
|
|
label: '조조 추격의 증거를 남긴다',
|
|
response: '손건은 채양 추격대의 움직임을 기록해 조조와의 긴장이 실제였음을 원소 진영에 설득할 근거로 남겼다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhang-mi-after-yuan-refuge',
|
|
title: '낯선 군영의 밥',
|
|
availableAfterBattleIds: [campBattleIds.fifteenth],
|
|
unitIds: ['zhang-fei', 'mi-zhu'],
|
|
bondId: 'zhang-fei__mi-zhu',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'장비: 원소군 밥은 양은 많아도 속이 편치 않소. 남의 군량을 먹으니 창끝까지 남의 눈치를 보는 기분이오.',
|
|
'미축: 그래서 우리 몫의 장부를 따로 두려 합니다. 받은 것은 받은 대로, 쓴 것은 쓴 대로 남겨야 훗날 떳떳하게 떠날 수 있습니다.',
|
|
'장비: 좋소. 그럼 나는 병사들이 주눅 들지 않게 술 대신 목소리를 나눠 주겠소. 배가 차도 기가 죽으면 싸움은 못 하니까.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-supply-ledger',
|
|
label: '보급 장부를 따로 둔다',
|
|
response: '미축은 유비군의 군량 장부를 따로 정리해 원소 진영에 빚이 쌓이는 일을 줄였고, 장비는 병사들에게 그 뜻을 크게 알렸다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'raise-camp-spirits',
|
|
label: '병사들의 사기를 다독인다',
|
|
response: '장비는 낯선 군영에서 움츠러든 병사들을 불러 세워 웃음과 호통으로 사기를 끌어올렸고, 미축은 작은 술과 밥을 나눴다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhao-after-liu-biao-refuge',
|
|
title: '다시 맡긴 창',
|
|
availableAfterBattleIds: [campBattleIds.sixteenth],
|
|
unitIds: ['liu-bei', 'zhao-yun'],
|
|
bondId: 'liu-bei__zhao-yun',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'조운: 형주 관문까지 함께 달리며 확신했습니다. 주공의 깃발은 몸을 숨기는 깃발이 아니라, 다시 백성을 모으는 깃발입니다.',
|
|
'유비: 자룡이 돌아온 것은 내게 큰 힘이오. 다만 우리는 아직 남의 땅에 의탁한 몸이니, 창끝만큼 마음도 곧아야 하오.',
|
|
'조운: 그렇다면 제 창은 앞길을 열고, 제 마음은 주공의 뜻이 흐트러지지 않게 지키겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-zhao-yun-vanguard',
|
|
label: '선봉을 맡긴다',
|
|
response: '유비는 조운에게 다음 행군의 선봉을 맡겼고, 조운은 조용히 말고삐를 잡으며 유비군의 앞길을 정찰했다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'ask-zhao-yun-people',
|
|
label: '백성의 길을 살핀다',
|
|
response: '조운은 형주 주변의 피난민 길목을 살펴 유비군이 머무는 동안 백성에게 폐가 되지 않을 길을 찾아냈다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhao-after-liu-biao-refuge',
|
|
title: '두 충의의 눈',
|
|
availableAfterBattleIds: [campBattleIds.sixteenth],
|
|
unitIds: ['guan-yu', 'zhao-yun'],
|
|
bondId: 'guan-yu__zhao-yun',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'관우: 자룡의 창끝은 빠르되 가볍지 않군. 적을 쫓을 때도 군의 중심을 잃지 않았소.',
|
|
'조운: 운장께서 전열을 잡아 주셨기에 제가 옆길을 열 수 있었습니다. 충의는 앞뒤가 다르지 않다는 것을 배웠습니다.',
|
|
'관우: 그렇다면 다음 전장에서도 서로의 빈틈을 맡깁시다. 주공의 길을 지키는 데는 칼과 창이 다르지 않소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'train-sword-spear-timing',
|
|
label: '칼과 창의 호흡을 맞춘다',
|
|
response: '관우와 조운은 짧은 훈련으로 돌파와 엄호의 순서를 맞췄고, 병사들은 두 장수의 호흡을 보고 사기를 얻었다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'share-guard-watch',
|
|
label: '야간 경계를 나눈다',
|
|
response: '두 사람은 밤 경계를 나누어 섰고, 형주 군영의 낯선 시선 속에서도 유비군의 군율은 흔들리지 않았다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-sun-after-liu-biao-refuge',
|
|
title: '객장의 예',
|
|
availableAfterBattleIds: [campBattleIds.sixteenth],
|
|
unitIds: ['liu-bei', 'sun-qian'],
|
|
bondId: 'liu-bei__sun-qian',
|
|
rewardExp: 18,
|
|
lines: [
|
|
'손건: 유표가 주공을 예로 맞이했지만, 형주 안의 사람들은 저마다 주공의 이름을 다르게 저울질할 것입니다.',
|
|
'유비: 손님의 예를 받되 주인의 마음을 어지럽히지는 말아야 하오. 그러나 백성을 돌볼 뜻까지 숨길 수는 없소.',
|
|
'손건: 그래서 말과 문서의 결을 나누겠습니다. 겉으로는 예를 지키고, 안으로는 뜻을 이어갈 사람을 찾겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-guest-etiquette',
|
|
label: '객장의 예를 분명히 한다',
|
|
response: '손건은 유비군의 군영 예절과 방문 절차를 정리해 형주 관리들의 경계심을 낮췄다.',
|
|
rewardExp: 8
|
|
},
|
|
{
|
|
id: 'listen-to-jingzhou-scholars',
|
|
label: '형주 선비들의 말을 듣는다',
|
|
response: '손건은 형주 선비들의 소문을 모아 융중의 와룡이라는 이름을 유비에게 조심스럽게 전했다.',
|
|
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
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhuge-after-bowang-ambush',
|
|
title: '불길 뒤의 큰길',
|
|
availableAfterBattleIds: [campBattleIds.eighteenth],
|
|
unitIds: ['liu-bei', 'zhuge-liang'],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'유비: 오늘 박망파에서 선생의 계책이 아니었다면, 조조군의 선봉을 작은 군영으로 막지 못했을 것이오.',
|
|
'제갈량: 주공께서 의심하지 않고 맡기셨기에 장수들도 움직였습니다. 계책은 사람의 마음이 모일 때 비로소 전장이 됩니다.',
|
|
'유비: 그렇다면 다음에는 더 큰 마음을 모아야겠소. 강동과 손잡는 길도 선생과 함께 열겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-grand-strategy',
|
|
label: '큰 계책을 계속 맡긴다',
|
|
response: '유비는 제갈량에게 다음 외교와 군사 판단을 함께 맡겼고, 군영 안의 의심은 크게 줄어들었다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'share-command-with-generals',
|
|
label: '장수들에게 계책을 설명하게 한다',
|
|
response: '제갈량은 장수들 앞에서 박망파의 순서를 차분히 되짚었고, 다음 명령은 더 빠르게 받아들여질 준비가 되었다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhang-zhuge-after-bowang-ambush',
|
|
title: '호통과 부채',
|
|
availableAfterBattleIds: [campBattleIds.eighteenth],
|
|
unitIds: ['zhang-fei', 'zhuge-liang'],
|
|
bondId: 'zhang-fei__zhuge-liang',
|
|
rewardExp: 20,
|
|
lines: [
|
|
'장비: 군사 양반, 오늘은 인정하겠소. 불길이 오르니 적들이 제 창보다 먼저 겁을 먹더군.',
|
|
'제갈량: 익덕 장군의 창이 뒤에서 기다린다는 것을 적도 알았기 때문입니다. 계책만 있고 칼끝이 없으면 적은 물러나지 않습니다.',
|
|
'장비: 좋소. 다음에도 먼저 설명만 해 주시오. 내가 어디서 소리쳐야 적이 더 잘 넘어지는지만 알면 됩니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'plan-zhang-fei-ambush-call',
|
|
label: '매복 호령을 맞춘다',
|
|
response: '제갈량은 장비가 뛰쳐나올 때와 물러설 때를 정했고, 장비는 드물게 끝까지 듣고 고개를 끄덕였다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'praise-bold-charge',
|
|
label: '돌파의 때를 칭찬한다',
|
|
response: '장비는 계책 안에서도 자신의 용맹이 필요하다는 말에 크게 웃었고, 다음 전장에서도 군사의 손짓을 보겠다고 했다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sun-zhuge-after-bowang-ambush',
|
|
title: '강동으로 보낼 말',
|
|
availableAfterBattleIds: [campBattleIds.eighteenth],
|
|
unitIds: ['sun-qian', 'zhuge-liang'],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'손건: 박망파의 승리는 작은 군이 큰 군을 늦출 수 있다는 증거입니다. 강동에 보낼 말에도 힘이 생겼습니다.',
|
|
'제갈량: 강동은 말보다 형세를 볼 것입니다. 조조가 멈추지 않는다는 사실과, 주공이 백성을 버리지 않는다는 사실을 함께 전해야 합니다.',
|
|
'손건: 그렇다면 문서는 제가 다듬고, 큰 논리는 선생께서 세우십시오. 말과 계책이 같은 방향을 보아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-jiangdong-letter',
|
|
label: '강동 서신을 초안한다',
|
|
response: '손건은 제갈량의 형세 판단을 바탕으로 강동에 보낼 첫 서신의 골격을 잡았다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'record-cao-vanguard-delay',
|
|
label: '조조 선봉 지연을 기록한다',
|
|
response: '박망파에서 벌어들인 시간을 기록으로 남기자, 유비군의 작은 승리가 다음 외교의 근거가 되었다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhao-after-changban-refuge',
|
|
title: '백마가 지킨 길',
|
|
availableAfterBattleIds: [campBattleIds.nineteenth],
|
|
unitIds: ['liu-bei', 'zhao-yun'],
|
|
bondId: 'liu-bei__zhao-yun',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'유비: 자룡, 장판파의 먼지 속에서도 그대의 창은 백성을 향해 길을 열었소.',
|
|
'조운: 주공께서 백성을 버리지 않으셨기에 저도 물러서지 않을 수 있었습니다.',
|
|
'유비: 오늘의 공은 전공보다 무겁소. 다음 길에서도 그 마음을 함께 지켜 주시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-refugee-guard',
|
|
label: '피난 행렬 호위를 맡긴다',
|
|
response: '조운은 말없이 창을 세웠고, 유비군의 피난길은 더 단단한 호위를 얻었다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'praise-white-horse-loyalty',
|
|
label: '백마의 의리를 칭찬한다',
|
|
response: '주공의 말에 조운은 깊이 고개를 숙였다. 흩어진 군영 안에서도 두 사람의 신뢰가 또렷해졌다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhang-zhuge-after-changban-refuge',
|
|
title: '다리목의 호령',
|
|
availableAfterBattleIds: [campBattleIds.nineteenth],
|
|
unitIds: ['zhang-fei', 'zhuge-liang'],
|
|
bondId: 'zhang-fei__zhuge-liang',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'장비: 군사, 다리목에서 한 번 호통치니 놈들이 눈치를 보더군. 이번엔 계책이 아니라 목청이 이긴 것 아니오?',
|
|
'제갈량: 익덕 장군의 호령이 먹힌 것은 설 곳을 정확히 골랐기 때문입니다. 그 자리까지 가는 길도 계책의 일부입니다.',
|
|
'장비: 하하, 그렇다면 다음에도 어디서 소리치면 되는지만 알려 주시오. 나머지는 내 창이 알아서 하겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'fix-bridge-ambush-signals',
|
|
label: '다리목 신호를 정한다',
|
|
response: '제갈량은 장비가 나서야 할 순간을 짧은 깃발 신호로 정했고, 장비는 복잡한 설명보다 마음에 든다고 웃었다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'let-roar-lead',
|
|
label: '호령을 먼저 세운다',
|
|
response: '장비의 호령이 적의 발을 묶고, 제갈량의 손짓이 그 뒤를 받치는 새 호흡이 생겼다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhuge-after-changban-refuge',
|
|
title: '피난길과 강동',
|
|
availableAfterBattleIds: [campBattleIds.nineteenth],
|
|
unitIds: ['liu-bei', 'zhuge-liang'],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'유비: 오늘 지킨 것은 길 하나였지만, 그 길 위의 사람들은 천하보다 가벼울 수 없었소.',
|
|
'제갈량: 바로 그 점이 강동에 전할 말입니다. 조조의 힘만이 아니라 주공이 무엇을 지키려 하는지를 보여야 합니다.',
|
|
'유비: 그렇다면 손권에게 갈 말도 이 피난길에서 시작해야겠구려.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-refugee-road-record',
|
|
label: '피난로 기록을 남긴다',
|
|
response: '제갈량은 장판파의 피난로와 조조군의 추격 속도를 함께 적어 강동 사절 문서의 근거로 삼았다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'align-people-and-strategy',
|
|
label: '백성과 대계를 함께 본다',
|
|
response: '유비와 제갈량은 백성을 지키는 마음과 천하를 보는 계책이 서로 멀지 않음을 확인했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sun-zhuge-after-jiangdong-envoy',
|
|
title: '강동으로 보낼 논리',
|
|
availableAfterBattleIds: [campBattleIds.twentieth],
|
|
unitIds: ['sun-qian', 'zhuge-liang'],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'손건: 문서는 갖추었습니다. 하지만 강동은 글보다 사람의 눈빛을 먼저 볼 것입니다.',
|
|
'제갈량: 그러니 말의 순서가 중요합니다. 조조의 위협, 주공의 뜻, 강동이 얻을 계산을 차례로 놓아야 합니다.',
|
|
'손건: 그 순서라면 두려움과 이익, 명분이 한 문장 안에서 다투지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'sharpen-envoy-argument',
|
|
label: '설득의 순서를 다듬는다',
|
|
response: '손건은 제갈량의 큰 논리를 짧은 문장으로 정리했고, 강동 회담의 첫 마디가 또렷해졌다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'prepare-sun-quan-questions',
|
|
label: '손권의 질문을 예상한다',
|
|
response: '두 사람은 손권이 물을 병력, 보급, 퇴로 문제를 하나씩 적었고, 사절 문서는 훨씬 실전적인 답안을 품게 되었다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhuge-after-jiangdong-envoy',
|
|
title: '적벽을 보는 눈',
|
|
availableAfterBattleIds: [campBattleIds.twentieth],
|
|
unitIds: ['liu-bei', 'zhuge-liang'],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'유비: 강동으로 가는 길은 열렸지만, 손권의 마음까지 열린 것은 아니오.',
|
|
'제갈량: 그래서 주공의 뜻이 필요합니다. 우리는 땅을 빼앗으러 가는 것이 아니라 조조를 막을 길을 함께 찾으러 갑니다.',
|
|
'유비: 그 말을 들으니 적벽의 불길도 결국 사람을 지키기 위한 불길이어야 하겠구려.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-red-cliffs-parley',
|
|
label: '강동 회담을 맡긴다',
|
|
response: '유비는 제갈량에게 강동 회담의 전권을 맡겼고, 군영의 장수들도 다음 명령의 방향을 이해하기 시작했다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'anchor-strategy-in-people',
|
|
label: '백성의 뜻을 기준으로 삼는다',
|
|
response: '제갈량은 유비의 말을 회담의 중심 문장으로 남겼다. 전략은 숫자만이 아니라 지켜야 할 사람에서 출발했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhao-zhuge-after-jiangdong-envoy',
|
|
title: '사절을 지킨 창',
|
|
availableAfterBattleIds: [campBattleIds.twentieth],
|
|
unitIds: ['zhao-yun', 'zhuge-liang'],
|
|
bondId: 'zhao-yun__zhuge-liang',
|
|
rewardExp: 22,
|
|
lines: [
|
|
'조운: 사절을 지키는 길은 적장을 베는 길보다 더 조심스러웠습니다.',
|
|
'제갈량: 자룡 장군의 창이 길을 흔들림 없이 붙들었기에 문서도 사람도 강나루에 닿았습니다.',
|
|
'조운: 다음에도 지켜야 할 사람이 있다면 제 창은 그쪽으로 먼저 향하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'guard-envoy-flank',
|
|
label: '사절 측면 호위를 맡긴다',
|
|
response: '조운은 사절단이 움직일 때 어느 길을 먼저 살필지 정했고, 제갈량은 그 기동력을 믿고 더 과감한 동선을 그렸다.',
|
|
rewardExp: 9
|
|
},
|
|
{
|
|
id: 'map-river-crossing',
|
|
label: '강나루 길을 표시한다',
|
|
response: '조운이 본 강변의 샛길이 지도에 더해지자, 다음 장강 전장으로 향할 발판이 생겼다.',
|
|
rewardExp: 8
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhuge-after-red-cliffs-vanguard',
|
|
title: '첫 강안 전선',
|
|
availableAfterBattleIds: [campBattleIds.twentyFirst],
|
|
unitIds: ['liu-bei', 'zhuge-liang'],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
rewardExp: 32,
|
|
lines: [
|
|
'유비: 강가에 첫 전선을 세우고 보니, 조조의 대군이 얼마나 큰 물결인지 더 분명히 보이는구려.',
|
|
'제갈량: 그래서 더더욱 작은 승리가 필요했습니다. 오늘의 포구는 화공을 논할 자리이자 병사들이 믿을 첫 발판입니다.',
|
|
'유비: 불길을 일으키더라도 그 끝에는 백성이 숨 쉴 길이 있어야 하오. 그 기준만은 잊지 맙시다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'hold-river-line',
|
|
label: '강안 전선을 굳힌다',
|
|
response: '제갈량은 유비의 뜻을 전선 배치도 첫머리에 적었고, 적벽의 싸움이 단순한 불길이 아님을 분명히 했다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'protect-civilians-before-fire',
|
|
label: '화공 전 피난로를 살핀다',
|
|
response: '유비는 포구 주변 피난로를 먼저 열게 했고, 제갈량은 그 길을 화공 계획의 제약이 아니라 기준으로 삼았다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhao-zhuge-after-red-cliffs-vanguard',
|
|
title: '강안의 흰 창',
|
|
availableAfterBattleIds: [campBattleIds.twentyFirst],
|
|
unitIds: ['zhao-yun', 'zhuge-liang'],
|
|
bondId: 'zhao-yun__zhuge-liang',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'조운: 강가 길목은 말이 마음껏 달릴 수 없었습니다. 그래도 적 척후가 전선을 흔들기 전에는 끊을 수 있었습니다.',
|
|
'제갈량: 자룡 장군이 먼저 강안의 빈틈을 막아 주었기에 포구 수비대가 흐트러지지 않았습니다.',
|
|
'조운: 다음 싸움에서도 군사의 깃발이 가리키는 빈틈부터 메우겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-river-flanks',
|
|
label: '강안 측면로를 표시한다',
|
|
response: '조운은 말을 돌릴 수 있는 좁은 길을 지도에 남겼고, 제갈량은 그 길을 다음 화공 엄호선으로 삼았다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'guard-strategist-route',
|
|
label: '군사의 이동로를 호위한다',
|
|
response: '제갈량의 이동로가 조운의 창끝 아래 정리되자, 전선 뒤쪽의 책략 운용이 한결 안정되었다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sun-zhuge-after-red-cliffs-vanguard',
|
|
title: '동맹군의 기록',
|
|
availableAfterBattleIds: [campBattleIds.twentyFirst],
|
|
unitIds: ['sun-qian', 'zhuge-liang'],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'손건: 오늘의 승리는 작지만, 강동에 보여 줄 기록으로는 충분히 무겁습니다.',
|
|
'제갈량: 맞습니다. 유비군이 전선을 세웠고, 강동은 불을 준비할 시간을 얻었습니다. 기록은 그 균형을 보여야 합니다.',
|
|
'손건: 그렇다면 전공보다 동맹의 호흡을 먼저 적겠습니다. 함께 이긴 기록이어야 다음 명령도 흔들리지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-allied-record',
|
|
label: '동맹 전과를 함께 적는다',
|
|
response: '손건은 유비군과 강동군의 역할을 나누어 적었고, 제갈량은 그 기록을 주유에게 보낼 논거로 다듬었다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'summarize-fire-prep',
|
|
label: '화공 준비 상황을 요약한다',
|
|
response: '전초전의 포구, 배, 바람 기록이 한 장의 보고로 묶였고, 다음 적벽 계책의 설득력이 더해졌다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhuge-after-red-cliffs-fire',
|
|
title: '불길 뒤의 대계',
|
|
availableAfterBattleIds: [campBattleIds.twentySecond],
|
|
unitIds: ['liu-bei', 'zhuge-liang'],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
rewardExp: 34,
|
|
lines: [
|
|
'유비: 적벽의 불길은 조조를 물렸지만, 우리에게 머물 땅이 생긴 것은 아니오.',
|
|
'제갈량: 그래서 이제 싸움의 목적이 바뀝니다. 물러나는 조조를 보며 형주 남부의 길을 열어야 합니다.',
|
|
'유비: 백성이 다시 떠돌지 않게 하려면, 다음 걸음은 전공보다 근본을 세우는 일이겠구려.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'plan-jingzhou-foundation',
|
|
label: '형주 근거지 계획을 세운다',
|
|
response: '제갈량은 형주 남부의 군현과 민심을 나누어 적었고, 유비는 처음으로 머물 땅의 조건을 분명히 말했다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'temper-victory-pride',
|
|
label: '승리의 들뜸을 경계한다',
|
|
response: '유비는 큰 승리 뒤의 방심을 경계했고, 제갈량은 그 말을 다음 작전의 첫 원칙으로 삼았다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-zhuge-after-red-cliffs-fire',
|
|
title: '불길을 지킨 전열',
|
|
availableAfterBattleIds: [campBattleIds.twentySecond],
|
|
unitIds: ['guan-yu', 'zhuge-liang'],
|
|
bondId: 'guan-yu__zhuge-liang',
|
|
rewardExp: 26,
|
|
lines: [
|
|
'관우: 불길이 번질 때 병사들이 흔들렸으나, 전열을 무너뜨리지는 않았습니다.',
|
|
'제갈량: 운장 장군의 침착함이 있었기에 화선의 길이 끊기지 않았습니다. 계책도 그것을 지킬 칼이 있어야 완성됩니다.',
|
|
'관우: 다음 땅을 얻는 싸움에서도, 계책이 가리키는 곳을 제 칼로 지키겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'guard-next-crossing',
|
|
label: '다음 도하 지점을 맡긴다',
|
|
response: '제갈량은 관우에게 다음 강가 전열을 맡겼고, 관우는 묵묵히 청룡언월도를 세워 답했다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'honor-steady-line',
|
|
label: '흔들리지 않은 전열을 칭찬한다',
|
|
response: '관우는 큰 공을 말하지 않았지만, 병사들이 그의 등 뒤에서 안정을 찾았다는 말에는 조용히 고개를 숙였다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mi-after-red-cliffs-fire',
|
|
title: '불길 속 보급로',
|
|
availableAfterBattleIds: [campBattleIds.twentySecond],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'미축: 불길이 번지는 전장은 칼보다 물과 약이 먼저 필요했습니다. 젖은 약품을 버리고 마른 꾸러미부터 살렸습니다.',
|
|
'유비: 전공이 커도 병사가 쓰러지면 다음 걸음은 없소. 그대가 붙잡은 보급로가 곧 우리의 근본이오.',
|
|
'미축: 다음부터는 빠른 말과 마른 약품을 한 묶음으로 준비하겠습니다. 전장도 이제 더 넓어질 테니까요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-fast-supply-line',
|
|
label: '기동 보급로를 정한다',
|
|
response: '미축은 기동 부대가 바로 가져갈 수 있는 보급 꾸러미를 따로 묶었고, 유비는 다음 전장의 장기전을 대비했다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'reserve-dry-medicine',
|
|
label: '젖지 않는 약품을 챙긴다',
|
|
response: '미축은 젖은 강가에서도 쓸 수 있는 약품을 따로 묶었고, 유비는 병사들이 승리 뒤에도 무너지지 않게 했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-ma-after-jingzhou-south-entry',
|
|
title: '백미를 맞이하다',
|
|
availableAfterBattleIds: [campBattleIds.twentyThird],
|
|
unitIds: ['liu-bei', 'ma-liang'],
|
|
bondId: 'liu-bei__ma-liang',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'마량: 주공께서 형주 남부의 마을을 먼저 살피셨다는 말을 들었습니다. 그래서 저도 붓을 들고 찾아왔습니다.',
|
|
'유비: 땅을 얻는 일보다 사람을 잃지 않는 일이 먼저요. 선생이 남부의 길과 민심을 알려 준다면 큰 힘이 되겠소.',
|
|
'마량: 칼로 열 수 없는 문은 말과 문서로 열겠습니다. 다음 군현부터는 제가 앞서 백성의 사정을 살피겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'ask-ma-liang-local-voices',
|
|
label: '군현의 민심을 묻는다',
|
|
response: '마량은 영릉과 계양의 토호, 마을 장로, 상인 길목을 차분히 짚어 주었고 유비군의 다음 행군 명분이 선명해졌다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'entrust-scholar-letters',
|
|
label: '선비들에게 보낼 글을 맡긴다',
|
|
response: '유비는 형주 선비들에게 보낼 첫 문장을 마량에게 맡겼고, 백미의 이름은 군영 안에서 조용히 힘을 얻기 시작했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-ma-after-jingzhou-south-entry',
|
|
title: '형주의 지맥',
|
|
availableAfterBattleIds: [campBattleIds.twentyThird],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang',
|
|
rewardExp: 26,
|
|
lines: [
|
|
'제갈량: 큰 계책은 지도를 보지만, 실제 길은 그곳에 사는 사람의 입에서 나옵니다. 마량 선생의 기록이 필요합니다.',
|
|
'마량: 형주 남부는 길 하나가 성 하나와 같습니다. 어디를 먼저 설득하고 어디를 피해야 할지 적어 두겠습니다.',
|
|
'제갈량: 좋습니다. 이번부터 출전 명단을 고를 때도 전열만이 아니라 문서와 설득을 함께 보아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'map-commandery-roads',
|
|
label: '남부 군현의 길을 표시한다',
|
|
response: '마량이 좁은 산길과 마을 우회로를 표시하자, 제갈량의 다음 전장 배치안이 훨씬 촘촘해졌다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'compare-scholar-networks',
|
|
label: '선비들의 관계망을 맞춰 본다',
|
|
response: '두 사람은 선비층의 관계를 병력 배치처럼 정리했고, 싸우지 않고 열 수 있는 문이 몇 곳 보이기 시작했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sun-ma-after-jingzhou-south-entry',
|
|
title: '문서와 백미',
|
|
availableAfterBattleIds: [campBattleIds.twentyThird],
|
|
unitIds: ['sun-qian', 'ma-liang'],
|
|
bondId: 'sun-qian__ma-liang',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'손건: 외교 문서는 말이 곧 병사입니다. 마량 선생이 남부의 이름과 체면을 바로잡아 주면 설득이 쉬워질 겁니다.',
|
|
'마량: 손건 선생의 문장은 군영 밖에서도 곧게 들립니다. 거기에 현지의 호칭과 예법을 붙이면 문이 열릴 것입니다.',
|
|
'손건: 다음 출전부터는 칼을 드는 장수뿐 아니라, 문서를 들고 갈 장수도 생각해야겠군요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-commandery-letters',
|
|
label: '군현에 보낼 서신을 쓴다',
|
|
response: '손건과 마량은 군현별 서신 초안을 만들었고, 전투 사이 선택지가 더 넓어질 준비가 갖춰졌다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'sort-local-titles',
|
|
label: '토호들의 호칭을 정리한다',
|
|
response: '사소한 호칭 하나가 문을 닫기도 연다. 두 사람은 남부 토호들의 이름과 예를 새로 정리했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-yi-after-guiyang',
|
|
title: '예로 열린 계양',
|
|
availableAfterBattleIds: [campBattleIds.twentyFourth],
|
|
unitIds: ['liu-bei', 'yi-ji'],
|
|
bondId: 'liu-bei__yi-ji',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'이적: 계양의 문이 칼끝이 아니라 군율과 서신 앞에서 열렸습니다. 그런 군이라면 저도 따를 이유가 있습니다.',
|
|
'유비: 형주의 사람을 얻으려면 형주의 예를 알아야 하오. 선생이 길을 알려 준다면 다음 군현도 덜 흔들릴 것이오.',
|
|
'이적: 다음부터는 싸우기 전에 먼저 물어야 할 이름과 예가 있습니다. 제가 그 문장을 준비하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-guiyang-etiquette',
|
|
label: '계양의 예법을 맡긴다',
|
|
response: '이적은 계양의 토호와 문사 이름을 정리했고, 유비군의 다음 설득 문서는 한층 단정해졌다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'share-liu-bei-cause-with-yi-ji',
|
|
label: '유비군의 뜻을 설명한다',
|
|
response: '유비는 백성을 먼저 세우려는 뜻을 말했고, 이적은 그 뜻을 형주 사람들이 읽을 수 있는 문장으로 다듬었다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'ma-yi-after-guiyang',
|
|
title: '백미와 문사',
|
|
availableAfterBattleIds: [campBattleIds.twentyFourth],
|
|
unitIds: ['ma-liang', 'yi-ji'],
|
|
bondId: 'ma-liang__yi-ji',
|
|
rewardExp: 26,
|
|
lines: [
|
|
'마량: 계양은 열렸지만 무릉은 산길이 험합니다. 길을 아는 사람과 마음을 아는 사람이 함께 움직여야 합니다.',
|
|
'이적: 마량 선생의 기록은 정확하고, 제 일은 그 기록을 상대가 받아들일 말로 바꾸는 것입니다.',
|
|
'마량: 그렇다면 다음 전장부터는 길과 문장이 함께 가겠군요. 무릉으로 가기 전에 마을별 이름을 맞춰 봅시다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'align-commandery-records',
|
|
label: '군현 기록을 맞춘다',
|
|
response: '마량과 이적은 마을별 호칭과 길목 기록을 하나로 맞추었고, 무릉 준비의 빈칸이 줄어들었다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'prepare-mountain-letters',
|
|
label: '산길 마을 서신을 준비한다',
|
|
response: '두 문사는 무릉 산길 마을에 보낼 짧은 서신을 써 두었고, 다음 행군의 설득력이 깊어졌다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sun-yi-after-guiyang',
|
|
title: '외교 문안',
|
|
availableAfterBattleIds: [campBattleIds.twentyFourth],
|
|
unitIds: ['sun-qian', 'yi-ji'],
|
|
bondId: 'sun-qian__yi-ji',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'손건: 저는 명분을 곧게 세우는 편이고, 이적 선생은 상대의 체면을 세우는 법을 아는군요.',
|
|
'이적: 둘 중 하나만 있으면 문이 반쯤만 열립니다. 곧은 뜻과 부드러운 예가 함께 있어야 합니다.',
|
|
'손건: 다음 군현으로 가는 서신은 둘이 함께 씁시다. 칼보다 먼저 도착해야 할 글입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-joint-diplomacy-note',
|
|
label: '공동 서신을 쓴다',
|
|
response: '손건과 이적은 유비군의 뜻과 형주의 예를 함께 담은 서신을 만들었고, 군영의 외교 축이 넓어졌다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'sort-commandery-audience',
|
|
label: '면담 순서를 정한다',
|
|
response: '두 사람은 누구를 먼저 만나야 성문이 열릴지 순서를 세웠고, 다음 군현 접근이 조금 더 현실적인 계획이 되었다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-gong-after-wuling',
|
|
title: '산길의 귀순',
|
|
availableAfterBattleIds: [campBattleIds.twentyFifth],
|
|
unitIds: ['liu-bei', 'gong-zhi'],
|
|
bondId: 'liu-bei__gong-zhi',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'공지: 무릉의 길은 험하지만, 백성이 숨을 곳은 더 좁습니다. 주공께서는 그 길을 밟지 않고 열어 주셨습니다.',
|
|
'유비: 그대가 산길과 마을을 아는 만큼, 백성의 두려움도 알고 있겠지. 앞으로 그 길을 함께 살피고 싶소.',
|
|
'공지: 제가 아는 길을 바치겠습니다. 다만 무릉 사람들의 이름을 먼저 불러 주십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-wuling-roads',
|
|
label: '무릉 길 안내를 맡긴다',
|
|
response: '공지는 무릉 산길의 갈림길과 창고 위치를 정리했고, 유비군의 행군 명단에 새 안내선이 생겼다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'promise-wuling-discipline',
|
|
label: '군율을 다시 약속한다',
|
|
response: '유비는 무릉 마을을 함부로 징발하지 않겠다고 말했고, 공지는 그 약속을 고을 사람들에게 전하겠다고 답했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'ma-gong-after-wuling',
|
|
title: '무릉 기록',
|
|
availableAfterBattleIds: [campBattleIds.twentyFifth],
|
|
unitIds: ['ma-liang', 'gong-zhi'],
|
|
bondId: 'ma-liang__gong-zhi',
|
|
rewardExp: 26,
|
|
lines: [
|
|
'마량: 기록에는 길이 하나로 적혀도, 실제 산에는 세 갈래가 숨어 있더군요.',
|
|
'공지: 맞습니다. 비가 오면 마을 사람도 다른 길을 씁니다. 무릉은 지도가 아니라 사람에게 길을 물어야 합니다.',
|
|
'마량: 그렇다면 제 기록은 그 사람들의 이름까지 품어야겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'revise-wuling-map',
|
|
label: '무릉 지도를 고친다',
|
|
response: '마량과 공지는 기존 지도에 숨은 우회로와 위험한 골짜기를 더했고, 다음 행군의 판단 근거가 두꺼워졌다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'record-mountain-clans',
|
|
label: '산길 마을 이름을 적는다',
|
|
response: '두 사람은 마을별 장로와 창고 위치를 기록했고, 무릉의 길은 점차 유비군의 말 안으로 들어왔다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yi-gong-after-wuling',
|
|
title: '예와 귀순',
|
|
availableAfterBattleIds: [campBattleIds.twentyFifth],
|
|
unitIds: ['yi-ji', 'gong-zhi'],
|
|
bondId: 'yi-ji__gong-zhi',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'이적: 귀순은 무릎을 꿇는 일이 아니라, 다음날 마을이 문을 닫지 않게 하는 약속입니다.',
|
|
'공지: 무릉 사람들은 말보다 행동을 봅니다. 약속을 글로 남기고, 창고를 지켜야 믿을 것입니다.',
|
|
'이적: 그렇다면 공지 장군의 길과 제 문장이 함께 가야겠군요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-wuling-pledge',
|
|
label: '무릉 서약문을 쓴다',
|
|
response: '이적은 공지의 말을 서약문으로 다듬었고, 무릉의 귀순은 전투 결과가 아니라 군영의 질서로 굳어졌다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'prepare-changsha-envoy',
|
|
label: '장사 사절 예를 논한다',
|
|
response: '두 사람은 장사로 보낼 예물과 인사 순서를 논했고, 황충을 함부로 대하지 않을 준비가 시작됐다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-huang-after-changsha',
|
|
title: '노장을 예우하다',
|
|
availableAfterBattleIds: [campBattleIds.twentySixth],
|
|
unitIds: ['liu-bei', 'huang-zhong'],
|
|
bondId: 'liu-bei__huang-zhong',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'황충: 늙은 장수의 체면을 지켜 주신 일은 전장의 승리보다 오래 남을 것입니다.',
|
|
'유비: 활은 나이를 묻지 않고 뜻을 묻는다고 들었소. 장군의 활이 백성을 지키는 데 쓰인다면 더 바랄 것이 없소.',
|
|
'황충: 그 뜻이라면 제 활과 남은 세월을 맡기겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'honor-veteran-bow',
|
|
label: '노장의 활을 예우한다',
|
|
response: '유비는 황충의 활과 전공을 병사들 앞에서 높였고, 노장은 조용히 고개를 숙였다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'ask-changsha-people',
|
|
label: '장사 백성을 묻는다',
|
|
response: '황충은 장사 성 안팎의 창고와 마을 사정을 차분히 들려주었고, 유비는 먼저 살필 곳을 정했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-huang-after-changsha',
|
|
title: '무인의 존중',
|
|
availableAfterBattleIds: [campBattleIds.twentySixth],
|
|
unitIds: ['guan-yu', 'huang-zhong'],
|
|
bondId: 'guan-yu__huang-zhong',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'관우: 장군의 활은 성루 위에서도 흔들리지 않았소. 칼을 든 몸으로도 그 기개를 알 수 있었소.',
|
|
'황충: 운장 장군의 칼 역시 병사를 물러서게 하지 않았습니다. 나이는 달라도 무인의 길은 통하는군요.',
|
|
'관우: 다음 전장에서는 서로의 거리와 호흡을 맞춰 봅시다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'compare-front-and-bow',
|
|
label: '전열과 활의 거리를 맞춘다',
|
|
response: '관우와 황충은 전열이 버티는 거리와 활이 닿는 거리를 함께 맞추며 장사 병사들의 시선을 모았다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'share-warrior-courtesy',
|
|
label: '무인의 예를 나눈다',
|
|
response: '두 장수는 말보다 짧은 예로 서로를 인정했고, 군영의 새 긴장감은 단단한 신뢰로 바뀌기 시작했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-wei-after-changsha',
|
|
title: '닫힌 문을 여는 창',
|
|
availableAfterBattleIds: [campBattleIds.twentySixth],
|
|
unitIds: ['liu-bei', 'wei-yan'],
|
|
bondId: 'liu-bei__wei-yan',
|
|
rewardExp: 26,
|
|
lines: [
|
|
'위연: 저는 성문 안의 답답함을 참지 못했습니다. 주공의 군이라면 닫힌 문을 여는 데 제 창을 쓰겠습니다.',
|
|
'유비: 거친 창도 향할 곳이 바르면 백성을 살리는 길이 되오. 다만 급한 마음이 먼저 나서지 않도록 함께 다듬읍시다.',
|
|
'위연: 명령이 분명하다면, 제 창도 분명히 따르겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'set-wei-yan-vanguard-rule',
|
|
label: '돌파 규칙을 정한다',
|
|
response: '유비는 위연에게 돌파 전에 반드시 백성의 길을 살피라 명했고, 위연은 거칠게나마 고개를 끄덕였다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'temper-wei-yan-ambition',
|
|
label: '뜻을 먼저 묻는다',
|
|
response: '위연은 공을 세우고 싶다는 마음을 숨기지 않았지만, 그 공이 누구를 위한 것인지 다시 생각하게 되었다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'huang-wei-after-changsha',
|
|
title: '장사의 전환',
|
|
availableAfterBattleIds: [campBattleIds.twentySixth],
|
|
unitIds: ['huang-zhong', 'wei-yan'],
|
|
bondId: 'huang-zhong__wei-yan',
|
|
rewardExp: 24,
|
|
lines: [
|
|
'황충: 성문이 열릴 때의 결단은 빠르더군. 다만 빠른 결단은 때로 먼저 상처를 남긴다.',
|
|
'위연: 장군처럼 오래 참는 법은 아직 모릅니다. 하지만 닫힌 성문을 그대로 두는 것도 참기 어렵습니다.',
|
|
'황충: 그러니 다음에는 내가 거리를 보고, 그대가 길을 열면 되겠군.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pair-bow-and-breakthrough',
|
|
label: '활과 돌파를 맞춘다',
|
|
response: '황충은 위연이 뛰어들 거리의 위험을 짚었고, 위연은 노장의 활이 닿는 길을 익혔다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'settle-changsha-memory',
|
|
label: '장사의 기억을 정리한다',
|
|
response: '두 사람은 한현 아래에서 서로 달랐던 선택을 말했고, 이제 같은 군에서 다른 역할을 맡기로 했다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-pang-after-yizhou',
|
|
title: '봉추가 보는 인의',
|
|
availableAfterBattleIds: [campBattleIds.twentySeventh],
|
|
unitIds: ['liu-bei', 'pang-tong'],
|
|
bondId: 'liu-bei__pang-tong',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'방통: 원군로를 열면서도 마을 창고를 지킨 일은 예상보다 인상적이었습니다.',
|
|
'유비: 큰 뜻이 백성을 짓밟고 지나가면, 그 뜻은 이미 비어 있는 것이오.',
|
|
'방통: 그렇다면 그 빈틈 없는 뜻에 제가 조금 날카로운 길을 보태 보겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'ask-pang-tong-yizhou-map',
|
|
label: '익주 지세를 묻는다',
|
|
response: '방통은 산문과 관문의 이름을 빠르게 짚으며, 유비군이 서두를 곳과 멈출 곳을 나누었다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'trust-pang-tong-boldness',
|
|
label: '과감한 계책을 맡긴다',
|
|
response: '유비가 방통의 과감함을 인정하자, 봉추의 눈빛은 한층 밝아졌다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-pang-after-yizhou',
|
|
title: '와룡과 봉추',
|
|
availableAfterBattleIds: [campBattleIds.twentySeventh],
|
|
unitIds: ['zhuge-liang', 'pang-tong'],
|
|
bondId: 'zhuge-liang__pang-tong',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'제갈량: 익주는 산이 많아 큰 판만으로는 늦을 때가 있습니다.',
|
|
'방통: 그래서 때로는 바른 길보다 빠른 틈을 먼저 보아야 하지요.',
|
|
'제갈량: 큰 판과 빠른 틈이 함께 있으면, 군은 길을 잃지 않을 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'divide-dragon-phoenix-plans',
|
|
label: '큰 판과 기책을 나눈다',
|
|
response: '제갈량은 전체 진군로를 맡고, 방통은 관문과 협곡을 흔드는 기책을 맡기로 했다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'compare-yizhou-people',
|
|
label: '익주 사람을 논한다',
|
|
response: '두 책사는 지도보다 먼저 사람의 마음을 살피기로 했고, 익주 공략의 색이 조금 더 깊어졌다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'huang-pang-after-yizhou',
|
|
title: '노궁과 기책',
|
|
availableAfterBattleIds: [campBattleIds.twentySeventh],
|
|
unitIds: ['huang-zhong', 'pang-tong'],
|
|
bondId: 'huang-zhong__pang-tong',
|
|
rewardExp: 26,
|
|
lines: [
|
|
'방통: 노장군의 활은 멀리서도 전열의 숨을 끊을 수 있더군요.',
|
|
'황충: 책사의 계책은 화살보다 먼저 날아간다 들었습니다. 어디를 겨눌지 알려 주시오.',
|
|
'방통: 다음 관문에서는 활이 닿는 곳부터 적의 마음을 흔들어 보겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-bow-kill-zone',
|
|
label: '활의 사거리를 표시한다',
|
|
response: '방통은 산길의 사각을 표시했고, 황충은 그 표시를 따라 활이 닿을 거리를 익혔다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'prepare-feigned-retreat',
|
|
label: '유인책을 준비한다',
|
|
response: '황충은 물러서는 척하며 적을 끌어낼 거리와, 방통의 매복 신호를 함께 맞췄다.',
|
|
rewardExp: 9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-fa-after-fupass',
|
|
title: '익주 내부의 길',
|
|
availableAfterBattleIds: [campBattleIds.twentyEighth],
|
|
unitIds: ['liu-bei', 'fa-zheng'],
|
|
bondId: 'liu-bei__fa-zheng',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'법정: 익주 사람들은 강한 군보다 약속을 지키는 군을 더 오래 기억합니다.',
|
|
'유비: 그래서 관문 창고를 봉했소. 길을 열어도 백성의 밥그릇을 건드리면 마음은 닫히오.',
|
|
'법정: 그렇다면 저는 그 마음이 어디서 열리고 닫히는지 알려 드리겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'ask-fa-zheng-people',
|
|
label: '익주 민심을 묻는다',
|
|
response: '법정은 관문보다 먼저 설득해야 할 마을과 사람의 이름을 적었다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'promise-yizhou-discipline',
|
|
label: '군율을 약속한다',
|
|
response: '유비가 군율을 다시 못박자, 법정은 안쪽 길을 밝히겠다고 답했다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'pang-fa-after-fupass',
|
|
title: '봉추와 내부책',
|
|
availableAfterBattleIds: [campBattleIds.twentyEighth],
|
|
unitIds: ['pang-tong', 'fa-zheng'],
|
|
bondId: 'pang-tong__fa-zheng',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'방통: 안쪽 사정을 아는 사람이 있으니, 이제 문을 두드릴지 흔들지 결정하기 쉬워졌군요.',
|
|
'법정: 흔들어야 할 문도 있고, 조용히 열어야 할 문도 있습니다. 순서를 틀리면 익주가 피로 답할 것입니다.',
|
|
'방통: 그러면 그 순서를 당신이, 그 틈을 제가 보겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-hidden-gates',
|
|
label: '숨은 문을 표시한다',
|
|
response: '법정은 익주 내부의 숨은 길목을 표시했고, 방통은 그 길목을 흔드는 계책을 덧붙였다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'set-bold-and-soft-plan',
|
|
label: '강온책을 나눈다',
|
|
response: '두 사람은 강하게 압박할 곳과 조용히 설득할 곳을 분리했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-fa-after-fupass',
|
|
title: '큰 판과 속내',
|
|
availableAfterBattleIds: [campBattleIds.twentyEighth],
|
|
unitIds: ['zhuge-liang', 'fa-zheng'],
|
|
bondId: 'zhuge-liang__fa-zheng',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'제갈량: 익주는 지세가 험하니 길을 얻어도 마음을 잃으면 오래 지키기 어렵습니다.',
|
|
'법정: 그래서 저는 안쪽 사람들의 두려움과 불만을 함께 보려 합니다.',
|
|
'제갈량: 큰 판에 그 속내를 더하면, 피를 줄이는 길도 보일 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'combine-map-and-sentiment',
|
|
label: '지도와 민심을 합친다',
|
|
response: '제갈량의 지도 위에 법정의 사람 이름이 더해지자, 익주 공략의 다음 순서가 또렷해졌다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'prepare-luo-castle-letter',
|
|
label: '낙성 서찰을 준비한다',
|
|
response: '두 책사는 다음 성으로 보낼 글과 병력 배치의 순서를 함께 정했다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-wu-after-luo',
|
|
title: '낙성의 새 길',
|
|
availableAfterBattleIds: [campBattleIds.twentyNinth],
|
|
unitIds: ['liu-bei', 'wu-yi'],
|
|
bondId: 'liu-bei__wu-yi',
|
|
rewardExp: 30,
|
|
lines: [
|
|
'오의: 성문을 지킨다는 명분으로 백성을 몰아넣는 일은 장수의 길이 아니었습니다.',
|
|
'유비: 성문은 닫혀 있어도 사람의 마음은 열 수 있소. 그대가 길을 보여준다면 피를 덜 흘릴 수 있겠지.',
|
|
'오의: 그렇다면 낙성의 길목과 성 안의 동요를 제가 살피겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'promise-luo-discipline',
|
|
label: '낙성 군율을 약속한다',
|
|
response: '유비가 약탈 금지를 다시 못박자 오의는 익주 장수들을 설득할 명분이 생겼다고 고개를 숙였다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'ask-wu-yi-gate',
|
|
label: '성문 방비를 묻는다',
|
|
response: '오의는 낙성 외곽의 문루와 보급로를 짚으며 다음 출전에서 자신이 맡을 역할을 분명히 했다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fa-wu-after-luo',
|
|
title: '익주 내부 설득',
|
|
availableAfterBattleIds: [campBattleIds.twentyNinth],
|
|
unitIds: ['fa-zheng', 'wu-yi'],
|
|
bondId: 'fa-zheng__wu-yi',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'법정: 익주의 장수들은 강한 군대보다 오래 지켜질 약속을 보고 움직입니다.',
|
|
'오의: 저도 그 약속을 확인했으니, 이제 흔들리는 장수들에게 말할 수 있습니다.',
|
|
'법정: 다음 전장에서는 누가 백성을 버렸고 누가 지켰는지부터 말합시다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-yizhou-names',
|
|
label: '설득할 장수를 적는다',
|
|
response: '두 사람은 낙성 주변 장수들의 이름을 정리했고, 말이 통할 사람과 경계할 사람을 나누었다.',
|
|
rewardExp: 10
|
|
},
|
|
{
|
|
id: 'prepare-luo-message',
|
|
label: '성 안에 보낼 말을 고른다',
|
|
response: '법정은 문장을 다듬고 오의는 받을 사람을 정했다. 낙성의 문은 칼보다 먼저 말에 흔들리기 시작했다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'pang-wu-after-luo',
|
|
title: '봉추와 성문 장수',
|
|
availableAfterBattleIds: [campBattleIds.twentyNinth],
|
|
unitIds: ['pang-tong', 'wu-yi'],
|
|
bondId: 'pang-tong__wu-yi',
|
|
rewardExp: 28,
|
|
lines: [
|
|
'방통: 낙성의 길은 곧아 보일수록 위험합니다. 사람을 믿되 길은 의심해야 하지요.',
|
|
'오의: 장임은 좁은 길에서 기다리는 법을 압니다. 성 밖보다 성 안쪽 길이 더 날카롭습니다.',
|
|
'방통: 그렇다면 다음 전장은 빠른 칼보다 빠른 눈이 먼저 필요하겠군요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-luofeng-risk',
|
|
label: '낙봉파 위험을 표시한다',
|
|
response: '방통은 오의의 설명을 지도에 겹쳐 놓고, 지나치게 좋은 길목에 붉은 표시를 남겼다.',
|
|
rewardExp: 11
|
|
},
|
|
{
|
|
id: 'prepare-decoy-column',
|
|
label: '유인 부대를 준비한다',
|
|
response: '오의는 성문 병력의 시선을 끌 수 있는 깃발과 행군 순서를 알려 주었고, 방통은 미소를 지었다.',
|
|
rewardExp: 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-pang-after-luofeng',
|
|
title: '살아 돌아온 봉추',
|
|
availableAfterBattleIds: [campBattleIds.thirtieth],
|
|
unitIds: ['liu-bei', 'pang-tong'],
|
|
bondId: 'liu-bei__pang-tong',
|
|
rewardExp: 34,
|
|
lines: [
|
|
'유비: 낙봉파의 화살이 그대에게 닿을 뻔했소. 오늘 그대를 잃었다면 익주 길은 물론 내 마음도 크게 꺾였을 것이오.',
|
|
'방통: 주공께서 길을 의심해 주셨기에 제가 길 위에서 목숨을 붙였습니다. 이제는 빠른 계책보다 오래 버틸 계책을 먼저 보겠습니다.',
|
|
'유비: 그대의 말이 오늘 군영의 경계가 되었소. 다음 낙성 본성 앞에서도 그 눈을 믿겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'thank-pang-tong-survival-luofeng',
|
|
label: '방통의 생환을 기뻐한다',
|
|
response: '유비가 직접 술잔을 낮추자 방통도 장난스러운 미소를 거두고 다음 전장의 방비도를 다시 펼쳤습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'ask-pang-tong-next-risk-luofeng',
|
|
label: '다음 위험을 묻는다',
|
|
response: '방통은 낙성 본성의 성문, 물길, 민심을 한 줄로 엮어 보이며 무리한 돌파를 피해야 한다고 답했습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'pang-wu-after-luofeng',
|
|
title: '매복을 읽은 두 사람',
|
|
availableAfterBattleIds: [campBattleIds.thirtieth],
|
|
unitIds: ['pang-tong', 'wu-yi'],
|
|
bondId: 'pang-tong__wu-yi',
|
|
rewardExp: 32,
|
|
lines: [
|
|
'오의: 장임이 숨어들 만한 길을 말씀드렸을 뿐인데, 선생께서는 적의 숨소리까지 짚어 내셨습니다.',
|
|
'방통: 길을 아는 사람과 마음을 읽는 사람이 같이 보면, 매복도 길 표지처럼 보이는 법이지요.',
|
|
'오의: 다음 성문 앞에서도 제가 아는 익주의 길을 숨기지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-luofeng-slope-map',
|
|
label: '낙봉파 지도를 보완한다',
|
|
response: '오의가 놓친 샛길을 더하고 방통이 그 길마다 적의 시야를 적어 넣자, 낙성 진입로가 더 분명해졌습니다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'honor-wu-yi-guide',
|
|
label: '오의의 안내를 치하한다',
|
|
response: '방통이 오의의 공을 군영 앞에서 분명히 말하자, 새로 합류한 장수의 어깨에서 긴장이 조금 풀렸습니다.',
|
|
rewardExp: 11
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-fa-after-luofeng',
|
|
title: '낙성 본성의 문',
|
|
availableAfterBattleIds: [campBattleIds.thirtieth],
|
|
unitIds: ['zhuge-liang', 'fa-zheng'],
|
|
bondId: 'zhuge-liang__fa-zheng',
|
|
rewardExp: 32,
|
|
lines: [
|
|
'제갈량: 낙봉파의 매복을 깨뜨렸으니 장임은 성문 뒤에서 더 단단히 버틸 것입니다.',
|
|
'법정: 성문은 군사만으로 열리지 않습니다. 익주의 장수들이 유비공을 두려워하지 않게 만드는 말도 함께 준비해야 합니다.',
|
|
'제갈량: 그렇다면 다음 싸움은 성벽을 치는 전투이자 마음을 여는 전투가 되겠군요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-luo-castle-parley',
|
|
label: '항복 권고문을 준비한다',
|
|
response: '두 책사가 문장을 다듬자 낙성 안쪽으로 보낼 글에는 위협보다 약속이 먼저 놓였습니다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'mark-main-gate-weakness',
|
|
label: '성문 약점을 표시한다',
|
|
response: '법정의 내부 정보와 제갈량의 배치도가 겹쳐지며, 다음 전장의 첫 돌파 지점이 좁혀졌습니다.',
|
|
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
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-li-after-mianzhu',
|
|
title: '성도 앞의 군율',
|
|
availableAfterBattleIds: [campBattleIds.thirtySecond],
|
|
unitIds: ['liu-bei', 'li-yan'],
|
|
bondId: 'liu-bei__li-yan',
|
|
rewardExp: 36,
|
|
lines: [
|
|
'이엄: 면죽관을 내주었으나, 성도 앞의 군율까지 무너지면 익주는 오래 앙금을 품을 것입니다.',
|
|
'유비: 항복한 장수의 말이라도 백성을 살리는 말이면 곧 군령이오. 그대가 익주 병사의 마음을 잡아 주시오.',
|
|
'이엄: 그렇다면 창고와 관문을 맡아, 항복한 병사도 약탈을 두려워하지 않게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-li-yan-discipline',
|
|
label: '군율 정비를 맡긴다',
|
|
response: '이엄은 면죽관 창고 명부를 다시 정리하고, 항복 병사에게도 같은 배급 규칙을 적용했습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'ask-li-yan-chengdu-order',
|
|
label: '성도 질서를 묻는다',
|
|
response: '이엄은 성도 안쪽 창고, 문지기, 장수들의 이해관계를 짚어 다음 권고문의 근거를 보탰습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-li-after-mianzhu',
|
|
title: '내정과 책략',
|
|
availableAfterBattleIds: [campBattleIds.thirtySecond],
|
|
unitIds: ['zhuge-liang', 'li-yan'],
|
|
bondId: 'zhuge-liang__li-yan',
|
|
rewardExp: 34,
|
|
lines: [
|
|
'제갈량: 성을 얻는 일보다 얻은 뒤의 질서가 더 어렵습니다. 이엄 장군은 그 무게를 알고 있군요.',
|
|
'이엄: 저는 관문을 막는 법은 알지만, 사람 마음을 여는 법은 아직 배워야 합니다.',
|
|
'제갈량: 그렇다면 관문의 숫자와 사람의 사정을 함께 보지요. 다음 싸움은 이미 싸움 뒤의 정치와 맞닿아 있습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'map-chengdu-storehouses',
|
|
label: '성도 창고도를 맞춘다',
|
|
response: '제갈량의 큰 지도 위에 이엄의 창고 명부가 겹치며, 성도 항복 뒤 혼란을 줄일 길이 보였습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'write-surrender-terms',
|
|
label: '항복 조건을 다듬는다',
|
|
response: '두 사람은 유장에게 보낼 문장에 처벌보다 안정을 먼저 적었습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yan-li-after-mianzhu',
|
|
title: '익주 장수의 결단',
|
|
availableAfterBattleIds: [campBattleIds.thirtySecond],
|
|
unitIds: ['yan-yan', 'li-yan'],
|
|
bondId: 'yan-yan__li-yan',
|
|
rewardExp: 34,
|
|
lines: [
|
|
'엄안: 나도 성문 위에서 끝까지 버티려 했소. 그러나 백성이 살아야 장수의 절개도 남는 법이오.',
|
|
'이엄: 노장께서 먼저 길을 여셨기에 저도 검을 내려놓을 수 있었습니다.',
|
|
'엄안: 이제는 우리가 익주 장수들에게 말해야 하오. 항복이 부끄러움이 아니라 백성을 살리는 선택이 될 수 있다고.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-yizhou-officer-names',
|
|
label: '익주 장수 명단을 맞춘다',
|
|
response: '엄안과 이엄은 끝까지 버틸 장수와 설득 가능한 장수를 나누어, 다음 전장의 말길을 열었습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'honor-yizhou-soldiers',
|
|
label: '항복 병사를 예우한다',
|
|
response: '두 장수가 함께 병사들을 달래자, 익주 병사들은 유비군 군영을 조금 덜 두려워하게 되었습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-huang-after-chengdu',
|
|
title: '항복과 보전',
|
|
availableAfterBattleIds: [campBattleIds.thirtyThird],
|
|
unitIds: ['liu-bei', 'huang-quan'],
|
|
bondId: 'liu-bei__huang-quan',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'황권: 패배한 장수에게 가장 두려운 것은 처벌보다, 백성이 새 질서 아래에서 버려지는 일입니다.',
|
|
'유비: 그 두려움을 알기에 그대를 맞이하오. 익주의 사람을 익주의 길로 안정시키는 일에 힘을 빌려 주시오.',
|
|
'황권: 그렇다면 저는 성도 안의 창고와 관직 명단을 정리해, 항복이 혼란으로 번지지 않게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-chengdu-order',
|
|
label: '성도 질서를 맡긴다',
|
|
response: '황권은 항복한 관료와 병사들의 이름을 정리하며, 유비군의 명령이 보복이 아니라 질서임을 보였습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'promise-yizhou-protection',
|
|
label: '익주 보전을 약속한다',
|
|
response: '유비의 약속이 군영 안에 전해지자 황권은 처음으로 긴 숨을 내쉬고 고개를 숙였습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-huang-after-chengdu',
|
|
title: '성도 수습책',
|
|
availableAfterBattleIds: [campBattleIds.thirtyThird],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan',
|
|
rewardExp: 36,
|
|
lines: [
|
|
'제갈량: 성도는 얻었으나 아직 다스린 것은 아닙니다. 관문보다 장부가 더 오래 남는 법이지요.',
|
|
'황권: 익주의 장부에는 사람의 두려움도 함께 적혀 있습니다. 급히 고치면 반발만 커질 것입니다.',
|
|
'제갈량: 그래서 그대의 신중함이 필요합니다. 새 나라는 칼보다 제도 위에 오래 서야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'sort-yizhou-ledgers',
|
|
label: '익주 장부를 정리한다',
|
|
response: '제갈량과 황권은 군량, 관직, 백성 명부를 나누어 보며 다음 장의 내정 과제를 분명히 했습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'draft-shu-foundation-policy',
|
|
label: '건국 준비안을 쓴다',
|
|
response: '두 사람은 아직 나라 이름을 말하지 않았지만, 이미 나라가 버틸 기둥을 하나씩 적기 시작했습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fa-huang-after-chengdu',
|
|
title: '익주 책사들의 균형',
|
|
availableAfterBattleIds: [campBattleIds.thirtyThird],
|
|
unitIds: ['fa-zheng', 'huang-quan'],
|
|
bondId: 'fa-zheng__huang-quan',
|
|
rewardExp: 35,
|
|
lines: [
|
|
'법정: 성도가 열렸으니 이제 빠르게 마음을 잡아야 합니다. 머뭇거리면 오래된 의심이 다시 살아납니다.',
|
|
'황권: 빠름이 항상 안정은 아닙니다. 익주 사람은 강제로 바뀌는 것을 오래 기억합니다.',
|
|
'법정: 좋습니다. 저는 흔들 곳을 짚고, 그대는 무너지지 않게 받치십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'divide-policy-tempo',
|
|
label: '정책 속도를 나눈다',
|
|
response: '법정은 급히 처리할 군사 문제를, 황권은 천천히 설득할 내정 문제를 맡아 균형을 잡았습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'appease-yizhou-officers',
|
|
label: '익주 장수를 달랜다',
|
|
response: '두 책사는 항복 장수들의 자리를 함부로 흔들지 않는 대신, 군율 위반은 단호히 다루기로 했습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-ma-after-jiameng',
|
|
title: '떠도는 창과 새 깃발',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFourth],
|
|
unitIds: ['liu-bei', 'ma-chao'],
|
|
bondId: 'liu-bei__ma-chao',
|
|
rewardExp: 40,
|
|
lines: [
|
|
'마초: 저는 조조를 미워했고, 장로에게 기대었으나 마음 둘 곳을 찾지 못했습니다.',
|
|
'유비: 떠돌던 창도 지킬 백성과 뜻을 만나면 깃발 아래 설 수 있소. 그대의 용맹을 사람을 살리는 데 쓰시오.',
|
|
'마초: 그렇다면 서량의 창은 더는 원한만 좇지 않겠습니다. 유비공의 깃발 아래에서 북쪽 길을 열겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'accept-ma-chao-oath',
|
|
label: '마초의 맹세를 받아들인다',
|
|
response: '유비가 직접 마초의 창을 들어 올리자, 서량 기병들은 새 깃발 앞에서 고개를 숙였습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'ask-ma-chao-hanzhong-road',
|
|
label: '한중 길을 묻는다',
|
|
response: '마초는 장로군의 관문, 기병이 달릴 수 있는 능선, 조조군의 감시 지점을 지도에 표시했습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhao-ma-after-jiameng',
|
|
title: '백마와 금마초',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFourth],
|
|
unitIds: ['zhao-yun', 'ma-chao'],
|
|
bondId: 'zhao-yun__ma-chao',
|
|
rewardExp: 36,
|
|
lines: [
|
|
'조운: 서량 기병의 돌격은 빠르나, 빠른 창일수록 돌아올 길도 보아야 합니다.',
|
|
'마초: 백마 장군의 말은 조용하지만 빈틈이 없군. 내 창은 때로 너무 멀리 나갔습니다.',
|
|
'조운: 다음 전장에서는 서로의 말발굽이 너무 멀어지지 않게 합시다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'practice-cavalry-turn',
|
|
label: '기병 회전을 연습한다',
|
|
response: '조운의 절제된 말놀림과 마초의 폭발적인 돌격이 이어지며, 기병대의 움직임이 한층 깊어졌습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'share-scouting-pace',
|
|
label: '정찰 속도를 맞춘다',
|
|
response: '두 장수는 빠른 돌파와 안전한 귀환의 간격을 맞추며 한중 전선의 정찰 기준을 세웠습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhang-ma-after-jiameng',
|
|
title: '호통과 돌격',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFourth],
|
|
unitIds: ['zhang-fei', 'ma-chao'],
|
|
bondId: 'zhang-fei__ma-chao',
|
|
rewardExp: 36,
|
|
lines: [
|
|
'장비: 하하! 네놈 창끝이 제법 매섭더군. 그래도 내 앞에서는 한 번 멈췄지 않느냐!',
|
|
'마초: 장비 장군의 호통도 말발굽을 흔들 만큼 거칠었습니다. 다음에는 같은 방향으로 흔들어 보지요.',
|
|
'장비: 좋다! 누가 먼저 적진을 갈라 놓는지 겨뤄 보자.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'promise-front-duel',
|
|
label: '전열 돌파를 겨룬다',
|
|
response: '장비와 마초가 웃으며 창을 맞대자, 병사들은 다음 전열 돌파가 조금 두려우면서도 기대된다고 수군거렸습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'split-shock-roles',
|
|
label: '돌격 역할을 나눈다',
|
|
response: '장비는 좁은 길을 부수고, 마초는 열린 측면을 파고드는 방식으로 서로의 돌격을 나누었습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'ma-chao-dai-after-yangping',
|
|
title: '서량의 두 창',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFifth],
|
|
unitIds: ['ma-chao', 'ma-dai'],
|
|
bondId: 'ma-chao__ma-dai',
|
|
rewardExp: 40,
|
|
lines: [
|
|
'마대: 형님, 흩어진 서량 기병을 데려왔습니다. 이제는 원한이 아니라 새 깃발을 보고 달리게 하겠습니다.',
|
|
'마초: 잘 왔다. 내가 너무 앞서 달리면 네가 뒤를 거두어라. 서량의 말발굽이 이번에는 흩어지지 않아야 한다.',
|
|
'마대: 명심하겠습니다. 형님이 길을 열면 저는 그 길이 무너지지 않게 지키겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-western-cavalry',
|
|
label: '서량 기병을 묶는다',
|
|
response: '마초와 마대가 함께 기병 명단을 정리하자, 흩어진 서량 병사들이 하나의 대오로 서기 시작했습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'divide-cavalry-charge',
|
|
label: '돌격과 회수를 나눈다',
|
|
response: '마초는 전열 돌파를, 마대는 후속 회수를 맡기로 하며 서량 기병의 움직임이 한층 안정되었습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-ma-dai-after-yangping',
|
|
title: '새 깃발의 기병',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFifth],
|
|
unitIds: ['liu-bei', 'ma-dai'],
|
|
bondId: 'liu-bei__ma-dai',
|
|
rewardExp: 34,
|
|
lines: [
|
|
'유비: 그대는 마초를 따라왔으나, 이제는 백성과 병사를 지키는 길도 함께 보아야 하오.',
|
|
'마대: 서량의 병사는 빠르게 달리는 법은 알지만, 오래 머무는 법은 서툽니다. 유비공의 군율을 배우겠습니다.',
|
|
'유비: 빠른 말도 돌아올 곳이 있어야 힘을 쓰는 법이오. 우리 깃발 아래에서 그 자리를 찾으시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'accept-ma-dai-oath',
|
|
label: '마대의 맹세를 받는다',
|
|
response: '마대가 창을 세워 예를 올리자, 유비군의 장수들은 새 기병장의 이름을 출전 명단에 적었습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'teach-camp-discipline',
|
|
label: '군영 군율을 일러준다',
|
|
response: '마대는 서량 기병에게 약탈 금지와 보급 질서를 먼저 전하며, 새 군영에 맞춰 움직이기 시작했습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhao-ma-dai-after-yangping',
|
|
title: '침착한 말발굽',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFifth],
|
|
unitIds: ['zhao-yun', 'ma-dai'],
|
|
bondId: 'zhao-yun__ma-dai',
|
|
rewardExp: 32,
|
|
lines: [
|
|
'조운: 마초 장군의 창이 앞을 열면, 그 뒤의 길은 누군가 차분히 지켜야 합니다.',
|
|
'마대: 제가 그 일을 맡겠습니다. 형님의 기세를 살리되, 병사가 흩어지지 않도록 하겠습니다.',
|
|
'조운: 좋습니다. 빠른 말발굽에도 조용한 박자가 있어야 오래 싸울 수 있습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'practice-follow-up-line',
|
|
label: '후속 대열을 연습한다',
|
|
response: '조운과 마대는 돌격 뒤 흩어진 병사를 다시 모으는 훈련을 반복하며 기병대의 안정감을 높였습니다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'mark-safe-return',
|
|
label: '귀환로를 표시한다',
|
|
response: '두 장수는 양평관 지도에 돌파로와 귀환로를 함께 표시해 다음 한중 전투의 위험을 줄였습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'huang-wang-after-dingjun',
|
|
title: '노장과 산길',
|
|
availableAfterBattleIds: [campBattleIds.thirtySixth],
|
|
unitIds: ['huang-zhong', 'wang-ping'],
|
|
bondId: 'huang-zhong__wang-ping',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'왕평: 정군산의 길은 곧게 오르는 길보다 돌아가는 길이 더 빠를 때가 많습니다.',
|
|
'황충: 흥, 늙은 다리는 돌아가는 길을 싫어하지만 전장은 고집으로 이기는 곳이 아니지.',
|
|
'왕평: 장군의 칼이 닿을 순간을 만들 수 있다면, 산길은 제게 맡겨 주십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trust-mountain-route',
|
|
label: '산길을 맡긴다',
|
|
response: '황충이 왕평의 지형 판단을 받아들이자, 노장의 돌파는 무모함이 아니라 계산된 일격으로 바뀌었습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'prepare-ridge-ambush',
|
|
label: '능선 매복을 준비한다',
|
|
response: '왕평이 숲길과 절벽 사이의 매복 지점을 표시했고, 황충은 그곳에서 칼을 뽑을 순간을 기다리기로 했습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-wang-after-dingjun',
|
|
title: '한중 지형책',
|
|
availableAfterBattleIds: [campBattleIds.thirtySixth],
|
|
unitIds: ['zhuge-liang', 'wang-ping'],
|
|
bondId: 'zhuge-liang__wang-ping',
|
|
rewardExp: 36,
|
|
lines: [
|
|
'제갈량: 지도에는 산이 선으로 그려지지만, 병사는 그 선 사이에서 지치고 헤맵니다.',
|
|
'왕평: 그래서 지도만 보는 장수는 한중을 오래 품지 못합니다. 물길, 숲길, 겨울 길이 모두 다릅니다.',
|
|
'제갈량: 그대의 눈이 필요합니다. 한중을 얻는 것보다 지키는 일이 더 길 테니까요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-hanzhong-terrain-book',
|
|
label: '한중 지형책을 쓴다',
|
|
response: '제갈량과 왕평은 정군산, 양평관, 가맹관의 길을 하나의 지형책으로 묶기 시작했습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'mark-winter-roads',
|
|
label: '겨울 행군로를 표시한다',
|
|
response: '왕평은 눈이 오면 막히는 길과 오히려 단단해지는 강변 길을 지도에 따로 표시했습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'ma-dai-wang-after-dingjun',
|
|
title: '기병과 산길잡이',
|
|
availableAfterBattleIds: [campBattleIds.thirtySixth],
|
|
unitIds: ['ma-dai', 'wang-ping'],
|
|
bondId: 'ma-dai__wang-ping',
|
|
rewardExp: 34,
|
|
lines: [
|
|
'마대: 말이 달릴 수 없는 길에서도 기병은 쓸모가 있습니까?',
|
|
'왕평: 달리지 못하면 지키면 됩니다. 좁은 능선에서는 빠른 돌격보다 정확한 퇴로가 더 중요합니다.',
|
|
'마대: 그럼 제가 지킬 길을 알려 주십시오. 형님의 돌격이 돌아올 길을 잃지 않게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'map-cavalry-return',
|
|
label: '기병 귀환로를 그린다',
|
|
response: '왕평과 마대가 능선의 귀환로를 정리하자, 서량 기병은 좁은 산길에서도 무리하게 흩어지지 않게 되었습니다.',
|
|
rewardExp: 12
|
|
},
|
|
{
|
|
id: 'train-dismount-guard',
|
|
label: '하마 방어를 훈련한다',
|
|
response: '마대는 말에서 내려 창진을 세우는 훈련을 받아, 산악 전투에서 기병의 빈틈을 줄였습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-wang-after-hanzhong-decisive',
|
|
title: '한중의 새 주인',
|
|
availableAfterBattleIds: [campBattleIds.thirtySeventh],
|
|
unitIds: ['liu-bei', 'wang-ping'],
|
|
bondId: 'liu-bei__wang-ping',
|
|
rewardExp: 40,
|
|
lines: [
|
|
'왕평: 한중의 병사들은 이긴 사람보다 오래 지켜 줄 사람을 기다립니다.',
|
|
'유비: 내가 이 땅을 얻었다면, 먼저 이 땅의 길과 사람을 배워야겠구려.',
|
|
'왕평: 그 말씀을 병사들에게 전하겠습니다. 길을 아는 자들이 먼저 마음을 열 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'promise-hanzhong-stability',
|
|
label: '한중을 안정시키겠다',
|
|
response: '유비는 왕평에게 한중 병사들의 가족과 창고를 먼저 돌보라고 명했고, 왕평은 산길 병사들의 마음을 모으기 시작했습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'listen-local-soldiers',
|
|
label: '현지 병사의 말을 듣는다',
|
|
response: '왕평은 오래된 길잡이와 창고병을 불러 한중을 지키는 실제 고충을 유비에게 전했습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fa-wang-after-hanzhong-decisive',
|
|
title: '산길 계책',
|
|
availableAfterBattleIds: [campBattleIds.thirtySeventh],
|
|
unitIds: ['fa-zheng', 'wang-ping'],
|
|
bondId: 'fa-zheng__wang-ping',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'법정: 오늘의 승리는 적을 밀어낸 것보다 길을 끊은 순서가 좋았습니다.',
|
|
'왕평: 계책이 너무 빠르면 병사가 따라오지 못합니다. 길은 속도보다 끊기지 않는 것이 먼저입니다.',
|
|
'법정: 그 균형을 알았으니, 다음에는 더 멀리 움직일 수 있겠군요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'plan-supply-chain',
|
|
label: '보급로를 다시 짠다',
|
|
response: '법정과 왕평은 전투에서 확인한 창고, 물길, 산길을 하나의 보급망으로 다시 정리했습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'mark-decoy-routes',
|
|
label: '유인로를 표시한다',
|
|
response: '왕평은 실제로 쓸 길과 적에게 보일 길을 구분했고, 법정은 그 차이를 다음 계책의 재료로 삼았습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'huang-fa-after-hanzhong-decisive',
|
|
title: '정군산의 여세',
|
|
availableAfterBattleIds: [campBattleIds.thirtySeventh],
|
|
unitIds: ['huang-zhong', 'fa-zheng'],
|
|
bondId: 'huang-zhong__fa-zheng',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'황충: 오늘은 늙은 몸보다 군사의 눈이 먼저 적을 베었소.',
|
|
'법정: 장군의 칼이 움직일 때를 맞췄을 뿐입니다. 그 순간을 놓치지 않은 것은 장군입니다.',
|
|
'황충: 그렇다면 다음에도 나를 너무 아끼지 마시오. 좋은 때라면 늙은 말도 달릴 줄 아오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'honor-veteran-charge',
|
|
label: '노장의 돌격을 기린다',
|
|
response: '법정은 황충의 돌격로를 병사들에게 설명했고, 노장의 결단은 한중 승리의 상징이 되었습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'temper-next-charge',
|
|
label: '다음 돌격을 아낀다',
|
|
response: '황충은 법정의 만류를 웃으며 받아들였고, 다음 큰 싸움을 위해 병사들의 숨을 고르게 했습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-ma-after-jing-defense',
|
|
title: '형주 문무',
|
|
availableAfterBattleIds: [campBattleIds.thirtyEighth],
|
|
unitIds: ['guan-yu', 'ma-liang'],
|
|
bondId: 'guan-yu__ma-liang',
|
|
rewardExp: 40,
|
|
lines: [
|
|
'마량: 형주는 칼보다 먼저 민심이 흔들리는 곳입니다. 봉화대 하나를 세우는 일도 백성에게는 약속으로 보입니다.',
|
|
'관우: 내 칼이 먼저 서면 백성이 두려워하겠지. 그대의 붓이 길을 가리켜 주면, 칼은 그 뒤를 따르겠소.',
|
|
'마량: 그렇다면 강릉의 문서와 나루의 곡식을 함께 묶겠습니다. 백성이 먼저 안심해야 군도 오래 섭니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-jing-letters',
|
|
label: '형주 문서를 정리한다',
|
|
response: '관우는 마량에게 군령과 민심 고시를 함께 맡겼고, 형주 방위의 말길이 조금 더 단단해졌습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'share-front-duty',
|
|
label: '전선 책임을 나눈다',
|
|
response: '마량은 봉화대와 마을의 순서를 다시 적었고, 관우는 병사들에게 백성을 먼저 안심시키라 명했습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-mi-after-jing-defense',
|
|
title: '강릉 보급',
|
|
availableAfterBattleIds: [campBattleIds.thirtyEighth],
|
|
unitIds: ['guan-yu', 'mi-zhu'],
|
|
bondId: 'guan-yu__mi-zhu',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'미축: 강릉의 창고는 큰데, 강을 오가는 길은 좁습니다. 나루가 흔들리면 병력보다 곡식이 먼저 끊깁니다.',
|
|
'관우: 적이 칼을 들기 전에 군량을 흔드는 셈이군. 그대가 길을 맡아 주면 나는 봉화대를 지키겠소.',
|
|
'미축: 그리하겠습니다. 보급은 조용해야 하지만, 끊겼을 때는 가장 크게 들리는 법입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'secure-river-stores',
|
|
label: '강릉 창고를 나눈다',
|
|
response: '미축은 창고를 작은 단위로 나누어 표식을 붙였고, 관우군의 나루 보급선이 더 질겨졌습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'guard-supply-boats',
|
|
label: '보급선 호위를 세운다',
|
|
response: '관우는 보급선에 소규모 호위를 붙였고, 미축은 강변 상인들의 불안을 누그러뜨렸습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-wang-after-jing-defense',
|
|
title: '강과 산의 방어',
|
|
availableAfterBattleIds: [campBattleIds.thirtyEighth],
|
|
unitIds: ['guan-yu', 'wang-ping'],
|
|
bondId: 'guan-yu__wang-ping',
|
|
rewardExp: 36,
|
|
lines: [
|
|
'왕평: 한중의 산길과 형주의 강길은 다르지만, 길을 잃은 병사가 무너지는 것은 같습니다.',
|
|
'관우: 그대는 길을 보는 눈이 있소. 강변에서도 산길의 차분함을 잃지 말아 주시오.',
|
|
'왕평: 급히 쫓는 병사는 물가에서 더 흔들립니다. 저는 퇴로와 표식을 먼저 세우겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-jing-retreat-routes',
|
|
label: '퇴로 표식을 세운다',
|
|
response: '왕평은 강변과 숲길의 표식을 정리했고, 관우는 기동대가 무리하게 깊이 들어가지 않도록 명했습니다.',
|
|
rewardExp: 13
|
|
},
|
|
{
|
|
id: 'teach-river-discipline',
|
|
label: '강변 진형을 익힌다',
|
|
response: '왕평의 차분한 훈련으로 강변 병사들의 이동 순서가 안정되었고, 관우는 그 실용성을 높이 샀습니다.',
|
|
rewardExp: 12
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-fa-after-fan-castle',
|
|
title: '한수 물길',
|
|
availableAfterBattleIds: [campBattleIds.thirtyNinth],
|
|
unitIds: ['guan-yu', 'fa-zheng'],
|
|
bondId: 'guan-yu__fa-zheng',
|
|
rewardExp: 42,
|
|
lines: [
|
|
'법정: 번성은 성벽만 보는 싸움이 아닙니다. 한수의 흐름이 바뀌면 적의 진형도 바뀝니다.',
|
|
'관우: 물길을 칼처럼 쓰라는 말이군. 그대가 둑을 읽어 주면, 나는 성문을 흔들겠소.',
|
|
'법정: 병사들에게 물과 진흙을 두려워하지 않게 해야 합니다. 다음 싸움은 발밑부터 전장이 될 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'survey-han-river-banks',
|
|
label: '한수 둑을 살핀다',
|
|
response: '법정은 수위와 둑의 약한 곳을 적었고, 관우는 병사들에게 수공 대비 군령을 내렸습니다.',
|
|
rewardExp: 16
|
|
},
|
|
{
|
|
id: 'prepare-water-commands',
|
|
label: '수공 군령을 준비한다',
|
|
response: '관우군은 물길 신호와 퇴로 표식을 다시 익혔고, 법정은 다음 전장의 순서를 더 선명하게 그렸습니다.',
|
|
rewardExp: 15
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-ma-after-fan-castle',
|
|
title: '청룡과 서량',
|
|
availableAfterBattleIds: [campBattleIds.thirtyNinth],
|
|
unitIds: ['guan-yu', 'ma-chao'],
|
|
bondId: 'guan-yu__ma-chao',
|
|
rewardExp: 40,
|
|
lines: [
|
|
'마초: 방덕의 기세가 거칠었습니다. 서량의 돌격을 아는 자답게 물러설 때도 눈빛이 남아 있더군요.',
|
|
'관우: 거친 말은 고삐를 알아야 하오. 그대가 기병을 묶어 주면, 내 칼이 앞길을 열겠소.',
|
|
'마초: 다음에는 제가 먼저 흔들고 장군께서 가르겠습니다. 적이 돌파라 믿는 길을 함정으로 만들겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'drill-counter-charge',
|
|
label: '반돌격을 연습한다',
|
|
response: '마초의 기병대가 짧게 치고 빠지는 법을 익혔고, 관우의 전열은 돌파 직후의 빈틈을 노리게 되었습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'mark-cavalry-lanes',
|
|
label: '기병 통로를 표시한다',
|
|
response: '관우와 마초가 강변의 말길을 나누어 표시하면서 방덕 기병을 끊어낼 준비가 더 단단해졌습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-huang-after-fan-castle',
|
|
title: '성벽을 보는 노장',
|
|
availableAfterBattleIds: [campBattleIds.thirtyNinth],
|
|
unitIds: ['guan-yu', 'huang-zhong'],
|
|
bondId: 'guan-yu__huang-zhong',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'황충: 성벽 위 궁병은 내려오지 않습니다. 그래서 더 차분히 흔들어야 합니다.',
|
|
'관우: 노장의 활은 성벽 위의 숨을 세는 듯하오. 외곽 보루를 잡은 뒤에는 그 눈이 더욱 필요하오.',
|
|
'황충: 활은 멀리 쏘되 마음은 급히 앞서가지 않겠습니다. 다음 싸움에서 성벽의 빈틈을 찾아 보겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'range-fort-archers',
|
|
label: '성벽 사거리를 잰다',
|
|
response: '황충은 번성 외곽의 사거리 표식을 남겼고, 관우군은 궁병의 첫 화살을 피할 길을 익혔습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'cover-river-advance',
|
|
label: '강변 진격을 엄호한다',
|
|
response: '황충의 엄호 계획 덕분에 강변을 따라 전진하는 부대의 부담이 줄었습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-huangquan-after-han-river',
|
|
title: '물길의 뒷정리',
|
|
availableAfterBattleIds: [campBattleIds.fortieth],
|
|
unitIds: ['guan-yu', 'huang-quan'],
|
|
bondId: 'guan-yu__huang-quan',
|
|
rewardExp: 42,
|
|
lines: [
|
|
'황권: 물이 이긴 뒤가 더 어렵습니다. 젖은 병사와 흔들린 백성을 함께 다독여야 합니다.',
|
|
'관우: 적을 무너뜨린 물길이 백성의 삶까지 무너뜨려서는 안 되오. 그대의 신중함이 필요하오.',
|
|
'황권: 둑의 약한 곳과 마을의 낮은 길을 먼저 적겠습니다. 다음 포위전은 뒤가 단단해야 오래 갑니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'inspect-flooded-villages',
|
|
label: '잠긴 마을을 살핀다',
|
|
response: '황권은 낮은 마을의 길을 정리했고, 관우군은 백성을 해치지 않는 수공 군령을 다시 세웠습니다.',
|
|
rewardExp: 16
|
|
},
|
|
{
|
|
id: 'write-bank-repair-orders',
|
|
label: '제방 보수 군령을 쓴다',
|
|
response: '관우와 황권이 제방 보수 순서를 정리하며 번성 포위 뒤의 민심 관리가 조금 더 단단해졌습니다.',
|
|
rewardExp: 15
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-li-after-han-river',
|
|
title: '젖은 보급선',
|
|
availableAfterBattleIds: [campBattleIds.fortieth],
|
|
unitIds: ['guan-yu', 'li-yan'],
|
|
bondId: 'guan-yu__li-yan',
|
|
rewardExp: 40,
|
|
lines: [
|
|
'이엄: 수공은 이겼지만 곡식도 젖었습니다. 병사들이 물을 이기려면 창고가 먼저 말라야 합니다.',
|
|
'관우: 앞에서 칼이 이겨도 뒤의 곡식이 상하면 오래 버티지 못하오. 창고를 맡아 주시오.',
|
|
'이엄: 젖은 포대를 버릴 것과 살릴 것을 나누겠습니다. 다음 포위전에서 병사들의 밥이 끊기지 않게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'dry-flooded-grain',
|
|
label: '젖은 곡식을 말린다',
|
|
response: '이엄이 곡식을 나누어 말리게 하자 병사들의 보급 불안이 줄었습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'split-siege-rations',
|
|
label: '포위전 군량을 나눈다',
|
|
response: '관우군의 군량이 여러 창고에 나뉘어 다음 장기전 준비가 더 안정되었습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-fa-after-han-river',
|
|
title: '수공의 대가',
|
|
availableAfterBattleIds: [campBattleIds.fortieth],
|
|
unitIds: ['guan-yu', 'fa-zheng'],
|
|
bondId: 'guan-yu__fa-zheng',
|
|
rewardExp: 38,
|
|
lines: [
|
|
'법정: 우금의 본대는 무너졌지만, 물이 만든 승리는 오래 설명해야 합니다. 두려움도 함께 남기 때문입니다.',
|
|
'관우: 군령이 칼보다 무거운 날이었소. 다음에는 성문 앞에서 그 무게를 다시 견뎌야 하오.',
|
|
'법정: 방덕과 조인의 반응을 보면 아직 끝난 싸움은 아닙니다. 승리 뒤의 긴장을 놓치지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'explain-water-orders',
|
|
label: '수공 군령을 설명한다',
|
|
response: '법정은 병사들에게 물길을 쓴 이유와 다음 포위전의 순서를 설명했고, 관우군의 동요가 줄었습니다.',
|
|
rewardExp: 14
|
|
},
|
|
{
|
|
id: 'prepare-siege-after-flood',
|
|
label: '포위전 순서를 정한다',
|
|
response: '관우와 법정은 수공 뒤의 진격 순서를 다시 짰고, 번성 성문을 향한 압박이 더 선명해졌습니다.',
|
|
rewardExp: 13
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-huang-after-fan-siege',
|
|
title: '성벽을 겨눈 노장',
|
|
availableAfterBattleIds: [campBattleIds.fortyFirst],
|
|
unitIds: ['guan-yu', 'huang-zhong'],
|
|
bondId: 'guan-yu__huang-zhong',
|
|
rewardExp: 44,
|
|
lines: [
|
|
'황충: 성벽 위 궁병은 오래 버티는 법을 압니다. 다음에는 사다리보다 먼저 그 눈을 흔들어야 합니다.',
|
|
'관우: 노장의 화살이 성문 위를 누르면, 칼을 든 병사들이 한 걸음 더 나아갈 수 있소.',
|
|
'황충: 병사들이 오르기 전에 제가 먼저 길을 열겠습니다. 성벽을 보는 눈은 아직 흐려지지 않았습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-wall-archers',
|
|
label: '성벽 궁병 위치를 표시한다',
|
|
response: '황충은 성벽의 사각을 표시했고, 관우군의 다음 공성 대형이 더 안정되었습니다.',
|
|
rewardExp: 16
|
|
},
|
|
{
|
|
id: 'prepare-siege-cover',
|
|
label: '사다리 엄호를 준비한다',
|
|
response: '관우와 황충은 사다리 부대의 엄호 순서를 정하며 성문 압박을 더 날카롭게 다듬었습니다.',
|
|
rewardExp: 15
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-maliang-after-fan-siege',
|
|
title: '형주의 낮은 목소리',
|
|
availableAfterBattleIds: [campBattleIds.fortyFirst],
|
|
unitIds: ['guan-yu', 'ma-liang'],
|
|
bondId: 'guan-yu__ma-liang',
|
|
rewardExp: 42,
|
|
lines: [
|
|
'마량: 번성의 성벽만 보면 형주의 마음을 놓칠 수 있습니다. 강 건너의 말이 점점 빨라지고 있습니다.',
|
|
'관우: 성을 치는 동안 뒤를 잊는다면 그것은 승리가 아니오. 형주의 소리를 계속 들려주시오.',
|
|
'마량: 나루의 상인과 마을 노인을 다시 만나겠습니다. 전선의 힘은 뒤의 믿음에서 나옵니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'listen-to-jing-villages',
|
|
label: '형주 마을의 소문을 듣는다',
|
|
response: '마량이 마을의 불안한 소문을 모아 관우에게 전했고, 후방 경계가 더 촘촘해졌습니다.',
|
|
rewardExp: 16
|
|
},
|
|
{
|
|
id: 'calm-river-merchants',
|
|
label: '강변 상인을 안심시킨다',
|
|
response: '관우군은 상인들에게 군율을 설명했고, 강변 보급로의 불안이 조금 줄었습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-guan-after-fan-siege',
|
|
title: '왕업과 성문',
|
|
availableAfterBattleIds: [campBattleIds.fortyFirst],
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
bondId: 'liu-bei__guan-yu_fan-siege',
|
|
rewardExp: 46,
|
|
lines: [
|
|
'유비: 운장, 성문은 흔들렸지만 형주의 바람이 편치 않소. 승세가 커질수록 마음도 무거워지는구려.',
|
|
'관우: 형님의 뜻을 높이려면 이 성을 넘어야 합니다. 다만 뒤의 나루까지 제 눈에서 놓지 않겠습니다.',
|
|
'유비: 그대의 칼이 앞을 열고, 우리의 의가 뒤를 지키게 합시다. 다음 선택이 더 중요해질 것이오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-siege-burden',
|
|
label: '공성의 짐을 나눈다',
|
|
response: '유비와 관우는 번성 포위의 책임을 함께 나누며 병사들에게 오만을 경계하라는 군령을 내렸습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'warn-about-rear',
|
|
label: '후방 경계를 당부한다',
|
|
response: '관우는 전선 장수들에게 뒤의 나루를 확인하라 명했고, 유비는 형주 민심을 살피게 했습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-mizhu-after-jing-rear',
|
|
title: '창고를 지키는 의',
|
|
availableAfterBattleIds: [campBattleIds.fortySecond],
|
|
unitIds: ['guan-yu', 'mi-zhu'],
|
|
bondId: 'guan-yu__mi-zhu_rear',
|
|
rewardExp: 46,
|
|
lines: [
|
|
'미축: 강동의 척후가 상선으로 들어오면 창고 문을 잠그는 것만으로는 부족합니다. 장부와 사람 마음이 함께 지켜져야 합니다.',
|
|
'관우: 군량이 흔들리면 성문 앞의 칼도 가벼워지오. 그대가 창고의 말과 숫자를 붙잡아 주시오.',
|
|
'미축: 상인 명부와 군량 표식을 새로 맞추겠습니다. 후방의 믿음이 앞의 진군을 버티게 할 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'reset-warehouse-ledgers',
|
|
label: '창고 장부를 다시 맞춘다',
|
|
response: '미축은 창고 장부와 선박 표식을 맞추었고, 강릉 후방의 보급 불안이 줄었습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'watch-merchant-boats',
|
|
label: '상선 감시를 늘린다',
|
|
response: '관우군은 상선의 출입 순서를 다시 세웠고, 위장 척후가 숨어들 여지가 조금 줄었습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-maliang-after-jing-rear',
|
|
title: '부드러운 말의 칼',
|
|
availableAfterBattleIds: [campBattleIds.fortySecond],
|
|
unitIds: ['guan-yu', 'ma-liang'],
|
|
bondId: 'guan-yu__ma-liang_rear',
|
|
rewardExp: 44,
|
|
lines: [
|
|
'마량: 육손의 글은 예를 갖췄지만 지나치게 낮았습니다. 낮은 말은 때로 칼보다 더 깊이 들어옵니다.',
|
|
'관우: 예가 지나치면 뜻을 숨기는 법이오. 강동의 말과 배를 함께 살피시오.',
|
|
'마량: 강변 마을의 소문부터 다시 모으겠습니다. 후방의 균열은 작은 말에서 시작될 때가 많습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'read-lu-xun-letter',
|
|
label: '육손의 글을 분석한다',
|
|
response: '마량은 문장의 예와 속뜻을 나누어 읽었고, 관우군은 강동의 다음 수를 더 경계하게 되었습니다.',
|
|
rewardExp: 16
|
|
},
|
|
{
|
|
id: 'gather-river-rumors',
|
|
label: '강변 소문을 모은다',
|
|
response: '마량이 강변 마을의 소문을 정리하자, 나루와 창고를 잇는 경계망이 조금 더 촘촘해졌습니다.',
|
|
rewardExp: 15
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-mizhu-after-jing-rear',
|
|
title: '백성과 군량',
|
|
availableAfterBattleIds: [campBattleIds.fortySecond],
|
|
unitIds: ['liu-bei', 'mi-zhu'],
|
|
bondId: 'liu-bei__mi-zhu_rear',
|
|
rewardExp: 42,
|
|
lines: [
|
|
'유비: 후방의 백성이 두려워하면 창고도 성문도 우리 편이 되지 못하오.',
|
|
'미축: 강동의 배가 물건을 싣고 와도 마음은 흔들 수 있습니다. 백성에게 먼저 설명하겠습니다.',
|
|
'유비: 군량을 지키되 백성의 삶을 누르지 마시오. 형주의 믿음을 잃으면 승리도 오래가지 못하오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-grain-with-villages',
|
|
label: '마을에 곡식을 나눈다',
|
|
response: '유비와 미축은 작은 곡식 창고를 열어 강변 마을의 불안을 달랬습니다.',
|
|
rewardExp: 15
|
|
},
|
|
{
|
|
id: 'explain-rear-orders',
|
|
label: '후방 군령을 설명한다',
|
|
response: '미축은 군령의 이유를 상인과 백성에게 설명했고, 형주 후방의 불신이 조금 누그러졌습니다.',
|
|
rewardExp: 14
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-maliang-after-gongan-collapse',
|
|
title: '성문 안의 균열',
|
|
availableAfterBattleIds: [campBattleIds.fortyThird],
|
|
unitIds: ['guan-yu', 'ma-liang'],
|
|
bondId: 'guan-yu__ma-liang_collapse',
|
|
rewardExp: 48,
|
|
lines: [
|
|
'마량: 공안 성문은 닫혔지만, 병사들의 눈은 아직 흔들립니다. 강동의 말이 칼보다 먼저 들어왔습니다.',
|
|
'관우: 군령이 엄하면 마음이 붙는 줄 알았으나, 두려움이 틈이 될 수도 있구려.',
|
|
'마량: 엄함을 거두자는 뜻은 아닙니다. 다만 이유를 설명하지 않은 군령은 적의 말에 밀릴 수 있습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'explain-gate-orders',
|
|
label: '성문 군령의 이유를 밝힌다',
|
|
response: '관우는 공안 병사들에게 군령의 이유를 설명했고, 마량은 성문 안의 불신을 줄일 말을 정리했습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'watch-soft-letters',
|
|
label: '강동의 글을 더 경계한다',
|
|
response: '마량은 육손의 낮은 글을 다시 분석했고, 관우군은 부드러운 문서가 남기는 흔적을 더 촘촘히 살피게 되었습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'mizhu-huangquan-after-gongan-collapse',
|
|
title: '흔들린 장부',
|
|
availableAfterBattleIds: [campBattleIds.fortyThird],
|
|
unitIds: ['mi-zhu', 'huang-quan'],
|
|
bondId: 'mi-zhu__huang-quan_collapse',
|
|
rewardExp: 46,
|
|
lines: [
|
|
'미축: 장부의 빈칸보다 두려운 것은, 빈칸을 숨기려는 사람의 침묵입니다.',
|
|
'황권: 미방을 몰아붙이면 더 깊이 숨을 것입니다. 숫자와 증언을 천천히 맞춰 도망칠 길을 줄여야 합니다.',
|
|
'미축: 창고의 문을 잠그는 것보다 사람의 마음이 새지 않게 하는 일이 더 어렵군요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'compare-hidden-ledgers',
|
|
label: '숨긴 장부를 대조한다',
|
|
response: '미축과 황권은 숨은 장부와 선박 명부를 맞추며, 후방 배신으로 이어질 작은 구멍을 찾아냈습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'avoid-public-blame',
|
|
label: '공개 문책을 늦춘다',
|
|
response: '황권은 성급한 문책을 막았고, 미축은 창고 관리들이 증언할 시간을 벌었습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhaoyun-wangping-after-gongan-collapse',
|
|
title: '나루를 끊는 눈',
|
|
availableAfterBattleIds: [campBattleIds.fortyThird],
|
|
unitIds: ['zhao-yun', 'wang-ping'],
|
|
bondId: 'zhao-yun__wang-ping_collapse',
|
|
rewardExp: 44,
|
|
lines: [
|
|
'왕평: 강동 수군은 큰길보다 갈대밭 뒤의 좁은 물목을 택했습니다. 다음에도 그럴 것입니다.',
|
|
'조운: 내가 빠르게 달려도 길을 모르면 늦소. 그대가 물목을 짚어 주면, 내가 먼저 끊겠소.',
|
|
'왕평: 성문보다 먼저 나루가 무너집니다. 나루를 막으면 성문도 하루 더 버틸 수 있습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-hidden-fords',
|
|
label: '숨은 물목을 표시한다',
|
|
response: '왕평은 갈대밭 사이 물목을 표시했고, 조운은 다음 출진의 차단 경로를 익혔습니다.',
|
|
rewardExp: 16
|
|
},
|
|
{
|
|
id: 'drill-fast-blockade',
|
|
label: '빠른 차단을 훈련한다',
|
|
response: '조운의 기동대는 왕평이 잡은 길을 따라 나루 차단 훈련을 반복했습니다.',
|
|
rewardExp: 15
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-wangping-after-maicheng',
|
|
title: '북문을 짚는 손',
|
|
availableAfterBattleIds: [campBattleIds.fortyFourth],
|
|
unitIds: ['guan-yu', 'wang-ping'],
|
|
bondId: 'guan-yu__wang-ping_maicheng',
|
|
rewardExp: 50,
|
|
lines: [
|
|
'왕평: 맥성의 북문은 넓어 보이지만, 실제로는 성 밖 둔덕과 마른 해자가 길을 나눕니다.',
|
|
'관우: 성이 작을수록 길 하나가 목숨이 되겠구려. 그대가 짚은 길을 놓치지 않겠소.',
|
|
'왕평: 무리하게 곧장 나가면 포위가 다시 닫힙니다. 한 번 열고, 한 번 더 버틸 지점이 필요합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-north-gate-rally',
|
|
label: '북문 집결점을 정한다',
|
|
response: '관우는 북문 밖 둔덕에 집결 깃발을 세우게 했고, 왕평은 돌파 뒤 다시 모일 길을 정리했습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'keep-second-line',
|
|
label: '두 번째 방어선을 세운다',
|
|
response: '왕평은 마른 해자를 따라 예비 방어선을 그었고, 관우군은 포위망이 다시 닫힐 때 버틸 지점을 얻었습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhaoyun-madai-after-maicheng',
|
|
title: '갈대 물목의 기병',
|
|
availableAfterBattleIds: [campBattleIds.fortyFourth],
|
|
unitIds: ['zhao-yun', 'ma-dai'],
|
|
bondId: 'zhao-yun__ma-dai_maicheng',
|
|
rewardExp: 48,
|
|
lines: [
|
|
'마대: 물가의 길은 말발굽이 깊이 빠집니다. 빠르게 달리는 것보다 멈추는 자리를 먼저 보아야 합니다.',
|
|
'조운: 그러면 내가 길을 열고, 그대가 뒤의 추격을 꺾으면 되겠소.',
|
|
'마대: 위군 기병은 급히 달려들 것입니다. 물목에서 말머리를 비틀게 만들면 돌파로 이어집니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'stagger-cavalry-crossing',
|
|
label: '기병 통과 순서를 나눈다',
|
|
response: '조운과 마대는 갈대 물목을 두 차례로 나누어 건너는 기병 운용을 정했습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'bait-wei-cavalry',
|
|
label: '위군 기병을 유인한다',
|
|
response: '마대는 좁은 물목까지 위군 기병을 끌어들이는 움직임을 정리했고, 조운은 빠져나갈 길을 확보했습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guan-huangzhong-after-maicheng',
|
|
title: '노장의 활, 운장의 길',
|
|
availableAfterBattleIds: [campBattleIds.fortyFourth],
|
|
unitIds: ['guan-yu', 'huang-zhong'],
|
|
bondId: 'guan-yu__huang-zhong_maicheng',
|
|
rewardExp: 48,
|
|
lines: [
|
|
'황충: 성 밖 궁수들은 길을 막기보다 마음을 흔듭니다. 깃발이 멈추면 병사들이 먼저 움츠러듭니다.',
|
|
'관우: 내가 앞에서 멈추지 않으면, 노장께서 뒤의 화살길을 지켜 주시겠소?',
|
|
'황충: 활은 늙어도 길을 잃지 않습니다. 운장께서 길을 열면, 나는 그 길 위의 눈을 끊겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'clear-archer-nests',
|
|
label: '궁수 거점을 먼저 끊는다',
|
|
response: '황충은 성 밖 궁수 거점을 표시했고, 관우는 돌파 전 화살길을 잠시 멈출 기회를 얻었습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'protect-guan-banner',
|
|
label: '관우의 깃발을 엄호한다',
|
|
response: '황충은 관우의 깃발이 흔들리지 않게 엄호 위치를 잡았고, 병사들의 사기가 버텼습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-zhang-after-yiling-vanguard',
|
|
title: '복수의 맹세',
|
|
availableAfterBattleIds: [campBattleIds.fortyFifth],
|
|
unitIds: ['liu-bei', 'zhang-fei'],
|
|
bondId: 'liu-bei__zhang-fei_yiling',
|
|
rewardExp: 52,
|
|
lines: [
|
|
'장비: 형님, 길이 열렸다면 더 기다릴 까닭이 없습니다. 오군의 깃발이 보이는 곳까지 단숨에 밀어붙입시다!',
|
|
'유비: 익덕의 분노가 내 마음과 다르지 않소. 그러나 병사들이 분노보다 먼저 쓰러져서는 안 되오.',
|
|
'장비: 그럼 제가 앞에서 길을 열겠습니다. 형님은 뒤의 깃발을 흔들리지 않게 붙잡아 주십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'temper-revenge-charge',
|
|
label: '분노의 돌격을 다스린다',
|
|
response: '유비는 장비의 돌격을 인정하되 진영의 호흡을 맞추라 당부했고, 장비는 창끝을 한 번 더 가다듬었습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'promise-brother-oath',
|
|
label: '형제의 맹세를 다시 세운다',
|
|
response: '두 사람은 무너진 형주의 길 위에서 도원의 맹세를 다시 떠올렸고, 병사들은 복수의 깃발 아래 모였습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-huangquan-after-yiling-vanguard',
|
|
title: '불길을 읽는 장부',
|
|
availableAfterBattleIds: [campBattleIds.fortyFifth],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan_yiling',
|
|
rewardExp: 50,
|
|
lines: [
|
|
'황권: 이릉 길은 넓어 보여도 진영이 길게 늘어지면 하나의 불씨가 여러 군막으로 옮겨붙습니다.',
|
|
'제갈량: 육손이 기다리는 까닭도 거기에 있겠지요. 승리보다 진영의 길이가 먼저 위험해졌습니다.',
|
|
'황권: 보급 장부에 불이 옮겨붙기 전에, 군막의 간격과 물길을 다시 정해야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'space-camps-carefully',
|
|
label: '군막 간격을 다시 잰다',
|
|
response: '제갈량과 황권은 군막의 간격과 물길을 다시 표시했고, 촉한군은 화공 위험을 조금 더 의식하게 되었습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'log-river-supply',
|
|
label: '강변 보급 장부를 정리한다',
|
|
response: '황권은 강변 보급 장부를 정리했고, 제갈량은 길어진 진영이 무너질 지점을 미리 짚었습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhaoyun-machao-after-yiling-vanguard',
|
|
title: '동진 기동대',
|
|
availableAfterBattleIds: [campBattleIds.fortyFifth],
|
|
unitIds: ['zhao-yun', 'ma-chao'],
|
|
bondId: 'zhao-yun__ma-chao_yiling',
|
|
rewardExp: 48,
|
|
lines: [
|
|
'마초: 오군은 달려들지 않고 뒤로 물러납니다. 말이 빠를수록 빈 곳을 치게 만들려는 속셈입니다.',
|
|
'조운: 그렇다면 빠르게 가되, 돌아올 길을 먼저 보아야 하오.',
|
|
'마초: 내가 측면을 흔들면 장군께서 끊어진 길을 메우십시오. 진영이 길어져도 기병이 서로를 잃지 않게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pair-cavalry-flanks',
|
|
label: '기병 측면을 짝지운다',
|
|
response: '조운과 마초는 기병대를 둘로 나누어도 서로의 깃발을 잃지 않도록 신호를 정했습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'hold-return-road',
|
|
label: '귀환로를 먼저 잡는다',
|
|
response: '마초는 돌격 전 귀환로를 표시했고, 조운은 뒤처진 진영을 빠르게 보강할 길을 익혔습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'liu-huangquan-after-yiling-fire',
|
|
title: '퇴로의 장부',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
unitIds: ['liu-bei', 'huang-quan'],
|
|
bondId: 'liu-bei__huang-quan_yiling-fire',
|
|
rewardExp: 52,
|
|
lines: [
|
|
'황권: 폐하, 불길은 군막을 태웠지만 장부까지 태우면 남은 병사를 다시 세울 수 없습니다.',
|
|
'유비: 내가 서두른 탓이 크오. 그렇기에 남은 자들의 이름을 더 잊지 말아야 하오.',
|
|
'황권: 퇴로를 기록하고 흩어진 부대를 셈하겠습니다. 다음 싸움보다 먼저, 남은 군의 숨을 이어야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'count-retreat-columns',
|
|
label: '퇴각 부대를 다시 센다',
|
|
response: '유비는 황권에게 퇴각 부대의 이름과 위치를 모두 다시 세게 했고, 남은 병사들의 동요가 조금 잦아들었습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'protect-supply-ledgers',
|
|
label: '보급 장부를 지킨다',
|
|
response: '황권은 불탄 군막에서 건진 보급 장부를 정리했고, 유비는 다음 길을 급히 정하지 않겠다고 고개를 끄덕였습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhaoyun-wangping-after-yiling-fire',
|
|
title: '불길 속 길잡이',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
unitIds: ['zhao-yun', 'wang-ping'],
|
|
bondId: 'zhao-yun__wang-ping_yiling-fire',
|
|
rewardExp: 50,
|
|
lines: [
|
|
'왕평: 불길이 지나간 숲은 길처럼 보이지만, 재가 깊어 말과 병사가 함께 빠집니다.',
|
|
'조운: 길이 사라졌다면 다시 만들면 되오. 다만 뒤처진 병사가 그 길을 알아볼 수 있어야 하오.',
|
|
'왕평: 강변의 낮은 돌과 그을린 나무를 표식으로 삼겠습니다. 불길이 만든 길을 퇴로로 바꾸겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-ash-road',
|
|
label: '그을린 길을 표시한다',
|
|
response: '조운은 그을린 숲길에 작은 깃발을 남겼고, 왕평은 퇴로 표식을 병사들에게 나누어 익혔습니다.',
|
|
rewardExp: 17
|
|
},
|
|
{
|
|
id: 'cover-stragglers',
|
|
label: '뒤처진 병사를 엄호한다',
|
|
response: '왕평은 늦은 병사들이 빠지는 낮은 땅을 짚었고, 조운은 추격대를 끊는 짧은 기동로를 잡았습니다.',
|
|
rewardExp: 16
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'zhuge-maliang-after-yiling-fire',
|
|
title: '남은 뜻의 기록',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang_yiling-fire',
|
|
rewardExp: 50,
|
|
lines: [
|
|
'마량: 이릉의 패착을 숨기면 병사들은 같은 불길을 다시 만날 것입니다.',
|
|
'제갈량: 아픈 기록일수록 남겨야 합니다. 나라가 오래 버티려면 승리보다 실패의 모양을 알아야 하지요.',
|
|
'마량: 백제성에 닿기 전에, 잃은 길과 남은 길을 나누어 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-fire-lesson',
|
|
label: '화공의 원인을 기록한다',
|
|
response: '제갈량과 마량은 늘어진 진영, 마른 숲, 바람의 방향을 차례로 적어 다음 작전의 금기로 삼았습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'prepare-baidi-council',
|
|
label: '백제성 논의를 준비한다',
|
|
response: '마량은 백제성에 모일 문서를 정리했고, 제갈량은 전장이 아닌 조정에서 이어질 책임을 조용히 받아들였습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'baidi-liu-zhuge-entrustment',
|
|
title: '마지막 부탁',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
availableDuringSteps: ['baidi-entrustment-camp'],
|
|
unitIds: ['liu-bei', 'zhuge-liang'],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
rewardExp: 58,
|
|
lines: [
|
|
'유비: 공명, 내 뜻이 사람을 살리는 데서 시작했으나 끝내 많은 사람을 잃게 했소.',
|
|
'제갈량: 폐하의 뜻은 아직 남았습니다. 신은 그 뜻이 흔들리지 않도록 나라의 숨을 이어 가겠습니다.',
|
|
'유비: 후주와 촉한의 백성을 부탁하오. 그대의 어깨에 너무 큰 짐을 남기는구려.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-people-first',
|
|
label: '백성을 먼저 부탁한다',
|
|
response: '제갈량은 군사보다 먼저 백성을 살피겠다고 답했고, 유비는 긴 숨을 내쉬며 고개를 끄덕였습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'entrust-state-ledger',
|
|
label: '나라의 장부를 맡긴다',
|
|
response: '유비는 남은 장부와 인장을 제갈량에게 맡겼고, 제갈량은 말없이 두 손으로 받아 들었습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'baidi-zhuge-maliang-record',
|
|
title: '유탁의 기록',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
availableDuringSteps: ['baidi-entrustment-camp'],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang_yiling-fire',
|
|
rewardExp: 54,
|
|
lines: [
|
|
'마량: 백제성의 말은 조정에서 여러 뜻으로 해석될 것입니다. 지금 정확히 적어 두어야 합니다.',
|
|
'제갈량: 기록이 흔들리면 사람의 마음도 흔들립니다. 폐하의 뜻은 무리한 복수가 아니라 남은 나라를 살리는 데 있습니다.',
|
|
'마량: 그 뜻을 문서로 남기겠습니다. 장수들이 각자 다르게 듣지 않도록 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-entrustment-edict',
|
|
label: '유탁 문서를 정리한다',
|
|
response: '마량은 백제성에서 오간 말을 조심스럽게 적었고, 제갈량은 다음 조정의 첫 문장을 함께 고쳤습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'prepare-regency-council',
|
|
label: '섭정 의정을 준비한다',
|
|
response: '제갈량과 마량은 장수들이 모일 순서와 논의할 안건을 정리해, 백제성 이후의 혼란을 줄였습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'baidi-liu-huangquan-retreat',
|
|
title: '남은 군의 숨',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
availableDuringSteps: ['baidi-entrustment-camp'],
|
|
unitIds: ['liu-bei', 'huang-quan'],
|
|
bondId: 'liu-bei__huang-quan_yiling-fire',
|
|
rewardExp: 54,
|
|
lines: [
|
|
'황권: 폐하, 남은 군을 살리려면 패전의 책임보다 먼저 흩어진 부대의 위치를 확인해야 합니다.',
|
|
'유비: 옳소. 나의 잘못을 따지는 일은 뒤로 미루고, 살아 있는 자들이 돌아올 길을 먼저 여시오.',
|
|
'황권: 백제성의 깃발 아래 다시 셈하겠습니다. 잃은 길을 기록해야 다음 길이 생깁니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'recover-retreat-roster',
|
|
label: '퇴각 명부를 회수한다',
|
|
response: '황권은 흩어진 퇴각 명부를 다시 모았고, 유비는 이름 하나하나를 놓치지 말라고 당부했습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'open-baidi-gates',
|
|
label: '성문 수용 순서를 정한다',
|
|
response: '백제성의 수용 순서가 정리되어, 부상병과 패잔병이 더 큰 혼란 없이 성 안으로 들어왔습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'nanzhong-zhuge-zhaoyun-vanguard',
|
|
title: '섭정의 첫 선봉',
|
|
availableAfterBattleIds: [campBattleIds.fortySeventh],
|
|
unitIds: ['zhuge-liang', 'zhao-yun'],
|
|
bondId: 'zhuge-liang__zhao-yun_nanzhong',
|
|
rewardExp: 56,
|
|
lines: [
|
|
'제갈량: 남중의 길은 이기기만 해서는 다시 닫힙니다. 먼저 병사들이 두려워하지 않을 길을 열어야 합니다.',
|
|
'조운: 선봉은 제가 맡겠습니다. 칼끝보다 깃발이 먼저 보이도록, 지나친 추격은 삼가겠습니다.',
|
|
'제갈량: 그래서 자룡이 필요합니다. 흔들린 나라의 첫 발은 빠르되 가볍지 않아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'steady-vanguard-route',
|
|
label: '안정된 선봉로를 맡긴다',
|
|
response: '조운은 산길 표식을 나누어 세우고, 제갈량은 선봉과 본대의 간격을 다시 계산했습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'protect-rear-wounded',
|
|
label: '후위 회수를 부탁한다',
|
|
response: '조운은 부상병과 보급대가 뒤처지지 않도록 후위 기동선을 먼저 확인했습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'nanzhong-zhuge-huangquan-ledger',
|
|
title: '나라 안의 장부',
|
|
availableAfterBattleIds: [campBattleIds.fortySeventh],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan_nanzhong',
|
|
rewardExp: 54,
|
|
lines: [
|
|
'황권: 남중의 창고를 빼앗기만 하면 반란은 잠시 잦아들 뿐입니다. 장부가 다시 돌아와야 길이 남습니다.',
|
|
'제갈량: 토벌과 회유의 순서를 틀리면 백성은 다시 산으로 숨을 것입니다.',
|
|
'황권: 군량을 셀 때도 사람의 마음을 함께 세겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'audit-southern-storehouse',
|
|
label: '창고 장부를 먼저 본다',
|
|
response: '황권은 남중 창고의 빼앗긴 양과 돌려줄 양을 나누어, 전후 처리의 기준을 세웠습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'prepare-amnesty-list',
|
|
label: '회유 명단을 준비한다',
|
|
response: '제갈량과 황권은 끝까지 싸울 수장과 먼저 달랠 촌장을 분리해 적었습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'nanzhong-maliang-wangping-villages',
|
|
title: '마을과 산길',
|
|
availableAfterBattleIds: [campBattleIds.fortySeventh],
|
|
unitIds: ['ma-liang', 'wang-ping'],
|
|
bondId: 'ma-liang__wang-ping_nanzhong',
|
|
rewardExp: 52,
|
|
lines: [
|
|
'왕평: 이 길은 지도대로만 가면 늪으로 빠집니다. 마을 사람들이 쓰는 낮은 둑길이 따로 있습니다.',
|
|
'마량: 그렇다면 마을을 적으로 돌리지 않는 것이 곧 지형을 얻는 일이군요.',
|
|
'왕평: 예. 산길은 칼로 여는 길이 있고, 말로 열리는 길이 있습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'ask-village-guides',
|
|
label: '마을 길잡이를 구한다',
|
|
response: '마량은 촌장들에게 군령보다 먼저 보호 약속을 전했고, 왕평은 낮은 둑길을 지도에 표시했습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'mark-marsh-detours',
|
|
label: '늪지 우회로를 표시한다',
|
|
response: '왕평은 진흙길의 깊이를 짚었고, 마량은 병사들이 마을 밭을 밟지 않는 우회 명령을 정리했습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'menghuo-zhuge-maliang-release',
|
|
title: '풀어 보내는 이유',
|
|
availableAfterBattleIds: [campBattleIds.fortyEighth],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang_menghuo',
|
|
rewardExp: 58,
|
|
lines: [
|
|
'마량: 맹획을 잡았는데 바로 돌려보내면 병사들이 의아해할 것입니다.',
|
|
'제갈량: 남중은 칼로 눌러도 산으로 숨습니다. 맹획이 직접 보고 돌아가야, 마을들이 촉한군의 뜻을 의심하지 않습니다.',
|
|
'마량: 그 뜻을 문서로 남기겠습니다. 처단이 아니라 회유라는 군령을 분명히 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-release-edict',
|
|
label: '석방 군령을 적는다',
|
|
response: '마량은 맹획 석방의 이유를 짧고 분명하게 적었고, 제갈량은 포로와 병사 앞에서 그 문장을 읽게 했습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'explain-to-officers',
|
|
label: '장수들에게 설명한다',
|
|
response: '제갈량은 장수들에게 남중을 얻는 싸움은 반복해서 설득하는 싸움이라고 설명했습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'menghuo-zhaoyun-machao-charge',
|
|
title: '맹수 돌격을 막은 기병',
|
|
availableAfterBattleIds: [campBattleIds.fortyEighth],
|
|
unitIds: ['zhao-yun', 'ma-chao'],
|
|
bondId: 'zhao-yun__ma-chao_menghuo',
|
|
rewardExp: 54,
|
|
lines: [
|
|
'마초: 남중의 기병은 말만 타는 것이 아니더군. 짐승과 함께 밀고 들어오니 보통 돌격과 달랐소.',
|
|
'조운: 기세를 부러뜨리되 깊이 쫓지 않는 것이 중요했습니다. 오늘의 목표는 섬멸이 아니었으니까요.',
|
|
'마초: 다음에는 더 거칠게 올 것이오. 그렇다면 더 단단히 받아 주면 되겠지.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'hold-beast-charge-line',
|
|
label: '돌격 차단선을 정리한다',
|
|
response: '조운과 마초는 맹수 기병이 몰리는 길목과 기병이 빠질 길을 따로 표시했습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'restrain-pursuit',
|
|
label: '추격 금지를 확인한다',
|
|
response: '마초는 병사들에게 깊은 추격을 금했고, 조운은 포로가 흩어지지 않도록 후위를 세웠습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'menghuo-huangquan-wangping-captives',
|
|
title: '포로와 산길 장부',
|
|
availableAfterBattleIds: [campBattleIds.fortyEighth],
|
|
unitIds: ['huang-quan', 'wang-ping'],
|
|
bondId: 'huang-quan__wang-ping_menghuo',
|
|
rewardExp: 54,
|
|
lines: [
|
|
'황권: 포로를 군량 장부처럼 세면 안 됩니다. 누구는 반란군이고, 누구는 끌려온 마을 사람입니다.',
|
|
'왕평: 산길도 같습니다. 군대가 쓰는 길과 마을이 살아가는 길을 구분하지 않으면 다음에 모두 막힙니다.',
|
|
'황권: 포로 명부와 산길 표식을 함께 정리합시다. 다음 회유의 근거가 될 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-captive-ledger',
|
|
label: '포로 명부를 나눈다',
|
|
response: '황권은 반란군, 마을 사람, 길잡이를 따로 적었고 왕평은 각자가 돌아갈 길을 표시했습니다.',
|
|
rewardExp: 18
|
|
},
|
|
{
|
|
id: 'mark-return-routes',
|
|
label: '귀환 산길을 표시한다',
|
|
response: '왕평은 포로가 돌아갈 낮은 산길을 정했고, 황권은 촉한군이 침범하지 말아야 할 마을 밭을 함께 적었습니다.',
|
|
rewardExp: 17
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'second-capture-zhuge-huangquan-ledger',
|
|
title: '두 번째 석방의 장부',
|
|
availableAfterBattleIds: [campBattleIds.fortyNinth],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan_second-capture',
|
|
rewardExp: 60,
|
|
lines: [
|
|
'황권: 맹획을 두 번이나 놓아 보내면 군량을 쓰는 병사들이 먼저 의심할 수 있습니다.',
|
|
'제갈량: 그러니 장부가 필요합니다. 풀어 보내는 일이 방종이 아니라 남중 전체를 얻기 위한 군율임을 보여야 합니다.',
|
|
'황권: 포로 명부와 지급 군량을 함께 남기겠습니다. 다음에 그가 돌아와도 우리가 흔들리지 않았다는 증거가 될 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'record-second-release',
|
|
label: '석방 장부를 남긴다',
|
|
response: '황권은 맹획의 두 번째 석방과 포로 귀환 순서를 장부에 적었고, 제갈량은 장수들에게 그 뜻을 설명했습니다.',
|
|
rewardExp: 21
|
|
},
|
|
{
|
|
id: 'calm-officer-doubts',
|
|
label: '장수들의 의문을 달랜다',
|
|
response: '제갈량은 당장의 분노보다 오래 남을 질서를 택해야 한다고 말했고, 황권은 그 말을 군율 조항으로 정리했습니다.',
|
|
rewardExp: 20
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'second-capture-maliang-wangping-village-road',
|
|
title: '돌아가는 길의 말',
|
|
availableAfterBattleIds: [campBattleIds.fortyNinth],
|
|
unitIds: ['ma-liang', 'wang-ping'],
|
|
bondId: 'ma-liang__wang-ping_second-capture',
|
|
rewardExp: 56,
|
|
lines: [
|
|
'왕평: 맹획이 돌아가는 산길마다 우리가 지킨 마을이 있습니다. 그 길을 막지 말고 보게 해야 합니다.',
|
|
'마량: 마을 사람들이 직접 말하게 하자는 뜻이군요. 촉한군이 약탈하지 않았다는 사실은 글보다 빠르게 퍼질 것입니다.',
|
|
'왕평: 예. 다음 싸움의 길은 오늘 돌아가는 길에서 이미 열릴 수 있습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'escort-release-route',
|
|
label: '귀환 길을 보호한다',
|
|
response: '왕평은 맹획 일행이 지나갈 낮은 산길을 열었고, 마량은 마을마다 군령과 보호 표식을 남겼습니다.',
|
|
rewardExp: 19
|
|
},
|
|
{
|
|
id: 'let-villages-speak',
|
|
label: '마을의 말을 남긴다',
|
|
response: '마량은 촉한군이 값을 치르고 산 물자와 돌려보낸 포로의 이름을 마을 어른들이 직접 말하게 했습니다.',
|
|
rewardExp: 18
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'second-capture-zhaoyun-huangzhong-net',
|
|
title: '생포망을 조이는 법',
|
|
availableAfterBattleIds: [campBattleIds.fortyNinth],
|
|
unitIds: ['zhao-yun', 'huang-zhong'],
|
|
bondId: 'zhao-yun__huang-zhong_second-capture',
|
|
rewardExp: 56,
|
|
lines: [
|
|
'황충: 맹획의 군세는 더 거칠어졌지만, 고지 궁병을 눌러 두니 포위망이 오래 버텼소.',
|
|
'조운: 돌격을 받아도 끝까지 베지 않는 절제가 더 어려웠습니다.',
|
|
'황충: 다음에도 잡아야 한다면, 먼저 도망갈 길과 몰아넣을 길을 구분해야겠군.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'tighten-capture-net',
|
|
label: '생포망을 다시 짠다',
|
|
response: '조운은 추격 금지선을 정했고, 황충은 적장이 빠져나갈 고지와 협곡을 표시했습니다.',
|
|
rewardExp: 19
|
|
},
|
|
{
|
|
id: 'pin-shaman-archers',
|
|
label: '주술사와 궁병을 묶는다',
|
|
response: '황충은 고지 궁병을 먼저 억누르는 순서를 정했고, 조운은 그 사이 기병이 파고들 길을 지켰습니다.',
|
|
rewardExp: 18
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'third-capture-zhuge-maliang-clans',
|
|
title: '호족에게 남길 말',
|
|
availableAfterBattleIds: [campBattleIds.fiftieth],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang_third-capture',
|
|
rewardExp: 62,
|
|
lines: [
|
|
'마량: 세 번째로 맹획을 놓아 보내면, 이제 호족들이 먼저 우리 뜻을 재기 시작할 것입니다.',
|
|
'제갈량: 그래서 말이 더 중요합니다. 맹획을 굴복시키는 것이 아니라 남중이 스스로 촉한의 질서를 택하게 해야 합니다.',
|
|
'마량: 호족마다 받아들일 말이 다를 것입니다. 다음 회의에서는 누구를 먼저 달랠지 정해야겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-clan-amnesty',
|
|
label: '호족 설득문을 쓴다',
|
|
response: '마량은 각 호족에게 보낼 문장을 나누어 적었고, 제갈량은 회유와 처벌의 기준을 더 분명히 했습니다.',
|
|
rewardExp: 22
|
|
},
|
|
{
|
|
id: 'prepare-clan-council',
|
|
label: '호족 회의를 준비한다',
|
|
response: '제갈량은 장수들에게 다음 전투 전 회의에서 남중 호족의 마음을 먼저 읽어야 한다고 지시했습니다.',
|
|
rewardExp: 21
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'third-capture-huangquan-wangping-order',
|
|
title: '장부와 길목의 질서',
|
|
availableAfterBattleIds: [campBattleIds.fiftieth],
|
|
unitIds: ['huang-quan', 'wang-ping'],
|
|
bondId: 'huang-quan__wang-ping_third-capture',
|
|
rewardExp: 58,
|
|
lines: [
|
|
'황권: 세 번의 생포 동안 포로 명부가 흐트러지지 않았습니다. 이 장부가 호족들을 설득할 증거가 됩니다.',
|
|
'왕평: 길목도 같습니다. 우리가 막은 길보다 열어 둔 길을 더 오래 기억할 것입니다.',
|
|
'황권: 다음에는 전투 전에 어느 길을 열어 둘지부터 정해야겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'compare-captive-ledgers',
|
|
label: '세 장부를 대조한다',
|
|
response: '황권은 세 번의 포로 명부를 대조했고, 왕평은 귀환 길목이 반복해서 지켜졌음을 지도에 남겼습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'mark-open-routes',
|
|
label: '열어 둘 길을 표시한다',
|
|
response: '왕평은 다음 전투에서 일부 호족이 물러날 길을 표시했고, 황권은 그 길의 군량 지급 기준을 적었습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'third-capture-zhaoyun-machao-restraint',
|
|
title: '세 번째 절제',
|
|
availableAfterBattleIds: [campBattleIds.fiftieth],
|
|
unitIds: ['zhao-yun', 'ma-chao'],
|
|
bondId: 'zhao-yun__ma-chao_third-capture',
|
|
rewardExp: 58,
|
|
lines: [
|
|
'마초: 세 번이나 잡고도 베지 않는 싸움은 생각보다 더 힘들군.',
|
|
'조운: 칼을 멈추는 것도 명령을 지키는 일입니다. 남중의 눈이 우리를 보고 있으니까요.',
|
|
'마초: 다음에도 돌격을 막아야 한다면, 이번처럼 잡되 무너뜨리지는 않겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'restrain-third-charge',
|
|
label: '돌격 억제선을 정한다',
|
|
response: '조운과 마초는 추격과 포위의 경계선을 다시 정해, 병사들이 분노로 깊이 파고들지 않게 했습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'share-cavalry-lessons',
|
|
label: '기병 운용을 나눈다',
|
|
response: '마초는 남만 기병의 습성을 설명했고, 조운은 그 돌격을 받아 내는 간격을 병사들에게 익히게 했습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fourth-capture-zhuge-maliang-amnesty',
|
|
title: '네 번째 석방의 조건',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFirst],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang_fourth-capture',
|
|
rewardExp: 64,
|
|
lines: [
|
|
'마량: 이번에는 물러난 호족들이 직접 회유 퇴로가 지켜졌다고 말할 것입니다.',
|
|
'제갈량: 그 말이 남중 안에서 맹획의 말과 겨루게 됩니다. 네 번째 석방은 약함이 아니라 약속을 지키는 힘이어야 합니다.',
|
|
'마량: 다음에는 강경파가 그 말을 끊으려 할 것입니다. 먼저 누구의 입을 열지 정해야겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'record-fourth-amnesty',
|
|
label: '네 번째 보증문을 남긴다',
|
|
response: '마량은 회유 퇴로를 통과한 호족들의 증언을 문서로 적었고, 제갈량은 다음 석방의 조건을 분명히 했습니다.',
|
|
rewardExp: 23
|
|
},
|
|
{
|
|
id: 'prepare-hardliner-questions',
|
|
label: '강경파 질문을 준비한다',
|
|
response: '제갈량은 끝까지 싸우려는 호족에게 던질 질문을 정했고, 마량은 마을 피해와 포로 귀환 기록을 함께 묶었습니다.',
|
|
rewardExp: 22
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fourth-capture-huangquan-wangping-ledger',
|
|
title: '퇴로와 장부의 보증',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFirst],
|
|
unitIds: ['huang-quan', 'wang-ping'],
|
|
bondId: 'huang-quan__wang-ping_fourth-capture',
|
|
rewardExp: 60,
|
|
lines: [
|
|
'황권: 회유 퇴로를 열어 두었으니, 돌아간 자가 다시 창을 들면 장부가 먼저 흔들립니다.',
|
|
'왕평: 그래서 길도 장부처럼 관리해야 합니다. 누구에게 열린 길인지, 어느 마을을 지나지 않는지 표시해야 합니다.',
|
|
'황권: 다음 전투에서는 그 보증을 더 엄격히 봐야겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-amnesty-ledger',
|
|
label: '보증 장부를 봉한다',
|
|
response: '황권은 회유 퇴로를 지난 이름들을 봉인했고, 왕평은 그들이 다시 병력으로 합류하지 못할 길을 표시했습니다.',
|
|
rewardExp: 21
|
|
},
|
|
{
|
|
id: 'separate-safe-roads',
|
|
label: '안전 길목을 분리한다',
|
|
response: '왕평은 마을길과 군용길을 다시 나누었고, 황권은 병사들이 보증 길목을 짓밟지 않도록 명령서를 적었습니다.',
|
|
rewardExp: 20
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fourth-capture-machao-madai-pursuit',
|
|
title: '추격하지 않는 속도',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFirst],
|
|
unitIds: ['ma-chao', 'ma-dai'],
|
|
bondId: 'ma-chao__ma-dai_fourth-capture',
|
|
rewardExp: 58,
|
|
lines: [
|
|
'마대: 형님, 이번에는 달려가서 베는 것보다 멈추는 것이 더 어려웠습니다.',
|
|
'마초: 남중의 기병은 밀어붙여야 무너진다. 하지만 퇴로를 밟으면 우리가 이긴 전장이 다시 적지가 된다.',
|
|
'마대: 다음에는 빠르게 막되, 물러나는 길은 더 분명히 남기겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-pursuit-line',
|
|
label: '추격 금지선을 긋는다',
|
|
response: '마초와 마대는 기병이 넘지 말아야 할 회유선을 정했고, 빠른 압박과 절제를 함께 훈련했습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'train-cavalry-recall',
|
|
label: '기병 회수 신호를 맞춘다',
|
|
response: '마대는 회수 신호를 짧게 정했고, 마초는 돌격대가 신호에 맞춰 멈추는 훈련을 반복시켰습니다.',
|
|
rewardExp: 19
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fifth-capture-zhuge-huangquan-separation',
|
|
title: '갈라진 이름의 장부',
|
|
availableAfterBattleIds: [campBattleIds.fiftySecond],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan_fifth-capture',
|
|
rewardExp: 66,
|
|
lines: [
|
|
'황권: 다섯 번째 생포 뒤에는 끝까지 싸운 자와 물러난 자의 이름을 더 엄격히 나누어야 합니다.',
|
|
'제갈량: 처벌을 위한 장부가 되면 다시 원한이 됩니다. 약속을 지킨 자를 보호하는 장부로 남겨야 합니다.',
|
|
'황권: 그렇다면 강경파를 누르되, 돌아선 자의 길은 먼저 지켜 두겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-hardliner-ledger',
|
|
label: '강경파 장부를 나눈다',
|
|
response: '황권은 강경파와 회유파의 이름을 따로 적었고, 제갈량은 석방의 기준을 병사들에게 다시 알렸습니다.',
|
|
rewardExp: 23
|
|
},
|
|
{
|
|
id: 'protect-returning-clans',
|
|
label: '돌아선 호족을 보호한다',
|
|
response: '제갈량은 물러난 호족의 마을을 함부로 징발하지 말라 명했고, 황권은 그 이름을 보증 장부에 옮겼습니다.',
|
|
rewardExp: 22
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fifth-capture-maliang-wangping-routes',
|
|
title: '말이 닿는 길',
|
|
availableAfterBattleIds: [campBattleIds.fiftySecond],
|
|
unitIds: ['ma-liang', 'wang-ping'],
|
|
bondId: 'ma-liang__wang-ping_fifth-capture',
|
|
rewardExp: 62,
|
|
lines: [
|
|
'마량: 말만 남기면 길이 막히고, 길만 열면 뜻이 흐려집니다.',
|
|
'왕평: 그래서 이번 지도에는 말이 닿을 마을과 군대가 지나갈 길을 따로 그려야 합니다.',
|
|
'마량: 다음 싸움에서는 그 지도가 곧 회유의 증거가 되겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'copy-amnesty-routes',
|
|
label: '회유 길을 베낀다',
|
|
response: '왕평은 낮은 길을 다시 그렸고, 마량은 그 길마다 붙일 짧은 문장을 적었습니다.',
|
|
rewardExp: 21
|
|
},
|
|
{
|
|
id: 'separate-village-messages',
|
|
label: '마을별 문구를 나눈다',
|
|
response: '마량은 마을마다 다른 두려움을 적었고, 왕평은 그 마을로 이어지는 좁은 길을 표시했습니다.',
|
|
rewardExp: 20
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fifth-capture-zhaoyun-huangzhong-containment',
|
|
title: '멀리서 막는 추격',
|
|
availableAfterBattleIds: [campBattleIds.fiftySecond],
|
|
unitIds: ['zhao-yun', 'huang-zhong'],
|
|
bondId: 'zhao-yun__huang-zhong_fifth-capture',
|
|
rewardExp: 60,
|
|
lines: [
|
|
'황충: 이번 강경파는 가까이 붙으면 오히려 회유 길을 흔들었소.',
|
|
'조운: 그래서 먼저 끊어야 했습니다. 칼끝이 아니라 거리로 막는 싸움이었습니다.',
|
|
'황충: 다음에도 고지와 중앙을 함께 잡아야 맹획을 잡되 남중을 잃지 않겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'train-distant-containment',
|
|
label: '원거리 억제선을 훈련한다',
|
|
response: '황충은 고지 사격선을 정했고, 조운은 그 앞을 지키는 간격을 병사들에게 익혔습니다.',
|
|
rewardExp: 20
|
|
},
|
|
{
|
|
id: 'hold-center-and-hill',
|
|
label: '중앙과 고지를 나눈다',
|
|
response: '조운은 중앙 방어선을 맡고, 황충은 고지 궁병을 견제할 위치를 다시 표시했습니다.',
|
|
rewardExp: 20
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sixth-capture-zhuge-maliang-pride',
|
|
title: '자존심을 남기는 말',
|
|
availableAfterBattleIds: [campBattleIds.fiftyThird],
|
|
unitIds: ['zhuge-liang', 'ma-liang'],
|
|
bondId: 'zhuge-liang__ma-liang_sixth-capture',
|
|
rewardExp: 70,
|
|
lines: [
|
|
'마량: 여섯 번째 생포 뒤에도 맹획은 쉽게 고개를 숙이지 않을 것입니다.',
|
|
'제갈량: 그렇기에 그의 체면을 완전히 부수지 않는 말이 필요합니다. 남중의 마음은 굴복보다 납득으로 움직입니다.',
|
|
'마량: 다음에는 이겼다는 말보다, 함께 살 길을 남겼다는 말을 먼저 전하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-pride-preserving-edict',
|
|
label: '체면을 남기는 격문을 쓴다',
|
|
response: '제갈량은 항복을 강요하지 않는 문장을 골랐고, 마량은 촌장들이 받아들일 수 있는 말을 덧붙였습니다.',
|
|
rewardExp: 24
|
|
},
|
|
{
|
|
id: 'prepare-final-persuasion',
|
|
label: '마지막 설득 문장을 고른다',
|
|
response: '마량은 마을마다 두려워하는 바를 정리했고, 제갈량은 마지막 석방 때 꺼낼 말을 조용히 다듬었습니다.',
|
|
rewardExp: 23
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sixth-capture-huangquan-wangping-trust',
|
|
title: '신뢰 장부와 계곡 길',
|
|
availableAfterBattleIds: [campBattleIds.fiftyThird],
|
|
unitIds: ['huang-quan', 'wang-ping'],
|
|
bondId: 'huang-quan__wang-ping_sixth-capture',
|
|
rewardExp: 66,
|
|
lines: [
|
|
'황권: 이번에는 장부에 이름만 적는 것으로 부족합니다. 어느 길을 지켰는지도 함께 남겨야 합니다.',
|
|
'왕평: 계곡은 한 번 길을 잃으면 다시 돌아오기 어렵습니다. 신뢰도 마찬가지겠지요.',
|
|
'황권: 그렇다면 다음 싸움 전에는 길과 약속을 같은 문서에 묶겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-ledger-to-routes',
|
|
label: '장부와 길표식을 묶는다',
|
|
response: '황권은 회유 장부 옆에 길표식을 붙였고, 왕평은 병사들이 넘지 말아야 할 계곡선을 표시했습니다.',
|
|
rewardExp: 22
|
|
},
|
|
{
|
|
id: 'verify-village-names',
|
|
label: '마을 이름을 대조한다',
|
|
response: '왕평이 길을 읽고 황권이 마을 이름을 대조하자, 잔당이 마을을 핑계로 숨을 틈이 줄었습니다.',
|
|
rewardExp: 21
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sixth-capture-zhaoyun-weiyan-restraint',
|
|
title: '빠른 차단과 절제',
|
|
availableAfterBattleIds: [campBattleIds.fiftyThird],
|
|
unitIds: ['zhao-yun', 'wei-yan'],
|
|
bondId: 'zhao-yun__wei-yan_sixth-capture',
|
|
rewardExp: 64,
|
|
lines: [
|
|
'위연: 잔당을 보면 더 깊이 몰아붙이고 싶지만, 이번에는 멈추라는 신호가 많았습니다.',
|
|
'조운: 빨리 달리는 것만큼 빨리 멈추는 것도 장수의 힘입니다.',
|
|
'위연: 다음에는 뚫을 곳과 멈출 곳을 먼저 나누겠습니다. 그래야 내 돌파도 군율 안에 남겠지요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'drill-fast-recall',
|
|
label: '빠른 회수 신호를 맞춘다',
|
|
response: '조운은 흰 깃발 신호를 정했고, 위연은 돌파대가 그 신호에 맞춰 돌아서는 훈련을 반복했습니다.',
|
|
rewardExp: 22
|
|
},
|
|
{
|
|
id: 'mark-breakthrough-limit',
|
|
label: '돌파 한계선을 긋는다',
|
|
response: '위연은 자신의 돌파선을 지도에 긋고, 조운은 그 선 너머의 마을길을 보호 구역으로 표시했습니다.',
|
|
rewardExp: 21
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'final-capture-zhuge-huangquan-oath',
|
|
title: '일곱 번의 약속',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan_final-capture',
|
|
rewardExp: 78,
|
|
lines: [
|
|
'황권: 맹획의 항복은 말로 끝나지 않습니다. 어떤 약속을 남기느냐가 남중의 다음 질서를 정할 것입니다.',
|
|
'제갈량: 그렇습니다. 우리가 이긴 기록보다 우리가 지킨 약속이 더 오래 남아야 합니다.',
|
|
'황권: 남중 평정 장부에는 벌할 이름보다 지켜야 할 약속을 먼저 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-nanzhong-oath',
|
|
label: '남중 서약을 봉한다',
|
|
response: '제갈량은 항복 서약의 문장을 낮추었고, 황권은 촌장들이 확인할 수 있도록 약속 조항을 따로 봉했습니다.',
|
|
rewardExp: 26
|
|
},
|
|
{
|
|
id: 'write-pacification-ledger',
|
|
label: '평정 장부를 쓴다',
|
|
response: '황권은 각 마을에 남길 약속을 정리했고, 제갈량은 군사들이 남중 백성을 징발하지 못하게 군령을 덧붙였습니다.',
|
|
rewardExp: 25
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'final-capture-maliang-wangping-council',
|
|
title: '마을과 회의장',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
unitIds: ['ma-liang', 'wang-ping'],
|
|
bondId: 'ma-liang__wang-ping_final-capture',
|
|
rewardExp: 72,
|
|
lines: [
|
|
'마량: 회의장에서 맹획이 고개를 숙였어도, 마을길이 어지러우면 항복은 오래가지 않습니다.',
|
|
'왕평: 그래서 군대가 떠난 뒤에도 남을 표식이 필요합니다. 길과 말이 함께 남아야 합니다.',
|
|
'마량: 촌장들이 스스로 설명할 수 있는 표식을 남기겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'leave-council-markers',
|
|
label: '회의장 표식을 남긴다',
|
|
response: '왕평은 회의장과 마을을 잇는 낮은 길에 표식을 남겼고, 마량은 그 뜻을 촌장들의 말로 바꾸었습니다.',
|
|
rewardExp: 24
|
|
},
|
|
{
|
|
id: 'prepare-village-notices',
|
|
label: '마을 고시문을 나눈다',
|
|
response: '마량은 마을마다 다른 고시문을 적었고, 왕평은 병사들이 어느 길을 지나지 말아야 하는지 표시했습니다.',
|
|
rewardExp: 23
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'final-capture-zhaoyun-madai-pursuit',
|
|
title: '멈추는 추격',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
unitIds: ['zhao-yun', 'ma-dai'],
|
|
bondId: 'zhao-yun__ma-dai_final-capture',
|
|
rewardExp: 68,
|
|
lines: [
|
|
'마대: 마지막 전투에서 가장 어려운 일은 달리는 것이 아니라 멈추는 것이었습니다.',
|
|
'조운: 항복로를 밟으면 승리도 원한이 됩니다. 빠른 기병일수록 군율을 먼저 알아야 합니다.',
|
|
'마대: 남중을 떠날 때도 그 선은 남겨 두겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-surrender-road',
|
|
label: '항복로를 표시한다',
|
|
response: '조운은 항복로를 지키는 흰 깃발을 세웠고, 마대는 기병이 그 선을 넘지 않도록 회수 신호를 다시 맞췄습니다.',
|
|
rewardExp: 23
|
|
},
|
|
{
|
|
id: 'train-returning-patrol',
|
|
label: '귀환 순찰을 훈련한다',
|
|
response: '마대는 남중 기병의 발걸음을 익혔고, 조운은 촉한군이 떠난 뒤에도 순찰선이 마을을 누르지 않게 정했습니다.',
|
|
rewardExp: 22
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-prep-zhuge-zhao-march',
|
|
title: '북쪽으로 놓는 첫 발',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
availableDuringSteps: ['northern-campaign-prep-camp'],
|
|
unitIds: ['zhuge-liang', 'zhao-yun'],
|
|
bondId: 'zhao-yun__zhuge-liang',
|
|
rewardExp: 76,
|
|
lines: [
|
|
'조운: 남중의 길은 정리되었지만, 북쪽 산길은 적이 먼저 기다리는 길입니다.',
|
|
'제갈량: 그래서 빠른 창보다 먼저 돌아올 수 있는 길을 정해야 합니다. 자룡의 기동대는 승리를 넓히는 손이 아니라, 전군을 잃지 않게 묶는 끈이 되어야 합니다.',
|
|
'조운: 알겠습니다. 다음 출진에서는 먼저 달리는 것보다, 모두가 따라올 수 있는 길을 열겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draw-return-routes',
|
|
label: '귀환로를 먼저 그린다',
|
|
response: '제갈량은 북방 지도 위에 귀환로를 먼저 긋고, 조운은 기병이 회수 신호를 받을 수 있는 고개와 들판을 표시했습니다.',
|
|
rewardExp: 25
|
|
},
|
|
{
|
|
id: 'assign-mobile-screen',
|
|
label: '기동 엄호대를 편성한다',
|
|
response: '조운은 빠른 장수들을 묶어 엄호 순서를 맞췄고, 제갈량은 그 부대가 본대와 너무 멀어지지 않도록 신호 체계를 정했습니다.',
|
|
rewardExp: 24
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-prep-huang-ma-ledgers',
|
|
title: '군량과 명분',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
availableDuringSteps: ['northern-campaign-prep-camp'],
|
|
unitIds: ['huang-quan', 'ma-liang'],
|
|
bondId: 'huang-quan__ma-liang_northern-prep',
|
|
rewardExp: 70,
|
|
lines: [
|
|
'황권: 북벌은 군량이 먼저 무너지는 싸움이 될 수 있습니다. 익주와 한중의 장부를 따로 보면 길이 끊깁니다.',
|
|
'마량: 장부만으로도 부족합니다. 백성에게 왜 이 길을 여는지 말이 남아야 합니다.',
|
|
'황권: 그러면 저는 수량을 맞추고, 마량은 그 수량이 원망이 되지 않게 말을 맞추십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'balance-hanzhong-granary',
|
|
label: '한중 창고 장부를 맞춘다',
|
|
response: '황권은 한중 창고의 군량과 수레 수를 다시 계산했고, 마량은 징발 대신 보상 문서를 함께 붙였습니다.',
|
|
rewardExp: 23
|
|
},
|
|
{
|
|
id: 'write-campaign-proclamation',
|
|
label: '북벌 고시문을 쓴다',
|
|
response: '마량은 북벌의 뜻을 짧게 적었고, 황권은 그 글 아래에 각 고을이 감당할 수 있는 보급 한계를 명확히 남겼습니다.',
|
|
rewardExp: 24
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-prep-wang-madai-ridges',
|
|
title: '산길과 말발굽',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
availableDuringSteps: ['northern-campaign-prep-camp'],
|
|
unitIds: ['wang-ping', 'ma-dai'],
|
|
bondId: 'wang-ping__ma-dai_northern-prep',
|
|
rewardExp: 66,
|
|
lines: [
|
|
'왕평: 북쪽 산길은 지도보다 좁고, 좁은 길은 기병에게도 보병에게도 함정이 됩니다.',
|
|
'마대: 그렇다면 말이 달릴 길보다 멈출 곳을 먼저 익히겠습니다.',
|
|
'왕평: 산길을 아는 자가 앞을 보고, 말을 아는 자가 뒤를 닫으면 본대가 길을 잃지 않을 것입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-ridge-halts',
|
|
label: '고개 정지선을 표시한다',
|
|
response: '왕평은 좁은 고개마다 정지선을 표시했고, 마대는 기병이 그 선에서 멈추는 짧은 훈련을 반복했습니다.',
|
|
rewardExp: 22
|
|
},
|
|
{
|
|
id: 'test-scout-rotation',
|
|
label: '정찰 교대 순서를 시험한다',
|
|
response: '마대는 기병 정찰의 교대 시간을 줄였고, 왕평은 산길마다 보병이 이어받을 위치를 정했습니다.',
|
|
rewardExp: 21
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-first-zhuge-zhao-route',
|
|
title: '기산 길목의 속도',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFifth],
|
|
unitIds: ['zhuge-liang', 'zhao-yun'],
|
|
bondId: 'zhuge-liang__zhao-yun_northern-first',
|
|
rewardExp: 82,
|
|
lines: [
|
|
'조운: 승상, 길을 열었으나 위군의 반응이 생각보다 빠릅니다. 다음 전장에서는 더 넓게 보고 먼저 움직여야겠습니다.',
|
|
'제갈량: 그래서 자룡의 기동이 필요합니다. 빠른 창은 무모함이 아니라, 돌아올 길을 알고 움직일 때 비로소 책략이 됩니다.',
|
|
'조운: 그렇다면 다음에는 제가 먼저 위군의 눈을 흔들겠습니다. 본대가 늦지 않도록 길을 표시해 두겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-qishan-return-road',
|
|
label: '퇴로 표식을 먼저 세운다',
|
|
response: '제갈량은 지형도를 펼쳤고, 조운은 돌무더기와 깃발로 병사들이 알아볼 수 있는 퇴로 표식을 정했습니다.',
|
|
rewardExp: 28
|
|
},
|
|
{
|
|
id: 'assign-mobile-vanguard',
|
|
label: '기동 선봉을 따로 둔다',
|
|
response: '조운은 빠른 병사들을 묶어 기동 선봉을 만들었고, 제갈량은 그 선봉이 본대와 끊어지지 않도록 신호를 맞췄습니다.',
|
|
rewardExp: 27
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-first-huang-ma-supply',
|
|
title: '장부와 민심의 길',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFifth],
|
|
unitIds: ['huang-quan', 'ma-liang'],
|
|
bondId: 'huang-quan__ma-liang_northern-first',
|
|
rewardExp: 78,
|
|
lines: [
|
|
'황권: 장부상으로는 길을 열었지만, 마을이 불안하면 보급은 오래 버티지 못합니다.',
|
|
'마량: 백성이 먼저 믿어야 수레가 지나갑니다. 오늘 확보한 마을에 약속을 남겨 두겠습니다.',
|
|
'황권: 그러면 저는 그 약속을 군량 배분표에 반영하겠습니다. 글과 곡식이 함께 가야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'balance-village-rations',
|
|
label: '마을 보급 배분을 조정한다',
|
|
response: '황권은 군량 배분표를 고쳤고, 마량은 마을마다 필요한 물자를 직접 확인해 장부 옆에 적었습니다.',
|
|
rewardExp: 26
|
|
},
|
|
{
|
|
id: 'write-northern-proclamation',
|
|
label: '북벌 고시문을 새긴다',
|
|
response: '마량은 북벌의 뜻을 부드럽게 풀어 썼고, 황권은 고시문 아래에 실제 지급 물자를 빠짐없이 적었습니다.',
|
|
rewardExp: 25
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-first-wang-madai-ridge',
|
|
title: '산길의 말발굽',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFifth],
|
|
unitIds: ['wang-ping', 'ma-dai'],
|
|
bondId: 'wang-ping__ma-dai_northern-first',
|
|
rewardExp: 74,
|
|
lines: [
|
|
'왕평: 산길은 넓어 보이는 곳보다 좁아지는 곳이 더 위험합니다. 적이 노릴 곳은 우리가 빨리 지나간 곳입니다.',
|
|
'마대: 그렇다면 제가 지나간 길을 다시 돌아보겠습니다. 말이 빠르게 달릴 수 없는 곳은 위군도 함부로 치고 나오기 어렵겠지요.',
|
|
'왕평: 좋습니다. 고개마다 멈출 자리를 정하고, 기병이 돌아올 길을 먼저 남겨 둡시다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-ridge-stopping-points',
|
|
label: '능선 정지점을 표시한다',
|
|
response: '왕평은 능선마다 정지점을 표시했고, 마대는 기병이 그 지점에서 멈추고 돌아서는 훈련을 반복했습니다.',
|
|
rewardExp: 24
|
|
},
|
|
{
|
|
id: 'rotate-mounted-scouts',
|
|
label: '기병 정찰 교대를 짠다',
|
|
response: '마대는 정찰 기병을 나누어 교대 시간을 줄였고, 왕평은 각 교대 지점마다 보병 엄호 위치를 정했습니다.',
|
|
rewardExp: 23
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-second-zhuge-wang-jieting',
|
|
title: '가정의 진영 위치',
|
|
availableAfterBattleIds: [campBattleIds.fiftySixth],
|
|
unitIds: ['zhuge-liang', 'wang-ping'],
|
|
bondId: 'zhuge-liang__wang-ping_northern-second',
|
|
rewardExp: 86,
|
|
lines: [
|
|
'왕평: 승상, 가정으로 이어지는 고개는 높은 곳만 보고 진영을 잡으면 물길이 멉니다.',
|
|
'제갈량: 산을 얻고도 군량과 물을 잃으면 승리의 모양만 남고 전선은 무너집니다.',
|
|
'왕평: 다음 전장에서는 길과 물을 함께 보겠습니다. 고집이 앞서면 제가 먼저 막겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-waterline-before-ridge',
|
|
label: '물길을 먼저 표시한다',
|
|
response: '왕평은 고개 아래 물길을 지도에 표시했고, 제갈량은 진영 후보지를 그 물길과 함께 다시 검토했습니다.',
|
|
rewardExp: 30
|
|
},
|
|
{
|
|
id: 'assign-ridge-observers',
|
|
label: '능선 관측병을 세운다',
|
|
response: '제갈량은 고개마다 관측병을 두었고, 왕평은 관측병이 물러날 길까지 함께 정했습니다.',
|
|
rewardExp: 28
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-second-zhao-madai-flank',
|
|
title: '우회 기병의 신호',
|
|
availableAfterBattleIds: [campBattleIds.fiftySixth],
|
|
unitIds: ['zhao-yun', 'ma-dai'],
|
|
bondId: 'zhao-yun__ma-dai_northern-second',
|
|
rewardExp: 82,
|
|
lines: [
|
|
'마대: 천수의 길은 말이 달릴 수 있는 곳과 달릴 수 없는 곳이 번갈아 나옵니다.',
|
|
'조운: 그러니 빠른 부대일수록 멈출 곳을 알아야 합니다. 앞서가는 창은 돌아올 길을 잃지 않아야 하오.',
|
|
'마대: 다음에는 말발굽 소리보다 깃발 신호를 먼저 보겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'set-flank-signal-flags',
|
|
label: '우회 신호 깃발을 맞춘다',
|
|
response: '조운과 마대는 기병 신호 깃발을 맞추었고, 정찰대는 멀리 돌더라도 본대와 끊어지지 않게 되었습니다.',
|
|
rewardExp: 27
|
|
},
|
|
{
|
|
id: 'rotate-horse-scouts',
|
|
label: '기병 정찰을 교대한다',
|
|
response: '마대는 말의 숨이 찰 때마다 교대할 지점을 정했고, 조운은 그 지점마다 엄호병을 붙였습니다.',
|
|
rewardExp: 26
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-second-ma-fa-jiangwei',
|
|
title: '강유라는 이름',
|
|
availableAfterBattleIds: [campBattleIds.fiftySixth],
|
|
unitIds: ['ma-liang', 'fa-zheng'],
|
|
bondId: 'ma-liang__fa-zheng_northern-second',
|
|
rewardExp: 78,
|
|
lines: [
|
|
'마량: 강유는 전장에서 끝까지 물러나지 않았지만, 백성을 해치며 버티지는 않았습니다.',
|
|
'법정: 그런 장수는 칼로만 꺾기 아깝습니다. 의심과 명분 사이에 들어갈 말이 필요합니다.',
|
|
'마량: 그렇다면 다음에는 먼저 글을 준비하겠습니다. 항복을 요구하기보다 길을 묻는 글이어야 하겠지요.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-jiangwei-letter',
|
|
label: '강유에게 보낼 글을 쓴다',
|
|
response: '마량은 부드러운 문장을 고르고, 법정은 그 문장 안에 위군의 의심을 흔들 작은 틈을 남겼습니다.',
|
|
rewardExp: 25
|
|
},
|
|
{
|
|
id: 'collect-tianshui-rumors',
|
|
label: '천수 소문을 모은다',
|
|
response: '법정은 천수 장수들의 평판을 모았고, 마량은 강유가 백성에게 어떤 이름으로 불리는지 따로 적었습니다.',
|
|
rewardExp: 24
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-third-zhuge-wang-after-jieting',
|
|
title: '가정의 물길',
|
|
availableAfterBattleIds: [campBattleIds.fiftySeventh],
|
|
unitIds: ['zhuge-liang', 'wang-ping'],
|
|
bondId: 'zhuge-liang__wang-ping_northern-third',
|
|
rewardExp: 90,
|
|
lines: [
|
|
'왕평: 승상, 고지를 얻고도 물길을 잃는다면 병사는 하루 만에 마음을 잃습니다.',
|
|
'제갈량: 오늘 그 차이를 보았습니다. 지도 위의 높은 점보다 병사가 버틸 낮은 길이 더 무거울 때가 있습니다.',
|
|
'왕평: 다음에는 진영을 세우기 전에 물, 수레, 퇴로를 먼저 세겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-jieting-water-rule',
|
|
label: '물길 배치 원칙을 적는다',
|
|
response: '제갈량은 왕평의 말을 군령으로 정리했고, 다음 북벌의 진영 후보에는 물길 표시가 먼저 붙게 되었습니다.',
|
|
rewardExp: 31
|
|
},
|
|
{
|
|
id: 'train-ridge-retreat',
|
|
label: '고지 퇴로 훈련을 한다',
|
|
response: '왕평은 고지에서 내려오는 길을 반복해 걷게 했고, 병사들은 높이 오르는 일보다 내려와 버티는 일을 먼저 배웠습니다.',
|
|
rewardExp: 29
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-third-ma-jiangwei-oath',
|
|
title: '강유의 첫 밤',
|
|
availableAfterBattleIds: [campBattleIds.fiftySeventh],
|
|
unitIds: ['ma-liang', 'jiang-wei'],
|
|
bondId: 'ma-liang__jiang-wei',
|
|
rewardExp: 82,
|
|
lines: [
|
|
'강유: 제 이름이 촉한의 장부에 적혔지만, 아직 병사들의 눈에는 어제의 적일 것입니다.',
|
|
'마량: 그래서 글보다 행동이 먼저일 때가 있습니다. 하지만 첫 행동이 흔들리지 않게 말이 길을 잡아 줄 수 있습니다.',
|
|
'강유: 천수의 길과 사람을 알고 있습니다. 제가 아는 것을 숨기지 않겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-jiangwei-first-duty',
|
|
label: '강유의 첫 임무를 적는다',
|
|
response: '마량은 강유의 첫 임무를 천수 길 안내와 병사 안심으로 나누어 적었고, 새 장수의 자리가 조금 선명해졌습니다.',
|
|
rewardExp: 27
|
|
},
|
|
{
|
|
id: 'share-tianshui-roads',
|
|
label: '천수 길을 함께 그린다',
|
|
response: '강유는 천수 사람들이 실제로 쓰는 낮은 길을 표시했고, 마량은 그 길이 군대의 길이 되지 않도록 주의문을 붙였습니다.',
|
|
rewardExp: 26
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-third-zhao-madai-recovery',
|
|
title: '되돌아오는 말발굽',
|
|
availableAfterBattleIds: [campBattleIds.fiftySeventh],
|
|
unitIds: ['zhao-yun', 'ma-dai'],
|
|
bondId: 'zhao-yun__ma-dai_northern-third',
|
|
rewardExp: 80,
|
|
lines: [
|
|
'조운: 가정에서는 빠른 말보다 돌아오는 말이 더 귀했습니다.',
|
|
'마대: 좁은 고개에서는 이기는 길과 살아 돌아오는 길이 다르지 않았습니다.',
|
|
'조운: 다음 출전에서도 기병은 먼저 멈출 자리를 알아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'set-return-posts',
|
|
label: '귀환 표식을 세운다',
|
|
response: '조운과 마대는 고개마다 돌아올 표식을 세웠고, 기병대는 돌파 뒤 흩어지지 않는 훈련을 익혔습니다.',
|
|
rewardExp: 26
|
|
},
|
|
{
|
|
id: 'pair-scout-and-reserve',
|
|
label: '정찰과 예비대를 묶는다',
|
|
response: '마대가 정찰을 내보내면 조운이 예비대를 가까이 붙이는 방식으로, 기병 운용의 무리한 빈틈이 줄었습니다.',
|
|
rewardExp: 25
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fourth-zhuge-jiangwei-first-road',
|
|
title: '새 장수에게 맡긴 길',
|
|
availableAfterBattleIds: [campBattleIds.fiftyEighth],
|
|
unitIds: ['zhuge-liang', 'jiang-wei'],
|
|
bondId: 'zhuge-liang__jiang-wei_northern-fourth',
|
|
rewardExp: 92,
|
|
lines: [
|
|
'강유: 승상, 오늘은 진군이 아니라 후퇴를 열었습니다. 제가 촉한에 온 첫 전공이 물러나는 길이라 마음이 무겁습니다.',
|
|
'제갈량: 병사를 살리고 장부를 지키는 일도 전공입니다. 나아갈 길을 아는 장수라면 물러날 길 또한 알아야 합니다.',
|
|
'강유: 다음 북방 길에서는 먼저 병사의 숨을 보겠습니다. 제가 아는 천수의 길을 촉한의 길로 만들겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'entrust-low-road',
|
|
label: '낮은 길을 맡긴다',
|
|
response: '제갈량은 강유에게 천수와 기산을 잇는 낮은 길 정리를 맡겼고, 강유는 새 장수로서 첫 책임을 받아들였습니다.',
|
|
rewardExp: 31
|
|
},
|
|
{
|
|
id: 'record-first-duty',
|
|
label: '첫 임무를 장부에 남긴다',
|
|
response: '강유의 첫 임무가 군량 수레와 병사 회수로 적혔습니다. 새 이름은 전공보다 책임으로 먼저 자리 잡았습니다.',
|
|
rewardExp: 29
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fourth-jiangwei-wangping-low-water',
|
|
title: '낮은 길과 물길',
|
|
availableAfterBattleIds: [campBattleIds.fiftyEighth],
|
|
unitIds: ['jiang-wei', 'wang-ping'],
|
|
bondId: 'jiang-wei__wang-ping_northern-fourth',
|
|
rewardExp: 88,
|
|
lines: [
|
|
'왕평: 천수의 낮은 길은 지도보다 사람의 발이 먼저 기억합니다. 오늘 강유 장군의 말이 늦었다면 수레가 막혔을 겁니다.',
|
|
'강유: 왕평 장군의 물길 표시가 없었다면 제가 아는 길도 군대의 길이 되지 못했습니다.',
|
|
'왕평: 다음에는 낮은 길과 물길을 한 장에 묶읍시다. 고지만 보는 실수는 다시 하지 않아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'merge-low-road-waterline',
|
|
label: '낮은 길과 물길을 합친다',
|
|
response: '왕평과 강유는 낮은 길과 물길을 한 장의 지도에 묶었고, 기산 후퇴의 교훈은 다음 북벌의 표준이 되었습니다.',
|
|
rewardExp: 30
|
|
},
|
|
{
|
|
id: 'train-cart-turns',
|
|
label: '수레 회전 지점을 훈련한다',
|
|
response: '강유가 길을 읽고 왕평이 멈출 자리를 정하자, 군량 수레는 좁은 길에서도 흩어지지 않는 법을 익혔습니다.',
|
|
rewardExp: 28
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fourth-zhao-madai-jiangwei-return',
|
|
title: '기병의 귀환선',
|
|
availableAfterBattleIds: [campBattleIds.fiftyEighth],
|
|
unitIds: ['jiang-wei', 'zhao-yun'],
|
|
bondId: 'jiang-wei__zhao-yun_northern-fourth',
|
|
rewardExp: 86,
|
|
lines: [
|
|
'조운: 강유 장군, 빠른 길을 아는 것과 빠르게 돌아오는 것은 다릅니다. 오늘 그 차이를 함께 보았습니다.',
|
|
'강유: 자룡 장군의 회수 신호가 없었다면 제가 아는 길도 너무 깊은 길이 되었을 겁니다.',
|
|
'조운: 다음에는 마대와도 신호를 맞추십시오. 북방의 기병은 앞서가는 만큼 돌아오는 법을 알아야 합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-return-flags',
|
|
label: '귀환 깃발을 맞춘다',
|
|
response: '조운은 귀환 깃발의 순서를 강유에게 알려 주었고, 강유는 천수 길목마다 같은 표식을 남기기로 했습니다.',
|
|
rewardExp: 29
|
|
},
|
|
{
|
|
id: 'compare-mounted-routes',
|
|
label: '기병로를 비교한다',
|
|
response: '조운의 회수로, 마대의 우회로, 강유의 천수 낮은 길이 하나의 기병 운용표로 정리되었습니다.',
|
|
rewardExp: 27
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fifth-jiangwei-fazheng-wall',
|
|
title: '성벽을 재는 계책',
|
|
availableAfterBattleIds: [campBattleIds.fiftyNinth],
|
|
unitIds: ['jiang-wei', 'fa-zheng'],
|
|
bondId: 'jiang-wei__fa-zheng_northern-fifth',
|
|
rewardExp: 94,
|
|
lines: [
|
|
'강유: 진창의 성벽은 제가 알던 길과 달랐습니다. 길은 찾을 수 있지만, 학소 같은 수비장은 마음을 쉽게 비우지 않습니다.',
|
|
'법정: 그래서 성벽을 깨기 전에 성벽이 버티는 까닭을 보아야 합니다. 사람, 군량, 봉화, 명분 중 어느 하나라도 흔들리면 문은 칼보다 먼저 약해집니다.',
|
|
'강유: 다음에는 제가 길만 말하지 않겠습니다. 성이 버티는 시간을 함께 계산하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-wall-timing',
|
|
label: '성벽의 시간을 적는다',
|
|
response: '강유는 진창 성벽의 수비 교대와 봉화 시간을 기록했고, 법정은 다음 공성 계책의 기준으로 삼았습니다.',
|
|
rewardExp: 32
|
|
},
|
|
{
|
|
id: 'compare-weak-points',
|
|
label: '약한 지점을 비교한다',
|
|
response: '법정은 성문보다 먼저 보급막과 봉화대를 짚었고, 강유는 그 지점을 잇는 낮은 길을 덧그렸습니다.',
|
|
rewardExp: 30
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fifth-weiyan-jiangwei-restraint',
|
|
title: '돌파와 절제',
|
|
availableAfterBattleIds: [campBattleIds.fiftyNinth],
|
|
unitIds: ['wei-yan', 'jiang-wei'],
|
|
bondId: 'wei-yan__jiang-wei_northern-fifth',
|
|
rewardExp: 90,
|
|
lines: [
|
|
'위연: 성이 굳다고 물러서기만 하면 북벌은 늘 장부 위에서 끝날 것이오. 한 번은 목을 걸고 찔러야 합니다.',
|
|
'강유: 찌를 곳을 아는 것과 돌아올 길을 버리는 것은 다릅니다. 오늘 왕쌍이 뒤를 물었을 때 그 차이가 보였습니다.',
|
|
'위연: 좋소. 다음에는 그대가 길을 보시오. 나는 그 길이 진짜라면 가장 먼저 들어가겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-breakthrough-to-return',
|
|
label: '돌파와 귀환을 묶는다',
|
|
response: '강유는 위연의 돌파로에 귀환 표식을 함께 그렸고, 위연은 다음 출전에서 무작정 깊이 들어가지 않겠다고 답했습니다.',
|
|
rewardExp: 31
|
|
},
|
|
{
|
|
id: 'test-forward-camp',
|
|
label: '전진 진영을 시험한다',
|
|
response: '위연의 전진 진영과 강유의 낮은 길이 함께 검토되며, 다음 북방 전장의 공격적인 선택지가 살아났습니다.',
|
|
rewardExp: 29
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fifth-huangquan-maliang-ledger',
|
|
title: '군량과 문서',
|
|
availableAfterBattleIds: [campBattleIds.fiftyNinth],
|
|
unitIds: ['huang-quan', 'ma-liang'],
|
|
bondId: 'huang-quan__ma-liang_northern-fifth',
|
|
rewardExp: 88,
|
|
lines: [
|
|
'황권: 진창 앞에서 하루를 더 버티려면, 한중 창고에서는 사흘을 먼저 움직여야 합니다.',
|
|
'마량: 병사들은 성이 열리지 않은 까닭을 패배로만 듣지 않아야 합니다. 돌아온 이유를 문서로 남기겠습니다.',
|
|
'황권: 숫자와 말이 함께 있어야 다음 북벌이 조급해지지 않습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-chencang-ledger',
|
|
label: '진창 장부를 봉한다',
|
|
response: '황권은 소모된 군량과 지킨 수레를 따로 적었고, 마량은 진창 철수의 이유를 군영 고시문으로 정리했습니다.',
|
|
rewardExp: 30
|
|
},
|
|
{
|
|
id: 'write-regroup-notice',
|
|
label: '재정비 고시문을 쓴다',
|
|
response: '마량은 병사들이 낙담하지 않도록 진창에서 얻은 교훈을 적었고, 황권은 다음 지급 기준을 그 아래에 붙였습니다.',
|
|
rewardExp: 28
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-sixth-jiangwei-weiyan-pass',
|
|
title: '열린 길과 멈출 선',
|
|
availableAfterBattleIds: [campBattleIds.sixtieth],
|
|
unitIds: ['jiang-wei', 'wei-yan'],
|
|
bondId: 'jiang-wei__wei-yan_northern-sixth',
|
|
rewardExp: 96,
|
|
lines: [
|
|
'위연: 이번에는 길이 열렸소. 진창 앞에서 멈춘 답답함을 무도 고개에서 조금은 풀었습니다.',
|
|
'강유: 길이 열렸기에 더 조심해야 합니다. 한 번 열렸다고 다음 길도 같은 모양은 아닙니다.',
|
|
'위연: 좋소. 다음에는 그대가 멈출 선을 말하시오. 나는 그 선이 보일 때까지 먼저 들어가겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-breakthrough-return-line',
|
|
label: '돌파 귀환선을 표시한다',
|
|
response: '강유는 위연의 돌파 지점마다 돌아올 표식을 그렸고, 위연은 그 표식이 공격을 늦추는 것이 아니라 살리는 것임을 받아들였습니다.',
|
|
rewardExp: 32
|
|
},
|
|
{
|
|
id: 'test-fast-pass-entry',
|
|
label: '빠른 고개 진입을 시험한다',
|
|
response: '위연은 빠르게 고개를 찌르는 훈련을 맡았고, 강유는 진입 뒤 본대와 다시 붙는 길을 함께 적었습니다.',
|
|
rewardExp: 30
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-sixth-zhao-madai-cavalry',
|
|
title: '두 기병의 회수선',
|
|
availableAfterBattleIds: [campBattleIds.sixtieth],
|
|
unitIds: ['zhao-yun', 'ma-dai'],
|
|
bondId: 'zhao-yun__ma-dai_northern-sixth',
|
|
rewardExp: 92,
|
|
lines: [
|
|
'마대: 곽회의 기병은 성급하지 않았습니다. 물러나는 척하며 다시 물목을 노렸습니다.',
|
|
'조운: 그래서 기병은 이기는 속도보다 돌아오는 속도를 먼저 알아야 하오.',
|
|
'마대: 다음에는 우회로를 넓히기 전에 돌아올 신호부터 맞추겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'align-cavalry-recall-flags',
|
|
label: '기병 회수 깃발을 맞춘다',
|
|
response: '조운과 마대는 회수 깃발 순서를 맞추었고, 기병대는 깊이 들어가도 본대의 신호를 놓치지 않게 되었습니다.',
|
|
rewardExp: 31
|
|
},
|
|
{
|
|
id: 'map-yinping-flank',
|
|
label: '음평 우회로를 그린다',
|
|
response: '마대는 음평 물목의 우회로를 그렸고, 조운은 그 길마다 예비대가 붙을 자리를 표시했습니다.',
|
|
rewardExp: 29
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-sixth-wangping-huangquan-ledger',
|
|
title: '산길과 장부',
|
|
availableAfterBattleIds: [campBattleIds.sixtieth],
|
|
unitIds: ['wang-ping', 'huang-quan'],
|
|
bondId: 'wang-ping__huang-quan_northern-sixth',
|
|
rewardExp: 90,
|
|
lines: [
|
|
'왕평: 두 고을을 얻었지만 산길 표식이 흐려지면 얻은 땅도 하루 만에 멀어집니다.',
|
|
'황권: 장부도 같습니다. 무도와 음평으로 나뉜 군량을 한 장부처럼 움직이게 해야 합니다.',
|
|
'왕평: 그러면 저는 고개마다 멈출 자리를 세우겠습니다. 황권 장군은 그 자리마다 며칠을 버틸 수 있는지 적어 주십시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pair-ridge-posts-ledgers',
|
|
label: '고개 표식과 장부를 묶는다',
|
|
response: '왕평의 고개 표식 옆에 황권의 군량 날짜가 붙었습니다. 병사들은 어디서 멈추고 얼마나 버틸지 한눈에 알게 되었습니다.',
|
|
rewardExp: 30
|
|
},
|
|
{
|
|
id: 'audit-two-commandery-grain',
|
|
label: '두 고을 군량을 대조한다',
|
|
response: '황권은 무도와 음평의 군량을 따로 세고, 왕평은 수레가 오갈 수 있는 고개만 골라 장부 옆에 표시했습니다.',
|
|
rewardExp: 28
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-seventh-liyan-huangquan-supply',
|
|
title: '빗길 장부',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFirst],
|
|
unitIds: ['li-yan', 'huang-quan'],
|
|
bondId: 'li-yan__huang-quan_northern-seventh',
|
|
rewardExp: 98,
|
|
lines: [
|
|
'이엄: 비가 오면 군량은 명령보다 늦게 움직입니다. 늦은 수레를 죄로만 보면 다음 수레는 더 늦어집니다.',
|
|
'황권: 그래서 날짜와 고개를 함께 적어야 합니다. 어느 길이 막혔고 어느 길을 남겼는지 장부가 말해야 합니다.',
|
|
'이엄: 좋습니다. 다음에는 변명보다 먼저 도착할 장부를 만들겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pair-rain-dates-ledgers',
|
|
label: '빗길 날짜와 장부를 묶는다',
|
|
response: '이엄은 수레가 늦어진 고개마다 날짜를 붙였고, 황권은 그 날짜를 다음 출정 가능량 옆에 옮겨 적었습니다.',
|
|
rewardExp: 34
|
|
},
|
|
{
|
|
id: 'mark-spare-grain-posts',
|
|
label: '예비 군량 표식을 세운다',
|
|
response: '황권은 한중 창고의 남은 군량을 나누었고, 이엄은 비가 그쳤을 때 가장 먼저 움직일 수레 자리를 따로 표시했습니다.',
|
|
rewardExp: 32
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-seventh-jiangwei-wangping-road',
|
|
title: '지킬 길과 버릴 길',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFirst],
|
|
unitIds: ['jiang-wei', 'wang-ping'],
|
|
bondId: 'jiang-wei__wang-ping_northern-seventh',
|
|
rewardExp: 96,
|
|
lines: [
|
|
'강유: 빗길에서는 빠른 길이 늘 옳지 않았습니다. 먼저 열린 길이 먼저 무너지는 곳도 있었습니다.',
|
|
'왕평: 그래서 표식은 전진보다 퇴각을 먼저 생각해야 합니다. 돌아올 길을 알아야 다음에 다시 나갑니다.',
|
|
'강유: 다음 북벌에서는 제가 보는 길마다 장군의 표식을 먼저 묻겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-return-first-roads',
|
|
label: '돌아올 길부터 표시한다',
|
|
response: '강유는 공격로 앞에 왕평의 후퇴 표식을 먼저 세웠고, 왕평은 그 길을 다음 출정 장부의 첫 줄에 적었습니다.',
|
|
rewardExp: 33
|
|
},
|
|
{
|
|
id: 'compare-commandery-paths',
|
|
label: '두 고을 길을 비교한다',
|
|
response: '왕평은 무도와 음평의 물길 표식을 나란히 세웠고, 강유는 어느 길이 공격로가 되고 어느 길이 보급로가 될지 따로 그렸습니다.',
|
|
rewardExp: 31
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-seventh-zhuge-weiyan-restraint',
|
|
title: '돌파와 제동',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFirst],
|
|
unitIds: ['zhuge-liang', 'wei-yan'],
|
|
bondId: 'zhuge-liang__wei-yan_northern-seventh',
|
|
rewardExp: 94,
|
|
lines: [
|
|
'위연: 조진의 본대가 흔들릴 때 더 깊이 찔렀다면 큰 공을 세울 수 있었습니다.',
|
|
'제갈량: 그렇기에 멈추게 했습니다. 오늘의 깊은 공은 내일의 끊긴 수레가 될 수도 있습니다.',
|
|
'위연: 알겠소. 다만 다음에 멈추라면, 어디까지 찌른 뒤 멈출지도 함께 알려 주시오.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'set-breakthrough-limit',
|
|
label: '돌파 한계선을 정한다',
|
|
response: '제갈량은 위연의 반격 한계선을 지도로 그었고, 위연은 그 선을 후퇴가 아니라 다음 돌파를 위한 약속으로 받아들였습니다.',
|
|
rewardExp: 32
|
|
},
|
|
{
|
|
id: 'reserve-counterattack-signal',
|
|
label: '반격 신호를 남긴다',
|
|
response: '위연은 빗길에서도 보이는 깃발 신호를 고쳤고, 제갈량은 그 신호가 보급선과 어긋나지 않도록 장부 옆에 붙였습니다.',
|
|
rewardExp: 30
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eighth-jiangwei-weiyan-forward',
|
|
title: '앞길을 여는 칼끝',
|
|
availableAfterBattleIds: [campBattleIds.sixtySecond],
|
|
unitIds: ['jiang-wei', 'wei-yan'],
|
|
bondId: 'jiang-wei__wei-yan_northern-eighth',
|
|
rewardExp: 100,
|
|
lines: [
|
|
'강유: 노성 앞에서는 빠른 길이 곧 안전한 길은 아니었습니다. 먼저 열어도 돌아올 표식이 없으면 길이 아닙니다.',
|
|
'위연: 그렇다면 다음에는 그대가 길을 고르고 내가 길을 넓히겠소. 대신 멈출 곳도 처음부터 말해 주시오.',
|
|
'강유: 전진선과 회수선을 함께 그리겠습니다. 돌파가 장수의 고집이 아니라 군의 길이 되도록 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-breakthrough-return-line',
|
|
label: '돌파선과 회수선을 함께 긋는다',
|
|
response: '강유는 낮은 길을 지도에 표시했고, 위연은 그 선을 넘어설 때 필요한 깃발 신호를 따로 정했습니다.',
|
|
rewardExp: 35
|
|
},
|
|
{
|
|
id: 'set-lucheng-pressure-rhythm',
|
|
label: '노성 압박 호흡을 맞춘다',
|
|
response: '위연은 성급한 돌파 대신 강유의 정찰 보고가 닿는 박자에 맞춰 전열을 흔들기로 했습니다.',
|
|
rewardExp: 32
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eighth-zhaoyun-wangping-return',
|
|
title: '회수선의 표식',
|
|
availableAfterBattleIds: [campBattleIds.sixtySecond],
|
|
unitIds: ['zhao-yun', 'wang-ping'],
|
|
bondId: 'zhao-yun__wang-ping_northern-eighth',
|
|
rewardExp: 98,
|
|
lines: [
|
|
'조운: 장합의 기병은 길이 열린 순간보다 돌아가는 순간을 노렸습니다.',
|
|
'왕평: 그래서 표식은 앞에만 세우면 안 됩니다. 돌아오는 병사가 어둠 속에서도 볼 수 있어야 합니다.',
|
|
'조운: 다음에는 예비대가 표식을 따라 먼저 움직이겠습니다. 병사들이 표식을 보면 제 깃발도 함께 보게 하겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pair-reserve-with-road-markers',
|
|
label: '예비대와 표식을 묶는다',
|
|
response: '왕평은 산길 표식을 다시 세웠고, 조운은 예비대가 어느 표식을 보면 즉시 움직일지 훈련 명령에 적었습니다.',
|
|
rewardExp: 34
|
|
},
|
|
{
|
|
id: 'test-zhanghe-pursuit-drill',
|
|
label: '장합 추격 대비를 훈련한다',
|
|
response: '조운의 기동대가 일부러 늦게 물러났고, 왕평은 그 길목마다 병사들이 헷갈린 지점을 따로 고쳤습니다.',
|
|
rewardExp: 31
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eighth-huangquan-maliang-ledger',
|
|
title: '장부와 고시문',
|
|
availableAfterBattleIds: [campBattleIds.sixtySecond],
|
|
unitIds: ['huang-quan', 'ma-liang'],
|
|
bondId: 'huang-quan__ma-liang_northern-eighth',
|
|
rewardExp: 96,
|
|
lines: [
|
|
'황권: 공세가 성공해도 장부가 비면 마을은 승리보다 징발을 먼저 기억합니다.',
|
|
'마량: 그래서 고시문은 승전 뒤가 아니라 출정 전부터 준비되어야 합니다. 약속이 늦으면 민심도 늦습니다.',
|
|
'황권: 다음 장부에는 군량 수치 옆에 고시문 날짜를 함께 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-ledger-to-proclamation',
|
|
label: '장부와 고시문 날짜를 묶는다',
|
|
response: '황권은 군량 배분표 옆에 마량의 고시문 날짜를 적었고, 마량은 약속한 배분이 실제 수레에 실리는지 확인했습니다.',
|
|
rewardExp: 33
|
|
},
|
|
{
|
|
id: 'prepare-front-village-reassurance',
|
|
label: '전방 마을 안심 문서를 준비한다',
|
|
response: '마량은 노성 앞 마을에 붙일 문서를 고쳤고, 황권은 그 문서가 지킬 수 있는 군량 수치를 함께 봉했습니다.',
|
|
rewardExp: 30
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-ninth-jiangwei-zhaoyun-return',
|
|
title: '낮은 길과 회수 깃발',
|
|
availableAfterBattleIds: [campBattleIds.sixtyThird],
|
|
unitIds: ['jiang-wei', 'zhao-yun'],
|
|
bondId: 'jiang-wei__zhao-yun_northern-ninth',
|
|
rewardExp: 104,
|
|
lines: [
|
|
'강유: 노성 뒤의 낮은 길은 빠르게 보였지만, 장합이 기다린 곳도 그 길이었습니다.',
|
|
'조운: 그래서 빠른 길에는 반드시 돌아올 깃발이 따라야 합니다. 앞선 부대가 보지 못한 뒤를 예비대가 보겠습니다.',
|
|
'강유: 다음 지도에는 길의 이름뿐 아니라 돌아오는 깃발의 순서까지 함께 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-low-road-to-reserve-flag',
|
|
label: '낮은 길과 예비 깃발을 묶는다',
|
|
response: '강유는 낮은 길마다 표식을 새겼고, 조운은 예비대가 어느 깃발부터 움직일지 훈련 명령으로 남겼습니다.',
|
|
rewardExp: 36
|
|
},
|
|
{
|
|
id: 'test-return-road-drill',
|
|
label: '회수로 훈련을 다시 돈다',
|
|
response: '조운의 기동대가 일부러 늦게 물러났고, 강유는 병사들이 헷갈린 낮은 길을 지도에서 다시 고쳤습니다.',
|
|
rewardExp: 33
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-ninth-weiyan-wangping-stopline',
|
|
title: '칼끝의 정지선',
|
|
availableAfterBattleIds: [campBattleIds.sixtyThird],
|
|
unitIds: ['wei-yan', 'wang-ping'],
|
|
bondId: 'wei-yan__wang-ping_northern-ninth',
|
|
rewardExp: 102,
|
|
lines: [
|
|
'위연: 이번에는 멈출 곳을 알았기에 더 세게 찌를 수 있었소.',
|
|
'왕평: 표식이 없었다면 돌파는 길이 아니라 절벽이 되었을 것입니다.',
|
|
'위연: 다음에도 선을 그어 주시오. 나는 그 선까지 가장 빠르게 가겠소.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'paint-breakthrough-stopline',
|
|
label: '돌파 정지선을 지도에 칠한다',
|
|
response: '왕평은 노성 추격로의 정지선을 붉게 칠했고, 위연은 그 선을 넘을 때 필요한 별도 명령을 요구했습니다.',
|
|
rewardExp: 35
|
|
},
|
|
{
|
|
id: 'reserve-after-breakthrough',
|
|
label: '돌파 뒤 예비대를 붙인다',
|
|
response: '위연은 돌파 뒤 빈틈을 인정했고, 왕평은 그 뒤를 메울 예비대 위치를 군령에 더했습니다.',
|
|
rewardExp: 32
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-ninth-huangquan-liyan-carts',
|
|
title: '장부와 수레의 속도',
|
|
availableAfterBattleIds: [campBattleIds.sixtyThird],
|
|
unitIds: ['huang-quan', 'li-yan'],
|
|
bondId: 'huang-quan__li-yan_northern-ninth',
|
|
rewardExp: 100,
|
|
lines: [
|
|
'황권: 추격이 빨라질수록 장부는 늦어집니다. 늦어진 장부는 결국 병사의 빈 그릇으로 돌아옵니다.',
|
|
'이엄: 수레가 늦은 곳을 숨기지 않겠습니다. 늦은 길을 알아야 다음 수레가 무리하지 않습니다.',
|
|
'황권: 그럼 이번 장부에는 승리한 길과 늦어진 길을 나란히 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pair-ledger-with-cart-delay',
|
|
label: '장부와 수레 지연을 묶는다',
|
|
response: '황권은 군량 소모 옆에 이엄의 수레 지연 시간을 적었고, 두 사람은 다음 출정의 실제 속도를 다시 계산했습니다.',
|
|
rewardExp: 34
|
|
},
|
|
{
|
|
id: 'reserve-lucheng-cart-team',
|
|
label: '노성 예비 수레를 남긴다',
|
|
response: '이엄은 노성 주변에 예비 수레를 남겼고, 황권은 그 수레가 마을 몫을 침범하지 않도록 장부에 못박았습니다.',
|
|
rewardExp: 31
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-tenth-jiangwei-wangping-riverline',
|
|
title: '물목과 정지선',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFourth],
|
|
unitIds: ['jiang-wei', 'wang-ping'],
|
|
bondId: 'jiang-wei__wang-ping_northern-tenth',
|
|
rewardExp: 108,
|
|
lines: [
|
|
'강유: 위수 물목은 길처럼 보였지만, 멈출 곳을 모르면 물길이 적의 칼이 됩니다.',
|
|
'왕평: 물목마다 정지선을 세우겠습니다. 빨리 건너는 길보다 돌아올 수 있는 길이 먼저입니다.',
|
|
'강유: 다음 지도에는 낮은 길과 정지선을 같은 색으로 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-crossing-to-stopline',
|
|
label: '물목과 정지선을 묶는다',
|
|
response: '강유는 낮은 물목을 지도에 옮겼고, 왕평은 그 길마다 멈출 깃발과 회수 순서를 새겼습니다.',
|
|
rewardExp: 38
|
|
},
|
|
{
|
|
id: 'drill-river-return-markers',
|
|
label: '강가 회수 표식을 다시 훈련한다',
|
|
response: '왕평은 병사들을 일부러 강안 안개 속으로 돌려 보냈고, 강유는 헷갈린 표식을 다시 낮은 길 기준으로 고쳤습니다.',
|
|
rewardExp: 35
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-tenth-zhuge-huangquan-ledger',
|
|
title: '강가의 장부',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFourth],
|
|
unitIds: ['zhuge-liang', 'huang-quan'],
|
|
bondId: 'zhuge-liang__huang-quan_northern-tenth',
|
|
rewardExp: 106,
|
|
lines: [
|
|
'황권: 위수 진영을 얻으면 기세는 오르지만, 수레가 길어지는 만큼 장부는 더 냉정해야 합니다.',
|
|
'제갈량: 이긴 길을 오래 지킬 수 없다면 다음 명령은 병사를 지치게 할 뿐입니다.',
|
|
'황권: 그러면 이번 장부에는 진영마다 버틸 날짜와 물릴 날짜를 함께 적겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-camp-duration-ledger',
|
|
label: '진영 지속 날짜를 장부에 적는다',
|
|
response: '황권은 위수 진영마다 버틸 수 있는 날짜를 적었고, 제갈량은 그 숫자에 맞춰 다음 군령의 폭을 줄였습니다.',
|
|
rewardExp: 36
|
|
},
|
|
{
|
|
id: 'reserve-weishui-cart-line',
|
|
label: '위수 예비 수레선을 남긴다',
|
|
response: '제갈량은 예비 수레선을 후방에 남겼고, 황권은 그 수레가 마을 몫을 건드리지 않게 다시 계산했습니다.',
|
|
rewardExp: 34
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eleventh-jiangwei-madai-flank',
|
|
title: '낮은 길과 측면',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFifth],
|
|
unitIds: ['jiang-wei', 'ma-dai'],
|
|
bondId: 'jiang-wei__ma-dai_northern-eleventh',
|
|
rewardExp: 112,
|
|
lines: [
|
|
'강유: 북안 낮은 길은 장합의 재추격이 먼저 지나갈 길이기도 했습니다.',
|
|
'마대: 그래서 옆을 물어야 했습니다. 적이 빠른 길을 믿을수록 측면이 먼저 흔들립니다.',
|
|
'강유: 다음 지도에는 빠른 길과 물어야 할 옆길을 같은 먹으로 남기겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-low-road-flanks',
|
|
label: '낮은 길의 측면 표식을 남긴다',
|
|
response: '강유는 낮은 물목을 다시 그렸고, 마대는 그 길마다 기병이 물 측면 표식을 덧붙였습니다.',
|
|
rewardExp: 40
|
|
},
|
|
{
|
|
id: 'delay-rearguard-cavalry',
|
|
label: '재추격 기병을 늦춘다',
|
|
response: '마대는 재추격 기병이 모였던 길을 기억했고, 강유는 다음 출정에서 그 길을 먼저 끊는 군령을 적었습니다.',
|
|
rewardExp: 36
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eleventh-zhuge-liyan-carts',
|
|
title: '군령과 수레',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFifth],
|
|
unitIds: ['zhuge-liang', 'li-yan'],
|
|
bondId: 'zhuge-liang__li-yan_northern-eleventh',
|
|
rewardExp: 110,
|
|
lines: [
|
|
'이엄: 북안 진영을 얻은 뒤에도 수레가 따라오지 못하면, 군령은 종이 위에서만 이깁니다.',
|
|
'제갈량: 그래서 오늘의 명령에는 나아갈 곳만이 아니라 물릴 곳도 적어야 합니다.',
|
|
'이엄: 다음에는 승리한 진영마다 수레를 붙일지, 깃발만 남길지 먼저 묻겠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-order-to-cart-line',
|
|
label: '군령과 수레선을 묶는다',
|
|
response: '제갈량은 전진 명령 옆에 수레가 닿을 날짜를 적었고, 이엄은 그보다 늦는 진영을 예비 표시로 돌렸습니다.',
|
|
rewardExp: 38
|
|
},
|
|
{
|
|
id: 'choose-retreatable-camps',
|
|
label: '물릴 수 있는 진영을 고른다',
|
|
response: '이엄은 물릴 수 있는 진영과 붙들 진영을 나누었고, 제갈량은 그 구분을 다음 북방 명령의 첫 줄에 올렸습니다.',
|
|
rewardExp: 36
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-twelfth-zhuge-jiangwei-inheritance',
|
|
title: '계승의 장부',
|
|
availableAfterBattleIds: [campBattleIds.sixtySixth],
|
|
unitIds: ['zhuge-liang', 'jiang-wei'],
|
|
bondId: 'zhuge-liang__jiang-wei_northern-twelfth',
|
|
rewardExp: 120,
|
|
lines: [
|
|
'제갈량: 오늘의 장부는 이긴 전투의 기록이 아니라, 돌아간 병사의 이름을 잃지 않기 위한 기록입니다.',
|
|
'강유: 승패보다 오래 남는 것이 있다면, 그 이름들을 다음 명령 앞에 다시 세우는 일이겠습니다.',
|
|
'제갈량: 그러니 북쪽을 볼 때마다 먼저 뒤에 선 사람들을 세어 보십시오. 그때 길이 다시 열립니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-inherited-wuzhang-ledger',
|
|
label: '오장원 장부를 봉한다',
|
|
response: '강유는 오장원 장부를 접어 품에 넣었고, 제갈량은 마지막 줄에 돌아간 병사들의 부대를 먼저 적게 했습니다.',
|
|
rewardExp: 42
|
|
},
|
|
{
|
|
id: 'copy-return-road-map',
|
|
label: '귀환로 지도를 베낀다',
|
|
response: '두 사람은 북쪽으로 나아가는 길보다 남쪽으로 돌아오는 길을 먼저 베꼈습니다. 다음 시대의 첫 명령은 그 지도에서 시작될 것입니다.',
|
|
rewardExp: 38
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-twelfth-wangping-madai-rearguard',
|
|
title: '퇴로와 측면',
|
|
availableAfterBattleIds: [campBattleIds.sixtySixth],
|
|
unitIds: ['wang-ping', 'ma-dai'],
|
|
bondId: 'wang-ping__ma-dai_northern-twelfth',
|
|
rewardExp: 116,
|
|
lines: [
|
|
'왕평: 퇴로는 물러나는 길이 아니라, 다시 모일 수 있는 줄입니다. 줄이 끊기면 승리한 병사도 흩어집니다.',
|
|
'마대: 측면을 흔드는 기병도 결국 그 줄을 노립니다. 다음에는 먼저 끊고, 나중에 쫓겠습니다.',
|
|
'왕평: 좋습니다. 우리가 막은 것은 적의 기병만이 아니라, 돌아갈 사람들의 두려움이었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-rearguard-rally-points',
|
|
label: '후위 집결점을 새긴다',
|
|
response: '왕평은 퇴로의 북소리 간격을 정했고, 마대는 측면 기병이 돌아설 표식을 야영지마다 새겼습니다.',
|
|
rewardExp: 40
|
|
},
|
|
{
|
|
id: 'rest-final-flank-horses',
|
|
label: '측면 기병을 쉬게 한다',
|
|
response: '마대의 기병은 말안장을 풀었고, 왕평은 쉬는 병사들까지 다음 집결 순서에 맞추어 세웠습니다.',
|
|
rewardExp: 36
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
const campVisits: CampVisitDefinition[] = [
|
|
{
|
|
id: 'jingzhou-scholar-rumors',
|
|
title: '형주 선비들의 모임',
|
|
location: '신야 객사',
|
|
availableAfterBattleIds: [campBattleIds.sixteenth],
|
|
bondId: 'liu-bei__sun-qian',
|
|
description: '형주의 선비들이 모이는 객사에서 와룡과 봉추에 관한 소문을 모읍니다.',
|
|
lines: [
|
|
'손건: 형주의 선비들은 겉으로는 술과 시를 말하지만, 속으로는 천하의 흐름을 재고 있습니다.',
|
|
'유비: 그들의 말을 듣되 우리 뜻을 팔지는 말아야 하오. 와룡이라는 이름이 참이라면, 예로 찾아가야 하겠지.',
|
|
'객사의 선비들은 유비군이 오래 머물 손님인지, 천하를 논할 사람인지 조심스럽게 살핍니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'ask-about-wolong',
|
|
label: '와룡의 거처를 묻는다',
|
|
response: '손건은 무리하게 캐묻지 않고 융중 근처의 초가와 제갈씨 집안에 관한 소문을 얻어 냈다.',
|
|
bondExp: 18,
|
|
itemRewards: ['와룡 소문 1']
|
|
},
|
|
{
|
|
id: 'share-liu-bei-intent',
|
|
label: '유비의 뜻을 조용히 전한다',
|
|
response: '손건은 유비가 형주의 땅보다 사람을 먼저 찾는다는 뜻을 전했고, 선비들의 경계가 조금 누그러졌다.',
|
|
bondExp: 20,
|
|
gold: 80
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'xinye-market-supplies',
|
|
title: '신야 장터 정비',
|
|
location: '신야 장터',
|
|
availableAfterBattleIds: [campBattleIds.sixteenth],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
description: '피난민과 상인이 뒤섞인 장터에서 다음 여정을 위한 군량과 약을 챙깁니다.',
|
|
lines: [
|
|
'미축: 형주의 장터는 물자가 많지만 값도 빠르게 오릅니다. 지금 무엇을 먼저 챙길지 정해야 합니다.',
|
|
'유비: 백성의 몫을 빼앗지 않는 선에서 군을 살릴 물자를 구하시오. 다음 길은 길어질 것이오.',
|
|
'상인들은 유비군의 명분을 믿고 값을 낮출지, 낯선 객군이라며 셈을 올릴지 눈치를 봅니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'buy-field-medicine',
|
|
label: '상처약을 먼저 구한다',
|
|
response: '미축은 좋은 약재를 골라 상처약을 더 확보했고, 병사들은 긴 행군에 조금 안심했다.',
|
|
bondExp: 12,
|
|
itemRewards: ['상처약 2']
|
|
},
|
|
{
|
|
id: 'stock-camp-grain',
|
|
label: '군량을 넉넉히 산다',
|
|
response: '미축은 콩과 마른 양식을 챙기며 군자금을 아꼈고, 남은 돈으로 다음 협상 여지도 마련했다.',
|
|
bondExp: 10,
|
|
gold: 120,
|
|
itemRewards: ['콩 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'refugee-village-patrol',
|
|
title: '피난민 마을 순행',
|
|
location: '형주 북쪽 마을',
|
|
availableAfterBattleIds: [campBattleIds.sixteenth],
|
|
bondId: 'liu-bei__zhao-yun',
|
|
description: '조운과 함께 형주 북쪽 마을을 돌아보며 민심과 길목의 위험을 살핍니다.',
|
|
lines: [
|
|
'조운: 조조군을 피해 내려온 백성들이 마을 바깥에 모여 있습니다. 관문은 열렸지만 마음은 아직 닫혀 있습니다.',
|
|
'유비: 의탁한 몸이라도 백성을 외면할 수는 없소. 우리가 머무는 동안 폐가 아니라 힘이 되게 합시다.',
|
|
'피난민들은 유비의 이름을 듣고도 쉽게 다가오지 못하지만, 조운의 차분한 호위에 조금씩 안정을 찾습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'guard-refugee-road',
|
|
label: '피난길을 호위한다',
|
|
response: '조운은 피난민 행렬을 안전한 길로 이끌었고, 유비군의 깃발은 형주 북쪽 마을에 좋은 소문으로 남았다.',
|
|
bondExp: 22,
|
|
itemRewards: ['민심 기록 1']
|
|
},
|
|
{
|
|
id: 'listen-to-village-elders',
|
|
label: '마을 어른들의 말을 듣는다',
|
|
response: '유비는 마을 어른들의 걱정을 오래 들었고, 조운은 다음 행군에서 피해야 할 샛길을 지도에 표시했다.',
|
|
bondExp: 18,
|
|
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']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'bowang-battlefield-inspection',
|
|
title: '박망파 전장 점검',
|
|
location: '박망파 숲길',
|
|
availableAfterBattleIds: [campBattleIds.eighteenth],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
description: '박망파의 불길이 지나간 숲길을 살피며 다음 조조군 남하에 대비합니다.',
|
|
lines: [
|
|
'제갈량: 불길은 적의 발을 묶었지만, 다음에는 더 큰 군세가 올 것입니다. 길목마다 피난로와 매복지를 함께 살펴야 합니다.',
|
|
'유비: 백성이 지나갈 길과 군이 버틸 길을 따로 표시해 두시오. 승리보다 중요한 것은 사람을 잃지 않는 것이오.',
|
|
'병사들은 탄 흔적을 지우며, 이 작은 승리가 더 큰 전장의 시작임을 느낍니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-refugee-routes',
|
|
label: '피난로를 표시한다',
|
|
response: '제갈량은 백성이 지나갈 길을 군의 이동로와 분리해 지도에 적었고, 유비군의 다음 퇴각 준비가 빨라졌다.',
|
|
bondExp: 22,
|
|
itemRewards: ['피난로 지도 1']
|
|
},
|
|
{
|
|
id: 'collect-ambush-lessons',
|
|
label: '매복 기록을 정리한다',
|
|
response: '박망파의 매복 순서를 다시 기록하자, 다음 전장에서 책략 명령을 더 빨리 전달할 근거가 마련되었다.',
|
|
bondExp: 20,
|
|
itemRewards: ['와룡의 서찰 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jiangdong-letter-prep',
|
|
title: '강동 서신 준비',
|
|
location: '신야 객사',
|
|
availableAfterBattleIds: [campBattleIds.eighteenth],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
description: '강동에 보낼 첫 서신과 조조군 남하 정보를 정리합니다.',
|
|
lines: [
|
|
'손건: 강동은 말만으로 움직이지 않을 것입니다. 조조군이 남하한다는 증거와 주공이 버티는 이유를 함께 담아야 합니다.',
|
|
'제갈량: 맞습니다. 손권에게 필요한 것은 두려움만이 아니라 싸울 수 있다는 계산입니다.',
|
|
'서신의 문장은 짧아지고, 지도 위에는 조조군의 길과 강동의 바람이 함께 표시됩니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'send-cao-threat-report',
|
|
label: '조조 남하 보고를 쓴다',
|
|
response: '손건은 박망파의 전황과 조조군의 움직임을 정리했고, 강동 사절의 첫 명분이 분명해졌다.',
|
|
bondExp: 18,
|
|
itemRewards: ['강동 풍문 1']
|
|
},
|
|
{
|
|
id: 'reserve-envoy-funds',
|
|
label: '사절 군자금을 남긴다',
|
|
response: '불필요한 지출을 줄여 강동으로 보낼 사절 비용을 따로 묶어 두었다.',
|
|
bondExp: 14,
|
|
gold: 180,
|
|
itemRewards: ['탁주 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'changban-refugee-camp',
|
|
title: '장판파 피난민 수습',
|
|
location: '남쪽 피난 진영',
|
|
availableAfterBattleIds: [campBattleIds.nineteenth],
|
|
bondId: 'liu-bei__zhao-yun',
|
|
description: '장판파를 벗어난 피난민을 돌보며 흩어진 병사와 민심을 다시 모읍니다.',
|
|
lines: [
|
|
'조운: 피난민 중에는 가족을 잃고 주저앉은 사람도 많습니다. 군이 먼저 손을 내밀어야 길이 이어집니다.',
|
|
'유비: 우리가 지킨 길이 사람을 버리는 길이 되어서는 안 되오. 군량을 나누되, 다음 행군도 버틸 수 있게 살피시오.',
|
|
'남쪽 진영에는 먼지와 상처가 가득하지만, 유비군의 깃발 아래 다시 줄이 만들어집니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-field-medicine',
|
|
label: '상처 입은 피난민을 돌본다',
|
|
response: '조운은 약을 나누며 질서를 세웠고, 피난민들은 유비군이 자신들을 버리지 않았음을 기억했다.',
|
|
bondExp: 22,
|
|
itemRewards: ['민심 기록 1']
|
|
},
|
|
{
|
|
id: 'gather-scattered-soldiers',
|
|
label: '흩어진 병사를 모은다',
|
|
response: '장판파에서 흩어진 병사들이 하나둘 돌아왔고, 군영은 다음 이동을 버틸 힘을 되찾았다.',
|
|
bondExp: 18,
|
|
gold: 140,
|
|
itemRewards: ['콩 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jiangdong-envoy-after-changban',
|
|
title: '강동 사절 문서',
|
|
location: '피난 진영 막사',
|
|
availableAfterBattleIds: [campBattleIds.nineteenth],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
description: '제갈량과 손건이 장판파 전황을 강동에 보낼 문서로 정리합니다.',
|
|
lines: [
|
|
'손건: 장판파의 일은 처참하지만, 강동이 조조의 남하를 실감할 증거이기도 합니다.',
|
|
'제갈량: 두려움만 적으면 사람은 굳고, 계산만 적으면 마음이 움직이지 않습니다. 주공이 지킨 길을 함께 써야 합니다.',
|
|
'작은 막사 안에서 피난로 지도와 조조군의 진격 보고가 하나의 문서로 묶입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-sun-quan-brief',
|
|
label: '손권에게 보낼 요지를 쓴다',
|
|
response: '손건은 제갈량의 형세 판단을 짧은 문장으로 정리했고, 강동 사절의 첫 말이 뚜렷해졌다.',
|
|
bondExp: 20,
|
|
itemRewards: ['강동 풍문 1']
|
|
},
|
|
{
|
|
id: 'prepare-envoy-supplies',
|
|
label: '사절 보급을 챙긴다',
|
|
response: '강동까지 갈 사절단의 노자와 약품을 따로 묶어 두었다. 긴 길을 위한 준비가 조금 더 단단해졌다.',
|
|
bondExp: 16,
|
|
gold: 180,
|
|
itemRewards: ['상처약 1', '탁주 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jiangdong-parley-records',
|
|
title: '강동 회담 문서 정리',
|
|
location: '강나루 막사',
|
|
availableAfterBattleIds: [campBattleIds.twentieth],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
description: '손권과 주유를 설득하기 위해 조조군 남하 정보와 유비군의 피난 기록을 회담 문서로 정리합니다.',
|
|
lines: [
|
|
'손건: 강동은 싸울 명분만이 아니라 승산을 볼 것입니다. 숫자와 길, 사람의 마음을 모두 적어야 합니다.',
|
|
'제갈량: 문서는 짧아야 하지만 빈틈은 없어야 합니다. 손권이 물을 질문을 먼저 문서 안에 넣겠습니다.',
|
|
'강나루 막사의 등불 아래, 장판파의 먼지와 강동의 바람이 같은 종이에 적힙니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'compile-cao-army-scale',
|
|
label: '조조군 규모를 정리한다',
|
|
response: '손건은 추격대의 규모와 조조군의 남하 속도를 정리했고, 강동 회담의 위기감이 분명해졌다.',
|
|
bondExp: 20,
|
|
itemRewards: ['강동 사절 문서 1']
|
|
},
|
|
{
|
|
id: 'write-liu-bei-cause',
|
|
label: '유비의 명분을 적는다',
|
|
response: '제갈량은 유비가 피난민을 버리지 않았다는 기록을 회담의 첫 문장에 두었다.',
|
|
bondExp: 18,
|
|
gold: 160,
|
|
itemRewards: ['민심 기록 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'river-port-firecraft',
|
|
title: '강나루 화공 논의',
|
|
location: '강나루 선착장',
|
|
availableAfterBattleIds: [campBattleIds.twentieth],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
description: '장강 전장을 대비해 배, 바람, 불길의 가능성을 조심스럽게 검토합니다.',
|
|
lines: [
|
|
'제갈량: 큰 군세는 넓은 땅에서 강하지만, 강 위에서는 바람과 배가 전장의 절반을 차지합니다.',
|
|
'유비: 불을 쓰더라도 백성이 피할 길은 먼저 열어야 하오. 이 싸움 역시 사람을 살리기 위한 길이어야 하오.',
|
|
'선착장에는 작은 배와 젖은 밧줄이 놓여 있고, 병사들은 처음으로 강 위의 전장을 상상합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'inspect-boats-and-wind',
|
|
label: '배와 바람을 살핀다',
|
|
response: '제갈량은 바람의 방향과 배의 간격을 기록했고, 훗날 화공을 논할 작은 단서가 마련되었다.',
|
|
bondExp: 22,
|
|
itemRewards: ['화공 논의 1']
|
|
},
|
|
{
|
|
id: 'secure-river-medicine',
|
|
label: '강가 보급을 챙긴다',
|
|
response: '유비는 강가 병사들의 상처와 추위를 살폈고, 미축은 젖지 않는 약품 꾸러미를 따로 묶었다.',
|
|
bondExp: 16,
|
|
gold: 140,
|
|
itemRewards: ['상처약 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'red-cliffs-firecraft-prep',
|
|
title: '적벽 화공 예행',
|
|
location: '강동 선착장',
|
|
availableAfterBattleIds: [campBattleIds.twentyFirst],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
description: '첫 강안 전선을 확보한 뒤, 배와 바람을 살피며 화공을 실제 전장 계획으로 다듬습니다.',
|
|
lines: [
|
|
'제갈량: 전초전으로 포구를 얻었으니 이제 불길이 어디서 시작되어 어디로 번질지 따져 볼 수 있습니다.',
|
|
'유비: 불은 무서운 계책이오. 다만 조조의 대군을 멈추지 못하면 더 많은 백성이 떠밀릴 것이오.',
|
|
'선착장에는 젖은 짚단과 빈 배가 놓였고, 병사들은 처음으로 불을 전장의 언어로 배우기 시작합니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'test-fire-boat-spacing',
|
|
label: '화선 간격을 시험한다',
|
|
response: '제갈량은 배 사이 간격과 바람의 흐름을 기록했고, 적벽 화공의 첫 실행안이 더 또렷해졌다.',
|
|
bondExp: 24,
|
|
itemRewards: ['화공 논의 1']
|
|
},
|
|
{
|
|
id: 'prepare-evacuation-line',
|
|
label: '퇴로와 피난로를 먼저 정한다',
|
|
response: '유비는 불길을 쓰기 전 아군과 백성이 빠질 길부터 표시하게 했고, 계책은 더 조심스러운 형태를 갖추었다.',
|
|
bondExp: 20,
|
|
gold: 160,
|
|
itemRewards: ['상처약 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'alliance-war-council-records',
|
|
title: '동맹 군의 회의록',
|
|
location: '강동 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentyFirst],
|
|
bondId: 'sun-qian__zhuge-liang',
|
|
description: '손건과 제갈량이 주유에게 보낼 전초전 기록과 다음 화공 논의를 정리합니다.',
|
|
lines: [
|
|
'손건: 동맹은 말로 맺었지만, 계속 이어 가려면 기록과 전공이 서로 맞아야 합니다.',
|
|
'제갈량: 오늘 유비군이 세운 전선과 강동군이 준비할 화공을 한 장의 흐름으로 묶겠습니다.',
|
|
'군막 안에는 조조 수군의 배치도와 포구 전초전 기록이 펼쳐지고, 다음 싸움의 책임이 문장 위에 놓입니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'write-joint-command-note',
|
|
label: '합동 지휘 요지를 적는다',
|
|
response: '손건은 유비군의 전선 유지와 강동군의 화공 준비를 함께 적어 동맹의 균형을 분명히 했다.',
|
|
bondExp: 22,
|
|
itemRewards: ['동맹 군의 기록 1']
|
|
},
|
|
{
|
|
id: 'clarify-zhou-yu-requests',
|
|
label: '주유에게 요청할 내용을 정한다',
|
|
response: '제갈량은 주유가 요구할 병력, 배, 시점을 예상했고 손건은 그 답을 짧은 회의록으로 정리했다.',
|
|
bondExp: 18,
|
|
gold: 180,
|
|
itemRewards: ['탁주 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'red-cliffs-aftermath-relief',
|
|
title: '적벽 전후 수습',
|
|
location: '강안 임시 진영',
|
|
availableAfterBattleIds: [campBattleIds.twentySecond],
|
|
bondId: 'liu-bei__mi-zhu',
|
|
description: '불길 뒤의 강안에서 부상자와 흩어진 병사를 수습하며 다음 행군의 보급을 정리합니다.',
|
|
lines: [
|
|
'미축: 승리는 컸지만 젖은 군량과 다친 병사가 많습니다. 지금 정리하지 않으면 형주로 향할 때 다시 흔들릴 것입니다.',
|
|
'유비: 불길 뒤에 남은 사람을 먼저 살펴야 하오. 승리의 이름보다 살아 움직일 군이 먼저요.',
|
|
'강안 임시 진영에는 젖은 깃발과 그을린 갑옷이 널려 있고, 병사들은 비로소 숨을 고릅니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'treat-burned-soldiers',
|
|
label: '화상 입은 병사를 돌본다',
|
|
response: '미축은 약품을 아끼지 않았고, 유비는 전공보다 병사를 먼저 살핀다는 군영의 기준을 다시 세웠다.',
|
|
bondExp: 22,
|
|
itemRewards: ['상처약 2']
|
|
},
|
|
{
|
|
id: 'restore-river-supplies',
|
|
label: '강안 보급을 다시 묶는다',
|
|
response: '젖은 군량을 버리고 쓸 수 있는 물자를 다시 묶자, 다음 형주 진입을 버틸 보급이 마련되었다.',
|
|
bondExp: 18,
|
|
gold: 180,
|
|
itemRewards: ['콩 3']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jingzhou-entry-council',
|
|
title: '형주 진입 논의',
|
|
location: '전후 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentySecond],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
description: '제갈량과 함께 적벽 이후의 형세를 정리하고 형주 남부로 향할 첫 명분을 준비합니다.',
|
|
lines: [
|
|
'제갈량: 조조가 물러난 지금, 빈틈은 오래 머물지 않습니다. 형주 남부의 군현은 두려움과 기대 사이에 있습니다.',
|
|
'유비: 힘으로 빼앗는다면 조조와 무엇이 다르겠소. 백성이 기대어 설 명분을 먼저 세워야 하오.',
|
|
'군막의 지도 위에는 적벽의 붉은 표시가 지워지고, 형주 남부의 길목이 새로 동그라미 쳐집니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-jingzhou-cause',
|
|
label: '형주 진입 명분을 쓴다',
|
|
response: '제갈량은 조조를 막은 공과 백성 보호의 뜻을 함께 적어, 형주 남부로 향할 첫 문장을 만들었다.',
|
|
bondExp: 24,
|
|
itemRewards: ['형주 진입 문서 1']
|
|
},
|
|
{
|
|
id: 'survey-southern-commandery',
|
|
label: '남부 군현 정보를 모은다',
|
|
response: '손건과 간옹이 들은 풍문까지 지도에 더해지자, 다음 행군에서 설득해야 할 성과 피해야 할 길이 분명해졌다.',
|
|
bondExp: 20,
|
|
gold: 200,
|
|
itemRewards: ['민심 기록 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'lingling-village-relief',
|
|
title: '영릉 마을 구호',
|
|
location: '영릉 외곽 마을',
|
|
availableAfterBattleIds: [campBattleIds.twentyThird],
|
|
bondId: 'liu-bei__ma-liang',
|
|
description: '형도영의 가병이 물러난 뒤, 유비와 마량이 마을을 돌며 남은 상처와 민심을 살핍니다.',
|
|
lines: [
|
|
'마량: 이 마을은 길목이라 어느 군대가 지나가도 먼저 흔들립니다. 오늘은 곡식보다 안심할 말이 필요합니다.',
|
|
'유비: 우리가 지나간 뒤에도 마을이 버틸 수 있어야 하오. 남은 약과 곡식을 나누고, 군율을 다시 세우시오.',
|
|
'마을 장로들은 처음에는 문을 반쯤만 열었지만, 약과 곡식이 먼저 들어오자 조심스레 군영의 이름을 물었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'leave-village-medicine',
|
|
label: '상처약을 남긴다',
|
|
response: '마량은 약을 받은 집마다 이름을 적었고, 유비군이 약탈군이 아니라는 소문이 영릉 주변으로 퍼졌다.',
|
|
bondExp: 22,
|
|
itemRewards: ['상처약 2']
|
|
},
|
|
{
|
|
id: 'write-relief-record',
|
|
label: '구호 기록을 남긴다',
|
|
response: '유비와 마량은 마을별 필요 물자를 문서로 남겼고, 다음 보급 계획이 한결 정확해졌다.',
|
|
bondExp: 18,
|
|
gold: 160,
|
|
itemRewards: ['형주 민심 기록 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'southern-commandery-ledger',
|
|
title: '남부 군현 문서',
|
|
location: '임시 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentyThird],
|
|
bondId: 'zhuge-liang__ma-liang',
|
|
description: '제갈량과 마량이 영릉 이후 차례로 열어야 할 군현의 길, 토호, 마을 사정을 정리합니다.',
|
|
lines: [
|
|
'제갈량: 전투로 이긴 곳은 하루면 흔들리고, 문서로 묶은 곳은 다음 계절까지 버팁니다.',
|
|
'마량: 계양은 먼저 설득할 사람이 있고, 무릉은 길이 험합니다. 장사는 장수의 이름부터 살펴야 합니다.',
|
|
'두 사람의 지도가 펼쳐지자 형주 남부는 막연한 땅이 아니라, 차례로 준비해야 할 선택지로 보이기 시작했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-guiyang-route',
|
|
label: '계양으로 가는 길을 표시한다',
|
|
response: '마량이 계양 길목과 설득 대상을 적자, 제갈량은 다음 군현 확보 순서를 새로 정리했다.',
|
|
bondExp: 20,
|
|
itemRewards: ['남부 군현 지도 1']
|
|
},
|
|
{
|
|
id: 'estimate-wuling-risk',
|
|
label: '무릉의 위험을 계산한다',
|
|
response: '험한 길과 분산된 마을이 지도에 표시되며, 다음 전장에는 기동 장수와 보급 장수의 선택이 더 중요해졌다.',
|
|
bondExp: 18,
|
|
gold: 180,
|
|
itemRewards: ['군현 문서 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'guiyang-civic-pledge',
|
|
title: '계양 민심 서약',
|
|
location: '계양 관청',
|
|
availableAfterBattleIds: [campBattleIds.twentyFourth],
|
|
bondId: 'liu-bei__yi-ji',
|
|
description: '유비와 이적이 계양 관청에서 군율과 세곡 처리 원칙을 밝히며 새로 열린 군현의 불안을 다독입니다.',
|
|
lines: [
|
|
'이적: 계양 사람들은 항복보다 그 뒤의 세금과 군율을 두려워합니다. 먼저 약속을 글로 남겨야 합니다.',
|
|
'유비: 우리가 이곳에 머무는 동안 백성의 창고를 함부로 열지 않겠소. 필요한 것은 값을 치르고, 군율을 어기면 벌하겠소.',
|
|
'관청 앞에 모인 사람들은 아직 조심스러웠지만, 글로 남긴 약속을 보고 처음으로 긴 숨을 내쉬었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'publish-guiyang-discipline',
|
|
label: '군율 서약을 내건다',
|
|
response: '이적은 유비의 약속을 관청 문에 걸었고, 계양의 불안은 조금씩 거래와 보급으로 바뀌기 시작했다.',
|
|
bondExp: 22,
|
|
itemRewards: ['계양 민심 기록 1']
|
|
},
|
|
{
|
|
id: 'settle-storehouse-rules',
|
|
label: '창고 사용 원칙을 정한다',
|
|
response: '계양 창고의 곡식 출납 원칙이 정해지자, 미축의 보급 계획도 다음 전장까지 이어질 수 있게 되었다.',
|
|
bondExp: 18,
|
|
gold: 180,
|
|
itemRewards: ['콩 3']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'wuling-mountain-scouting',
|
|
title: '무릉 산길 정찰',
|
|
location: '무릉 길목',
|
|
availableAfterBattleIds: [campBattleIds.twentyFourth],
|
|
bondId: 'ma-liang__yi-ji',
|
|
description: '마량과 이적이 무릉으로 이어지는 산길과 마을 이름을 대조하며 다음 전투의 위험을 미리 살핍니다.',
|
|
lines: [
|
|
'마량: 이 길은 좁고 마을은 흩어져 있습니다. 기병만 앞세우면 빠르지만, 뒤의 보급이 끊길 수 있습니다.',
|
|
'이적: 산길 마을은 말보다 신뢰를 먼저 봅니다. 먼저 이름을 부르고, 그 다음에 도움을 청해야 합니다.',
|
|
'두 사람은 길목마다 작은 표식을 남기고, 다음 전투에서 놓치기 쉬운 마을 이름을 지도 옆에 적었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-wuling-villages',
|
|
label: '산길 마을을 표시한다',
|
|
response: '무릉의 작은 마을들이 지도에 표시되자, 다음 전장에서는 이동과 보급의 선택이 더 분명해졌다.',
|
|
bondExp: 20,
|
|
itemRewards: ['무릉 산길 지도 1']
|
|
},
|
|
{
|
|
id: 'prepare-mountain-supplies',
|
|
label: '산길 보급을 준비한다',
|
|
response: '이적은 마을별 필요한 물자를 적고, 마량은 안전한 우회로를 더했다. 무릉 진입 준비가 조금 더 단단해졌다.',
|
|
bondExp: 18,
|
|
gold: 160,
|
|
itemRewards: ['상처약 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'wuling-road-repair',
|
|
title: '무릉 산길 정비',
|
|
location: '무릉 서쪽 고개',
|
|
availableAfterBattleIds: [campBattleIds.twentyFifth],
|
|
bondId: 'liu-bei__gong-zhi',
|
|
description: '유비와 공지가 전투로 망가진 산길을 정비하며, 마을 사람들이 다시 오갈 수 있는 길을 먼저 회복합니다.',
|
|
lines: [
|
|
'공지: 이 고개가 막히면 무릉 사람들은 곡식을 팔러 갈 수도, 약을 구하러 갈 수도 없습니다.',
|
|
'유비: 군이 지나간 자리라면 군이 먼저 고쳐야 하오. 병사들을 나누어 길을 다듬게 하시오.',
|
|
'마을 사람들은 처음엔 멀리서 지켜보다가, 길이 열리자 조심스럽게 삽과 바구니를 들고 나왔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'repair-wuling-pass',
|
|
label: '고개를 먼저 고친다',
|
|
response: '공지가 길목을 지휘하고 유비가 병사들을 나누자, 무릉 서쪽 고개가 다시 짐수레를 받을 수 있게 되었다.',
|
|
bondExp: 22,
|
|
itemRewards: ['무릉 보급 기록 1']
|
|
},
|
|
{
|
|
id: 'protect-returning-villagers',
|
|
label: '귀환민을 호위한다',
|
|
response: '돌아오는 마을 사람들을 호위하자, 무릉의 불안한 소문은 유비군이 길을 지킨다는 말로 바뀌었다.',
|
|
bondExp: 18,
|
|
gold: 180,
|
|
itemRewards: ['콩 3']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'changsha-veteran-rumor',
|
|
title: '장사 노장 소문',
|
|
location: '무릉 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentyFifth],
|
|
bondId: 'yi-ji__gong-zhi',
|
|
description: '이적과 공지가 장사로 향하기 전에 황충과 한현의 성격, 장사 성내 분위기를 정리합니다.',
|
|
lines: [
|
|
'이적: 장사에는 황충이라는 노장이 있습니다. 예우 없이 대하면 성문보다 마음이 먼저 닫힐 것입니다.',
|
|
'공지: 한현은 의심이 많고, 황충은 무인의 체면을 중히 여깁니다. 싸움보다 먼저 사람을 보아야 합니다.',
|
|
'두 사람은 장사로 보낼 사절 명단과 예물을 차례로 적었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-huang-zhong-courtesy',
|
|
label: '황충 예우를 준비한다',
|
|
response: '이적은 노장을 대하는 예문을 쓰고, 공지는 장사 내부 소문을 덧붙였다. 다음 장의 첫 대면이 조금 더 조심스러워졌다.',
|
|
bondExp: 20,
|
|
itemRewards: ['장사 사절 문서 1']
|
|
},
|
|
{
|
|
id: 'map-changsha-approach',
|
|
label: '장사 접근로를 표시한다',
|
|
response: '공지의 무릉 길 정보와 이적의 문서가 합쳐져, 장사로 향하는 길과 만날 사람의 순서가 정리되었다.',
|
|
bondExp: 18,
|
|
gold: 200,
|
|
itemRewards: ['남부 군현 지도 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'changsha-veteran-honor',
|
|
title: '장사 노장 예우',
|
|
location: '장사 성루',
|
|
availableAfterBattleIds: [campBattleIds.twentySixth],
|
|
bondId: 'liu-bei__huang-zhong',
|
|
description: '유비와 황충이 장사 성루에 올라 전투 뒤 흔들린 병사들을 달래고, 노장의 명예를 새 군율 안에 세웁니다.',
|
|
lines: [
|
|
'황충: 병사들은 제가 패한 것이 아니라 버림받은 것이라 여길까 두려워합니다.',
|
|
'유비: 장군을 예로 맞는다면 병사들도 자기 자리를 잃지 않을 것이오. 성루에서 함께 군율을 밝힙시다.',
|
|
'장사 성루 위에는 아직 화살 자국이 남아 있었지만, 병사들의 시선은 조금씩 아래로 내려왔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'honor-changsha-soldiers',
|
|
label: '장사 병사를 예우한다',
|
|
response: '황충의 이름으로 장사 병사들의 항복 절차가 정리되자, 성 안의 불안은 빠르게 가라앉았다.',
|
|
bondExp: 22,
|
|
itemRewards: ['장사 병적 기록 1']
|
|
},
|
|
{
|
|
id: 'open-castle-storehouse',
|
|
label: '성내 창고를 점검한다',
|
|
response: '창고를 함부로 열지 않고 장부부터 확인하자, 장사 상인들이 다시 거래를 시작할 수 있게 되었다.',
|
|
bondExp: 18,
|
|
gold: 220,
|
|
itemRewards: ['콩 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yi-province-road-council',
|
|
title: '익주 길 논의',
|
|
location: '장사 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentySixth],
|
|
bondId: 'liu-bei__zhuge-liang',
|
|
description: '제갈량과 새로 합류한 장수들의 정보를 모아, 형주 남부 이후 익주로 향할 큰 방향을 처음 정리합니다.',
|
|
lines: [
|
|
'제갈량: 형주 남부는 뿌리가 되었지만, 뿌리만으로 천하를 받칠 수는 없습니다. 익주로 향할 명분과 길이 필요합니다.',
|
|
'유비: 길이 멀수록 함께 갈 사람을 더 살펴야 하오. 새로 온 장수들의 뜻도 함께 묶읍시다.',
|
|
'군막의 지도에는 형주 남부 표시 옆으로 익주로 향하는 긴 선이 처음 그어졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-yi-province-cause',
|
|
label: '익주 명분을 적는다',
|
|
response: '제갈량은 형주 남부 안정과 백성 보호의 기록을 바탕으로, 익주로 향할 첫 명분을 정리했다.',
|
|
bondExp: 24,
|
|
itemRewards: ['익주 진입 초안 1']
|
|
},
|
|
{
|
|
id: 'assign-new-officers',
|
|
label: '새 장수 역할을 나눈다',
|
|
response: '황충, 위연, 공지의 역할이 정리되자 다음 대장정에서 누가 전열과 후방을 맡을지 윤곽이 생겼다.',
|
|
bondExp: 20,
|
|
gold: 220,
|
|
itemRewards: ['군영 편성 기록 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yizhou-entry-council',
|
|
title: '익주 원군로 회의',
|
|
location: '익주 길목 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentySeventh],
|
|
bondId: 'liu-bei__pang-tong',
|
|
description: '방통이 합류한 뒤, 유비와 새 책사가 익주로 들어가는 명분과 속도를 처음 조율합니다.',
|
|
lines: [
|
|
'방통: 익주는 문이 많은 땅입니다. 모든 문을 두드리면 늦고, 모든 문을 부수면 민심을 잃습니다.',
|
|
'유비: 그러니 열어야 할 문과 기다려야 할 문을 구분해야 하오.',
|
|
'군막의 촛불 아래 익주의 관문 이름과 마을 이름이 하나씩 적히기 시작했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'choose-yizhou-cause-first',
|
|
label: '명분을 먼저 세운다',
|
|
response: '방통은 유비의 뜻에 맞춰 원군 요청 문서를 앞세우고, 군의 이동 속도는 조금 늦추되 마을의 신뢰를 얻도록 조정했다.',
|
|
bondExp: 24,
|
|
itemRewards: ['익주 원군 문서 1']
|
|
},
|
|
{
|
|
id: 'choose-yizhou-speed-first',
|
|
label: '관문 속도를 높인다',
|
|
response: '방통은 빠르게 흔들 수 있는 관문과 우회로를 짚었다. 유비는 백성이 다치지 않는 선을 분명히 못박았다.',
|
|
bondExp: 22,
|
|
gold: 240,
|
|
itemRewards: ['산문 정찰 기록 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fu-pass-road-survey',
|
|
title: '부수관 길 정찰',
|
|
location: '익주 산길',
|
|
availableAfterBattleIds: [campBattleIds.twentySeventh],
|
|
bondId: 'zhuge-liang__pang-tong',
|
|
description: '제갈량과 방통이 다음 관문으로 이어지는 산길을 살피며, 출전 후보와 병과 조합을 논합니다.',
|
|
lines: [
|
|
'제갈량: 이 길은 한 번 막히면 보급이 끊깁니다. 전열보다 후원이 무너질 수 있습니다.',
|
|
'방통: 그렇다면 적이 막기 전에 우리가 먼저 막힌 척해야겠군요. 적을 끌어내면 관문은 비게 됩니다.',
|
|
'두 책사는 산의 그림자와 강의 굽이를 지도 위에 겹쳐 보았습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-fu-pass-supply',
|
|
label: '보급선을 먼저 살핀다',
|
|
response: '제갈량은 보급선을, 방통은 유인 지점을 표시했다. 다음 전투의 출전 선택이 한층 중요해졌다.',
|
|
bondExp: 22,
|
|
itemRewards: ['부수관 보급표 1']
|
|
},
|
|
{
|
|
id: 'prepare-feint-on-pass',
|
|
label: '유인책을 세운다',
|
|
response: '방통의 기책과 제갈량의 큰 판이 합쳐져, 적을 관문 밖으로 끌어내는 초안이 마련되었다.',
|
|
bondExp: 24,
|
|
gold: 260,
|
|
itemRewards: ['관문 유인 초안 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fu-pass-storehouse-seal',
|
|
title: '부수관 창고 봉인',
|
|
location: '부수관 창고',
|
|
availableAfterBattleIds: [campBattleIds.twentyEighth],
|
|
bondId: 'liu-bei__fa-zheng',
|
|
description: '법정의 조언에 따라 부수관 창고를 함부로 열지 않고 장부와 민심을 먼저 정리합니다.',
|
|
lines: [
|
|
'법정: 익주 사람들은 창고 문이 열리는 순간을 보고 새 주인을 판단할 것입니다.',
|
|
'유비: 병사들이 배고파도 백성의 곡식을 빼앗아서는 안 되오. 장부를 먼저 세우겠소.',
|
|
'창고 앞에 모인 사람들은 병사들이 물러서고 장부가 펼쳐지는 모습을 조용히 지켜보았습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-storehouse-ledger',
|
|
label: '장부를 먼저 세운다',
|
|
response: '법정은 창고 장부를 정리했고, 유비군은 필요한 보급만 표시해 주민의 불안을 줄였다.',
|
|
bondExp: 24,
|
|
itemRewards: ['부수관 창고 장부 1']
|
|
},
|
|
{
|
|
id: 'guard-storehouse-night',
|
|
label: '야간 경계를 세운다',
|
|
response: '창고를 노리는 잔병을 막아 내자, 부수관 주민들은 유비군이 약속을 지킨다는 말을 전하기 시작했다.',
|
|
bondExp: 20,
|
|
gold: 260,
|
|
itemRewards: ['콩 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'luo-castle-entry-council',
|
|
title: '낙성 진입 회의',
|
|
location: '부수관 군막',
|
|
availableAfterBattleIds: [campBattleIds.twentyEighth],
|
|
bondId: 'pang-tong__fa-zheng',
|
|
description: '방통과 법정이 낙성으로 이어지는 길을 두고 빠른 기책과 내부 설득의 균형을 조율합니다.',
|
|
lines: [
|
|
'방통: 빠르게 움직이면 낙성 앞의 적이 미처 모이지 못합니다.',
|
|
'법정: 그러나 빠른 길일수록 뒤에 남는 사람의 마음이 상합니다. 누가 남고 누가 따를지 먼저 보아야 합니다.',
|
|
'두 사람의 말이 부딪히자, 군막 안의 지도 위에는 빠른 길과 조심스러운 길이 동시에 그어졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-fast-luo-entry',
|
|
label: '빠른 진입로를 잡는다',
|
|
response: '방통은 협곡을 통과하는 빠른 진입로를 표시했고, 법정은 그 길의 반발을 줄일 연락책을 붙였다.',
|
|
bondExp: 24,
|
|
itemRewards: ['낙성 진입 초안 1']
|
|
},
|
|
{
|
|
id: 'prepare-soft-luo-entry',
|
|
label: '내부 설득을 우선한다',
|
|
response: '법정은 설득해야 할 호족과 관리의 이름을 적었고, 방통은 그 사이를 흔들 유인책을 더했다.',
|
|
bondExp: 22,
|
|
gold: 280,
|
|
itemRewards: ['익주 연락 명단 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'luo-village-reassurance',
|
|
title: '낙성 외곽 마을 위무',
|
|
location: '낙성 외곽 마을',
|
|
availableAfterBattleIds: [campBattleIds.twentyNinth],
|
|
bondId: 'liu-bei__wu-yi',
|
|
description: '오의가 앞장서서 마을 사람들을 안심시키고, 유비군이 낙성 백성을 해치지 않는다는 약속을 확인합니다.',
|
|
lines: [
|
|
'오의: 이 마을 사람들이 마음을 열면 낙성 안쪽도 곧 소문을 듣게 됩니다.',
|
|
'유비: 군량과 말은 백성에게서 빼앗지 않는다. 이 약속을 먼저 전하시오.',
|
|
'마을의 불안이 조금 누그러지고 낙성으로 이어지는 작은 길들이 지도에 더해졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'guard-luo-village',
|
|
label: '마을 경계를 세운다',
|
|
response: '오의가 아는 길목에 경계를 세우자 백성들은 유비군의 군율을 조금 더 믿게 되었다.',
|
|
bondExp: 24,
|
|
itemRewards: ['콩 4']
|
|
},
|
|
{
|
|
id: 'share-grain-ledger',
|
|
label: '군량 명부를 공개한다',
|
|
response: '유비군이 쓸 군량과 남길 군량을 나누어 적자, 오의는 설득할 말이 생겼다고 말했다.',
|
|
bondExp: 22,
|
|
gold: 300,
|
|
itemRewards: ['낙성 마을 명부 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'luofeng-shadow-council',
|
|
title: '낙봉파 그림자 회의',
|
|
location: '낙성 진군로',
|
|
availableAfterBattleIds: [campBattleIds.twentyNinth],
|
|
bondId: 'pang-tong__wu-yi',
|
|
description: '방통과 오의가 낙성 안쪽 길을 살피며, 다음 전장으로 이어질 위험한 좁은 길을 표시합니다.',
|
|
lines: [
|
|
'방통: 이름부터 좋지 않군요. 봉황이 떨어지는 비탈이라니.',
|
|
'오의: 그 길은 빠르지만 너무 조용합니다. 장임이라면 그 조용함을 그냥 두지 않을 겁니다.',
|
|
'두 사람은 다음 출전에서 정찰과 호위를 어떻게 나눌지 군영 지도 위에 표시했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'send-forward-scouts',
|
|
label: '선행 정찰을 보낸다',
|
|
response: '오의가 길을 아는 사람을 붙이고 방통이 정찰 순서를 정했다. 다음 전장의 불안이 조금 줄었다.',
|
|
bondExp: 24,
|
|
itemRewards: ['낙봉파 정찰표 1']
|
|
},
|
|
{
|
|
id: 'prepare-ambush-counter',
|
|
label: '매복 대응책을 세운다',
|
|
response: '방통은 일부러 느슨해 보이는 행군을 제안했고, 오의는 적이 물어뜯을 만한 길목을 짚었다.',
|
|
bondExp: 22,
|
|
gold: 320,
|
|
itemRewards: ['매복 대응 초안 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'luofeng-slope-shrine',
|
|
title: '낙봉파 길가의 제단',
|
|
location: '낙봉파 비탈',
|
|
availableAfterBattleIds: [campBattleIds.thirtieth],
|
|
bondId: 'liu-bei__pang-tong',
|
|
description: '유비와 방통이 매복을 넘긴 비탈의 작은 제단에 들러, 살아 남은 병사들과 다음 길의 마음을 다집니다.',
|
|
lines: [
|
|
'방통: 이곳에서 한 걸음만 늦었다면 제 이름은 길가의 이야기가 되었을지도 모릅니다.',
|
|
'유비: 그러니 살아 남은 오늘을 가볍게 쓰지 맙시다. 다음 성문 앞에서도 백성을 먼저 보겠소.',
|
|
'제단 앞에 놓인 낡은 향이 꺼지고, 병사들은 낙봉파를 넘었다는 사실을 뒤늦게 실감했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'comfort-luofeng-wounded',
|
|
label: '부상병을 위문한다',
|
|
response: '유비가 부상병의 손을 잡자 방통도 다음 행군표에서 무리한 구간을 줄였습니다.',
|
|
bondExp: 26,
|
|
itemRewards: ['상처약 3']
|
|
},
|
|
{
|
|
id: 'offer-luofeng-prayer',
|
|
label: '전몰자를 기린다',
|
|
response: '군영의 말소리가 낮아지고, 방통은 다음 계책에 병사들이 돌아올 길을 먼저 적었습니다.',
|
|
bondExp: 24,
|
|
gold: 340,
|
|
itemRewards: ['맑은 향 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'luo-castle-main-council',
|
|
title: '낙성 본성 공략 회의',
|
|
location: '낙성 앞 군영',
|
|
availableAfterBattleIds: [campBattleIds.thirtieth],
|
|
bondId: 'fa-zheng__wu-yi',
|
|
description: '법정과 오의가 낙성 본성의 문, 물길, 수비 장수들의 성향을 대조하며 다음 공략의 밑그림을 만듭니다.',
|
|
lines: [
|
|
'법정: 장임은 강하지만, 낙성 안의 모든 장수가 같은 마음으로 버티는 것은 아닙니다.',
|
|
'오의: 제가 아는 이름들을 적겠습니다. 문을 여는 사람과 끝까지 막을 사람을 나누어 보지요.',
|
|
'두 사람이 성문 지도를 다시 그리자, 다음 전장의 목표가 단순한 격파가 아니라 설득을 포함한 공략으로 바뀌었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'sort-luo-officer-names',
|
|
label: '장수 명단을 분류한다',
|
|
response: '익주 장수들의 이름 옆에 설득, 견제, 격파라는 표시가 붙으며 다음 전장의 말길이 열렸습니다.',
|
|
bondExp: 26,
|
|
itemRewards: ['낙성 장수 명단 1']
|
|
},
|
|
{
|
|
id: 'trace-luo-supply-water',
|
|
label: '보급 수로를 추적한다',
|
|
response: '오의가 작은 물길을 짚자 법정은 그 길이 성 안의 불안을 흔들 수 있다고 판단했습니다.',
|
|
bondExp: 24,
|
|
gold: 360,
|
|
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']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'mianzhu-storehouse-relief',
|
|
title: '면죽관 창고 구호',
|
|
location: '면죽관 창고',
|
|
availableAfterBattleIds: [campBattleIds.thirtySecond],
|
|
bondId: 'liu-bei__li-yan',
|
|
description: '유비와 이엄이 면죽관 창고를 열어 항복 병사와 백성에게 공평하게 배급할 방법을 정합니다.',
|
|
lines: [
|
|
'이엄: 창고를 빼앗긴 병사들은 패배보다 굶주림을 더 두려워합니다.',
|
|
'유비: 그렇다면 먼저 명부를 세우고, 우리 병사와 항복 병사를 같은 줄에 세웁시다.',
|
|
'창고 앞의 긴장된 줄은 천천히 낮아졌고, 면죽관의 문은 전리품이 아니라 구호소처럼 쓰이기 시작했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'equal-storehouse-ration',
|
|
label: '배급 줄을 하나로 합친다',
|
|
response: '유비군과 항복 병사가 같은 줄에서 배급을 받자, 성도까지 보낼 소문이 한층 부드러워졌습니다.',
|
|
bondExp: 28,
|
|
itemRewards: ['콩 6', '상처약 3']
|
|
},
|
|
{
|
|
id: 'record-mianzhu-ledger',
|
|
label: '창고 장부를 남긴다',
|
|
response: '이엄은 창고 장부에 손댄 곡식과 남긴 곡식을 모두 적어, 다음 성도 권고의 증거를 만들었습니다.',
|
|
bondExp: 26,
|
|
gold: 420,
|
|
itemRewards: ['면죽관 창고 장부 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'chengdu-surrender-council',
|
|
title: '성도 항복 권고 회의',
|
|
location: '면죽관 본막',
|
|
availableAfterBattleIds: [campBattleIds.thirtySecond],
|
|
bondId: 'zhuge-liang__li-yan',
|
|
description: '제갈량과 이엄이 성도에 보낼 항복 조건을 정리하고, 유장이 받아들일 만한 명분을 다듬습니다.',
|
|
lines: [
|
|
'제갈량: 권고문은 이기는 군대의 칼이 아니라, 성 안의 사람이 붙잡을 난간이어야 합니다.',
|
|
'이엄: 유장공이 체면을 잃지 않는 문장이라면, 문을 여는 사람도 배신자가 아니라 중재자가 될 수 있습니다.',
|
|
'두 사람은 항복 뒤 관직과 창고, 병사의 처우를 조목조목 적어 다음 장의 실마리를 만들었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'soften-surrender-terms',
|
|
label: '항복 조건을 부드럽게 쓴다',
|
|
response: '권고문은 위협보다 보전과 질서를 앞세웠고, 성도 안쪽으로 들어갈 여지가 넓어졌습니다.',
|
|
bondExp: 28,
|
|
itemRewards: ['성도 항복 권고문 1']
|
|
},
|
|
{
|
|
id: 'prepare-city-officer-list',
|
|
label: '성도 장수 명단을 정리한다',
|
|
response: '이엄의 기억과 제갈량의 판단이 겹쳐지며, 다음 설득전에서 먼저 만날 장수들이 정해졌습니다.',
|
|
bondExp: 26,
|
|
gold: 440,
|
|
itemRewards: ['성도 장수 명단 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'chengdu-storehouse-order',
|
|
title: '성도 창고 정리',
|
|
location: '성도 남창',
|
|
availableAfterBattleIds: [campBattleIds.thirtyThird],
|
|
bondId: 'liu-bei__huang-quan',
|
|
description: '유비와 황권이 성도 창고를 열어 항복 병사와 백성의 배급 질서를 바로잡습니다.',
|
|
lines: [
|
|
'황권: 성도 창고는 익주의 배를 붙잡는 곳입니다. 여기서 불공평하다는 말이 나오면 항복은 금세 원망이 됩니다.',
|
|
'유비: 그렇다면 전리품보다 장부를 먼저 세우시오. 우리 병사도 그 질서를 따르게 하겠소.',
|
|
'창고 문 앞에는 칼보다 붓이 먼저 놓였고, 성도 백성은 새 군령이 약탈이 아님을 확인하기 시작했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'share-chengdu-rations',
|
|
label: '배급 기준을 공개한다',
|
|
response: '배급 기준이 성도 남창에 붙자 항복 병사와 백성의 불안이 조금씩 가라앉았습니다.',
|
|
bondExp: 30,
|
|
itemRewards: ['콩 8', '상처약 4']
|
|
},
|
|
{
|
|
id: 'preserve-chengdu-ledger',
|
|
label: '창고 장부를 보전한다',
|
|
response: '황권은 기존 장부를 태우지 않고 그대로 보전해, 유비군이 익주를 빼앗기만 한 것이 아님을 보였습니다.',
|
|
bondExp: 28,
|
|
gold: 480,
|
|
itemRewards: ['성도 창고 장부 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'shu-foundation-council',
|
|
title: '촉한 건국 준비 회의',
|
|
location: '성도 임시 관청',
|
|
availableAfterBattleIds: [campBattleIds.thirtyThird],
|
|
bondId: 'zhuge-liang__huang-quan',
|
|
description: '제갈량과 황권이 익주 수습 뒤 유비군이 국가의 틀로 나아가기 위한 첫 행정 과제를 정합니다.',
|
|
lines: [
|
|
'제갈량: 형주와 익주가 이어졌으니 이제 군영의 규칙만으로는 부족합니다. 법, 창고, 장수의 자리를 세워야 합니다.',
|
|
'황권: 익주 사람은 새 이름보다 오늘의 질서를 먼저 봅니다. 세금을 낮추고 병사의 약탈을 막는 것이 첫걸음입니다.',
|
|
'임시 관청의 등불 아래에서 다음 장의 목적은 전장 하나가 아니라 나라의 틀로 넓어졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-yizhou-stabilization',
|
|
label: '익주 안정책을 쓴다',
|
|
response: '제갈량과 황권은 세금, 군량, 항복 장수의 배치를 나누어 적으며 촉한 건국의 첫 초안을 만들었습니다.',
|
|
bondExp: 30,
|
|
itemRewards: ['익주 안정책 1']
|
|
},
|
|
{
|
|
id: 'prepare-hanzhong-road',
|
|
label: '한중 길을 검토한다',
|
|
response: '아직 이른 이야기였지만, 북쪽 관문과 한중으로 향하는 길이 지도 가장자리에 조용히 표시되었습니다.',
|
|
bondExp: 28,
|
|
gold: 500,
|
|
itemRewards: ['한중 길 초안 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jiameng-stable-mustering',
|
|
title: '가맹관 마구간 정비',
|
|
location: '가맹관 마구간',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFourth],
|
|
bondId: 'liu-bei__ma-chao',
|
|
description: '마초가 합류한 뒤 서량 기병의 말과 유비군의 보급 체계를 함께 맞춥니다.',
|
|
lines: [
|
|
'마초: 서량 말은 성질이 급합니다. 배불리 먹이고 달릴 길을 먼저 보여 주어야 힘을 냅니다.',
|
|
'유비: 우리 군영의 말과 사람도 그대의 길에 맞추어 배워야 하오. 북쪽 길은 이제 함께 열 길이오.',
|
|
'마구간에는 새로 들어온 서량 말의 숨소리가 가득했고, 병사들은 다음 출전 명단을 떠올리며 고삐를 고쳤습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'align-cavalry-feed',
|
|
label: '말먹이 기준을 맞춘다',
|
|
response: '서량 말의 먹이와 휴식 기준이 정리되자, 마초의 기병대가 유비군의 행군 속도에 맞춰 움직이기 시작했습니다.',
|
|
bondExp: 30,
|
|
itemRewards: ['콩 8', '탁주 3']
|
|
},
|
|
{
|
|
id: 'mark-cavalry-routes',
|
|
label: '기병 돌파로를 표시한다',
|
|
response: '마초가 지도 위에 기병이 달릴 수 있는 능선과 피해야 할 늪길을 표시했습니다.',
|
|
bondExp: 28,
|
|
gold: 520,
|
|
itemRewards: ['서량 말먹이 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'hanzhong-front-council',
|
|
title: '한중 전선 회의',
|
|
location: '가맹관 본막',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFourth],
|
|
bondId: 'zhao-yun__ma-chao',
|
|
description: '조운과 마초가 한중으로 이어지는 관문, 산길, 기병 운용을 놓고 다음 출전 구성을 논의합니다.',
|
|
lines: [
|
|
'조운: 가맹관을 넘었다고 길이 열린 것은 아닙니다. 좁은 산길에서는 빠른 말도 한 번 멈춰야 합니다.',
|
|
'마초: 멈출 곳을 알면 더 세게 달릴 수 있지요. 장로의 군세와 조조의 눈이 모두 북쪽 길에 걸려 있습니다.',
|
|
'본막의 지도 위에는 한중 전선과 촉한 건국의 다음 숙제가 한 줄로 이어졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'plan-hanzhong-scouting',
|
|
label: '한중 정찰을 준비한다',
|
|
response: '조운의 정찰 기준과 마초의 기병 감각이 합쳐져, 다음 장의 출전 후보가 더 분명해졌습니다.',
|
|
bondExp: 30,
|
|
itemRewards: ['한중 정찰도 1']
|
|
},
|
|
{
|
|
id: 'divide-pass-roles',
|
|
label: '관문 돌파 역할을 나눈다',
|
|
response: '조운은 후방을 지키고 마초는 전열을 찌르는 방식으로, 두 기병장의 역할이 정리되었습니다.',
|
|
bondExp: 28,
|
|
gold: 540,
|
|
itemRewards: ['가맹관 전술 기록 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yangping-storehouse-survey',
|
|
title: '양평관 창고 조사',
|
|
location: '양평관 외곽 창고',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFifth],
|
|
bondId: 'liu-bei__ma-dai',
|
|
description: '유비와 마대가 양평관 창고의 말먹이, 군량, 통행로를 확인해 한중 본전을 준비합니다.',
|
|
lines: [
|
|
'마대: 말은 창보다 먼저 먹이를 찾습니다. 이 창고를 잡으면 서량 기병도 한중 길에서 오래 버틸 수 있습니다.',
|
|
'유비: 군량이 있어야 백성의 것을 빼앗지 않소. 창고를 얻었다면 먼저 장부를 세워야 하오.',
|
|
'창고 안에는 말먹이와 마른 군량이 쌓여 있었고, 한중 본전을 위한 첫 보급 기준이 마련되었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'sort-horse-feed',
|
|
label: '말먹이를 분류한다',
|
|
response: '마대가 말먹이와 기병 장비를 따로 정리하자 서량 기병의 다음 출전 준비가 빨라졌습니다.',
|
|
bondExp: 30,
|
|
itemRewards: ['서량 말먹이 2', '콩 8']
|
|
},
|
|
{
|
|
id: 'record-storehouse-ledger',
|
|
label: '창고 장부를 남긴다',
|
|
response: '유비는 창고 장부를 군영에 공개해 양평관에서 얻은 물자가 약탈이 아니라 군율 아래 쓰이게 했습니다.',
|
|
bondExp: 28,
|
|
gold: 560,
|
|
itemRewards: ['양평관 창고 장부 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yangping-ridge-drill',
|
|
title: '양평관 능선 훈련',
|
|
location: '양평관 서쪽 능선',
|
|
availableAfterBattleIds: [campBattleIds.thirtyFifth],
|
|
bondId: 'ma-chao__ma-dai',
|
|
description: '마초와 마대가 좁은 능선에서 기병 돌격과 회수 훈련을 맞추며 다음 한중 전투의 기동 축을 만듭니다.',
|
|
lines: [
|
|
'마초: 넓은 벌판이 아니라도 말은 달릴 수 있다. 대신 멈출 때를 알아야 한다.',
|
|
'마대: 형님이 뚫으면 제가 뒤를 묶겠습니다. 능선에서는 한 걸음 늦은 창이 대오를 살립니다.',
|
|
'서쪽 능선에는 짧은 돌격과 빠른 회수가 반복되며, 서량 기병의 새 박자가 자리 잡았습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'drill-ridge-charge',
|
|
label: '능선 돌격을 연습한다',
|
|
response: '마초의 선봉과 마대의 후속 대열이 맞물리며 좁은 능선에서도 기병이 흔들리지 않게 되었습니다.',
|
|
bondExp: 32,
|
|
itemRewards: ['기병 훈련 기록 1']
|
|
},
|
|
{
|
|
id: 'set-recovery-signal',
|
|
label: '회수 신호를 정한다',
|
|
response: '깃발과 북소리로 회수 신호를 정하자, 서량 기병은 돌격 후 돌아오는 박자를 배웠습니다.',
|
|
bondExp: 30,
|
|
gold: 580,
|
|
itemRewards: ['탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'dingjun-ridge-survey',
|
|
title: '정군산 능선 조사',
|
|
location: '정군산 서쪽 능선',
|
|
availableAfterBattleIds: [campBattleIds.thirtySixth],
|
|
bondId: 'zhuge-liang__wang-ping',
|
|
description: '제갈량과 왕평이 정군산의 능선, 절벽, 보급로를 다시 확인해 한중 결전을 준비합니다.',
|
|
lines: [
|
|
'왕평: 이 능선은 보기보다 바람이 셉니다. 깃발이 흔들리는 방향만 봐도 적의 시야를 피할 수 있습니다.',
|
|
'제갈량: 바람과 길을 함께 적어 두면 다음 전투의 한 수가 됩니다.',
|
|
'두 사람은 산길을 하나씩 짚으며 한중 본전의 진형 후보를 정리했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'record-ridge-winds',
|
|
label: '능선 바람을 기록한다',
|
|
response: '제갈량은 왕평의 설명을 바탕으로 정군산 능선의 바람과 시야를 지형책에 보탰습니다.',
|
|
bondExp: 32,
|
|
itemRewards: ['산악 지형도 1']
|
|
},
|
|
{
|
|
id: 'secure-ridge-store',
|
|
label: '능선 보급소를 확보한다',
|
|
response: '왕평이 작은 보급소의 위치를 알려 주며 한중 결전에서 버틸 군량을 조금 더 확보했습니다.',
|
|
bondExp: 30,
|
|
gold: 620,
|
|
itemRewards: ['콩 10', '상처약 5']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'dingjun-veteran-drill',
|
|
title: '정군산 노장 훈련',
|
|
location: '정군산 중턱',
|
|
availableAfterBattleIds: [campBattleIds.thirtySixth],
|
|
bondId: 'huang-zhong__wang-ping',
|
|
description: '황충과 왕평이 고지 돌파와 산길 매복을 맞춰 보며 다음 한중 결전의 선봉 감각을 다듬습니다.',
|
|
lines: [
|
|
'황충: 젊은 장수들은 빠르게 오른다지만, 오래 버티는 법은 아직 모를 때가 많지.',
|
|
'왕평: 이 산은 오래 버티는 장수를 좋아합니다. 급한 발보다 끊기지 않는 숨이 중요합니다.',
|
|
'정군산 중턱에는 노장의 숨과 산길잡이의 눈이 맞물리며 새로운 훈련 길이 생겼습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'drill-high-ground-strike',
|
|
label: '고지 일격을 연습한다',
|
|
response: '황충은 왕평이 잡아 준 짧은 고지 길을 따라 치고 빠지는 훈련을 반복했습니다.',
|
|
bondExp: 32,
|
|
itemRewards: ['기병 훈련 기록 1']
|
|
},
|
|
{
|
|
id: 'set-old-guard-pace',
|
|
label: '노장 진군 속도를 맞춘다',
|
|
response: '왕평은 황충의 호흡에 맞춰 병사들의 오르막 속도를 정했고, 무리한 추격을 줄였습니다.',
|
|
bondExp: 30,
|
|
gold: 600,
|
|
itemRewards: ['탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'hanzhong-king-audience',
|
|
title: '한중왕 즉위 논의',
|
|
location: '한중 본막',
|
|
availableAfterBattleIds: [campBattleIds.thirtySeventh],
|
|
bondId: 'liu-bei__wang-ping',
|
|
description: '유비, 제갈량, 왕평이 한중의 이름을 어떻게 세우고 백성에게 어떤 약속을 먼저 보일지 논의합니다.',
|
|
lines: [
|
|
'제갈량: 한중을 얻은 뒤에는 군사보다 이름이 먼저 움직입니다. 이름이 있어야 백성이 어디에 기대야 하는지 압니다.',
|
|
'왕평: 한중 병사들은 새 명령보다 새 약속을 더 기다립니다.',
|
|
'유비는 한중왕의 이름이 칼끝의 승리만이 아니라 백성의 질서가 되어야 함을 마음에 새겼습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-king-edict',
|
|
label: '즉위 격문을 준비한다',
|
|
response: '제갈량은 유비의 뜻을 격문 초안으로 정리했고, 왕평은 한중 병사들이 이해할 말로 고쳐 적었습니다.',
|
|
bondExp: 34,
|
|
itemRewards: ['한중왕 격문 초안 1']
|
|
},
|
|
{
|
|
id: 'promise-local-relief',
|
|
label: '한중 구휼을 약속한다',
|
|
response: '유비는 창고를 먼저 백성과 병사 가족에게 열도록 명했고, 한중의 불안은 조금씩 잦아들었습니다.',
|
|
bondExp: 32,
|
|
gold: 720,
|
|
itemRewards: ['콩 12', '상처약 6']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'hanzhong-north-gate-supply',
|
|
title: '북문 보급 정비',
|
|
location: '한중 북쪽 창고',
|
|
availableAfterBattleIds: [campBattleIds.thirtySeventh],
|
|
bondId: 'fa-zheng__wang-ping',
|
|
description: '법정과 왕평이 조조군이 남긴 창고와 퇴로를 조사해 다음 국경 전선의 보급 기준을 세웁니다.',
|
|
lines: [
|
|
'법정: 이 창고가 비면 한중은 승리한 날부터 다시 흔들립니다.',
|
|
'왕평: 산길 창고는 큰 것 하나보다 작은 것 셋이 낫습니다. 막히는 길이 달마다 달라지기 때문입니다.',
|
|
'두 사람은 조조군이 남긴 표식과 한중 병사들의 기억을 맞춰 북문 보급망을 다시 그렸습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'split-mountain-stores',
|
|
label: '산길 창고를 나눈다',
|
|
response: '왕평은 보급을 작은 창고로 나누었고, 법정은 적이 한 번에 끊기 어렵도록 운송 순서를 바꾸었습니다.',
|
|
bondExp: 34,
|
|
itemRewards: ['한중 보급표 1', '탁주 4']
|
|
},
|
|
{
|
|
id: 'audit-cao-stores',
|
|
label: '조조군 창고를 조사한다',
|
|
response: '남겨진 장부에서 말먹이와 활시위가 발견되어, 다음 전선에서 기병과 궁병을 함께 운용할 여유가 생겼습니다.',
|
|
bondExp: 32,
|
|
gold: 760,
|
|
itemRewards: ['말먹이 2', '활시위 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jing-beacon-repair',
|
|
title: '형주 봉화대 보수',
|
|
location: '강릉 북쪽 봉화대',
|
|
availableAfterBattleIds: [campBattleIds.thirtyEighth],
|
|
bondId: 'guan-yu__ma-liang',
|
|
description: '관우와 마량이 강릉 북쪽 봉화대를 돌아보며, 위와 오의 움직임을 빠르게 알릴 길을 정비합니다.',
|
|
lines: [
|
|
'마량: 봉화가 늦으면 강릉은 적을 눈앞에 두고서야 알게 됩니다.',
|
|
'관우: 봉화대가 칼보다 먼저 움직여야 하겠군. 이곳은 반드시 고치겠소.',
|
|
'병사들은 젖은 장작을 걷어 내고 새 기둥을 세우며, 강변 초소의 신호 순서를 다시 익혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'raise-beacon-watch',
|
|
label: '봉화 감시조를 세운다',
|
|
response: '감시조가 밤낮으로 교대하게 되어 형주의 첫 경보선이 더 빨라졌습니다.',
|
|
bondExp: 32,
|
|
itemRewards: ['형주 봉화도 1']
|
|
},
|
|
{
|
|
id: 'share-warning-orders',
|
|
label: '경보 군령을 돌린다',
|
|
response: '마량은 각 마을에 같은 경보 문구를 전했고, 관우군의 대응 순서가 선명해졌습니다.',
|
|
bondExp: 30,
|
|
gold: 640,
|
|
itemRewards: ['상처약 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jing-river-patrol',
|
|
title: '강릉 나루 순찰',
|
|
location: '강릉 동쪽 나루',
|
|
availableAfterBattleIds: [campBattleIds.thirtyEighth],
|
|
bondId: 'guan-yu__mi-zhu',
|
|
description: '관우와 미축이 상선과 군량선을 함께 살피며, 다음 형주 전선을 위한 강변 보급을 다집니다.',
|
|
lines: [
|
|
'미축: 상선이 두려워하면 군량선도 늦어집니다. 나루의 신뢰가 전선의 속도입니다.',
|
|
'관우: 백성이 오가는 길을 지켜야 군도 부끄럽지 않소.',
|
|
'강변 상인들은 군량 표식을 새로 확인했고, 병사들은 짐을 옮기는 순서를 다시 맞췄습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'escort-river-merchants',
|
|
label: '상선 호위를 약속한다',
|
|
response: '나루 상인들이 길을 다시 열며 관우군의 강변 보급이 안정되었습니다.',
|
|
bondExp: 30,
|
|
gold: 700,
|
|
itemRewards: ['콩 10']
|
|
},
|
|
{
|
|
id: 'store-river-medicine',
|
|
label: '나루 약재를 비축한다',
|
|
response: '미축은 상처약과 탁주를 군량선 가까이에 나누어 실었고, 장기전 준비가 조금 더 단단해졌습니다.',
|
|
bondExp: 28,
|
|
itemRewards: ['상처약 5', '탁주 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fan-castle-river-lines',
|
|
title: '한수 물길 정찰',
|
|
location: '번성 남쪽 제방',
|
|
availableAfterBattleIds: [campBattleIds.thirtyNinth],
|
|
bondId: 'guan-yu__fa-zheng',
|
|
description: '관우와 법정이 번성 남쪽 제방을 살피며 다음 수공과 성문 공략을 준비합니다.',
|
|
lines: [
|
|
'법정: 이 둑은 평소에는 길이지만 비가 오면 성벽보다 큰 무기가 됩니다.',
|
|
'관우: 병사들이 물길을 알면 두려움이 줄겠지. 제방의 높이와 퇴로를 모두 표시하시오.',
|
|
'정찰대는 수위 표식과 둑의 약한 곳을 남겼고, 다음 전장에 쓸 물길 지도가 완성되어 갔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-flood-stakes',
|
|
label: '수위 말뚝을 세운다',
|
|
response: '한수의 수위 변화가 눈에 보이게 되어 다음 수공 준비가 더 구체화되었습니다.',
|
|
bondExp: 34,
|
|
itemRewards: ['한수 수위표 1']
|
|
},
|
|
{
|
|
id: 'secure-bank-retreat',
|
|
label: '제방 퇴로를 확보한다',
|
|
response: '법정은 진흙길과 마른길을 나누어 표시했고, 관우군의 강변 이동 순서가 안정되었습니다.',
|
|
bondExp: 32,
|
|
gold: 720,
|
|
itemRewards: ['상처약 5']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fan-castle-siege-storehouse',
|
|
title: '공성 보급창 정비',
|
|
location: '번성 외곽 보급창',
|
|
availableAfterBattleIds: [campBattleIds.thirtyNinth],
|
|
bondId: 'guan-yu__mi-zhu',
|
|
description: '관우와 미축이 외곽 보루에 남은 자재와 곡식을 정리해, 다음 장기전과 공성전에 대비합니다.',
|
|
lines: [
|
|
'미축: 성을 압박하려면 칼보다 먼저 밧줄, 나무, 약재, 곡식이 끊기지 않아야 합니다.',
|
|
'관우: 보급이 뒤에서 흔들리면 앞의 의지도 무뎌지오. 창고를 작게 나누어 잃을 때의 피해를 줄입시다.',
|
|
'병사들은 노획한 목재와 곡식을 나누어 실었고, 강변 보급창의 표식이 새로 붙었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'split-siege-stores',
|
|
label: '공성 자재를 나눈다',
|
|
response: '자재가 여러 보급창으로 나뉘어 다음 공성 준비가 끊기지 않게 되었습니다.',
|
|
bondExp: 32,
|
|
itemRewards: ['공성 목재 1', '탁주 3']
|
|
},
|
|
{
|
|
id: 'hire-river-carriers',
|
|
label: '강변 인부를 고용한다',
|
|
response: '강변 인부들이 보급선을 오가며 물자 이동을 도왔고, 병사들의 피로가 줄었습니다.',
|
|
bondExp: 30,
|
|
gold: 820,
|
|
itemRewards: ['콩 12']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'han-river-dike-repair',
|
|
title: '한수 제방 보수',
|
|
location: '한수 남쪽 둑',
|
|
availableAfterBattleIds: [campBattleIds.fortieth],
|
|
bondId: 'guan-yu__huang-quan',
|
|
description: '관우와 황권이 수공 뒤 약해진 제방을 살피며, 민심과 다음 포위전의 길을 동시에 다집니다.',
|
|
lines: [
|
|
'황권: 승리한 물길도 그대로 두면 아군의 뒤를 칩니다. 둑의 약한 곳을 먼저 막아야 합니다.',
|
|
'관우: 백성의 밭이 물에 잠기면 승리의 깃발도 가벼워지오. 제방을 보수하시오.',
|
|
'병사들은 젖은 흙을 걷어 내고 새 말뚝을 박으며, 다음 전장의 퇴로와 보급로를 다시 표시했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'repair-weak-bank',
|
|
label: '약한 둑을 먼저 막는다',
|
|
response: '제방의 약한 곳이 보수되어 강변 마을과 보급로가 조금 더 안정되었습니다.',
|
|
bondExp: 34,
|
|
itemRewards: ['제방 보수도 1']
|
|
},
|
|
{
|
|
id: 'mark-dry-routes',
|
|
label: '마른길을 표시한다',
|
|
response: '황권은 물이 빠지는 길을 표시했고, 관우군은 다음 포위전의 이동 순서를 다시 익혔습니다.',
|
|
bondExp: 32,
|
|
gold: 760,
|
|
itemRewards: ['상처약 5']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'han-river-prisoners',
|
|
title: '칠군 포로 정리',
|
|
location: '한수 임시 포로진',
|
|
availableAfterBattleIds: [campBattleIds.fortieth],
|
|
bondId: 'guan-yu__li-yan',
|
|
description: '관우와 이엄이 물에 젖은 포로와 노획 물자를 정리하며, 번성 포위 전의 후방 부담을 줄입니다.',
|
|
lines: [
|
|
'이엄: 포로를 굶기면 후방이 흔들리고, 너무 풀어 주면 적의 귀가 됩니다. 순서가 필요합니다.',
|
|
'관우: 항복한 병사를 함부로 대하면 의로움이 상하오. 다만 군령은 엄정해야 하오.',
|
|
'포로진에는 이름표와 식량 표식이 붙었고, 노획 물자는 공성 준비에 쓸 것과 백성에게 돌릴 것으로 나뉘었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-prisoner-units',
|
|
label: '포로 부대를 나눈다',
|
|
response: '포로들이 소대별로 정리되어 후방 혼란이 줄고, 이엄의 보급 관리가 더 쉬워졌습니다.',
|
|
bondExp: 32,
|
|
itemRewards: ['칠군 항복문 1', '콩 10']
|
|
},
|
|
{
|
|
id: 'return-flooded-tools',
|
|
label: '노획 농기구를 돌려준다',
|
|
response: '젖은 농기구가 강변 백성에게 돌아가자 수공 뒤의 민심이 조금 누그러졌습니다.',
|
|
bondExp: 30,
|
|
gold: 860,
|
|
itemRewards: ['탁주 3']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fan-siege-wall-works',
|
|
title: '번성 외성 자재 점검',
|
|
location: '번성 남문 공성진',
|
|
availableAfterBattleIds: [campBattleIds.fortyFirst],
|
|
bondId: 'guan-yu__huang-zhong',
|
|
description: '관우와 황충이 성벽 앞 공성진을 돌며 사다리, 방패, 궁병 엄호 위치를 다시 정합니다.',
|
|
lines: [
|
|
'황충: 성벽 아래에 오래 머무르면 병사들의 눈이 먼저 흔들립니다. 엄호 지점을 더 낮게 잡겠습니다.',
|
|
'관우: 성문을 두드리는 병사들이 하늘만 보게 두어서는 안 되오. 방패와 화살의 순서를 맞춥시다.',
|
|
'공성진에는 새 사다리와 큰 방패가 나뉘어 놓였고, 궁병들은 성벽 사각을 다시 익혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'reinforce-siege-ladders',
|
|
label: '사다리를 보강한다',
|
|
response: '공성 사다리가 보강되어 다음 성벽 압박 준비가 더 단단해졌습니다.',
|
|
bondExp: 34,
|
|
itemRewards: ['공성 사다리 1', '상처약 5']
|
|
},
|
|
{
|
|
id: 'set-cover-archers',
|
|
label: '엄호 궁병을 배치한다',
|
|
response: '황충의 궁병 배치가 성벽 아래 병사들의 부담을 줄였습니다.',
|
|
bondExp: 32,
|
|
gold: 880,
|
|
itemRewards: ['탁주 3']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fan-siege-river-watch',
|
|
title: '강동 정찰선 감시',
|
|
location: '강릉 동쪽 나루',
|
|
availableAfterBattleIds: [campBattleIds.fortyFirst],
|
|
bondId: 'guan-yu__ma-liang',
|
|
description: '관우와 마량이 강 건너 배의 움직임을 살피며, 번성 포위 중 비워진 후방의 위험을 점검합니다.',
|
|
lines: [
|
|
'마량: 성벽을 향한 깃발이 많아질수록 나루의 빈틈도 커집니다. 오늘 본 작은 배가 내일의 큰 소식이 될 수 있습니다.',
|
|
'관우: 강동이 움직인다면 성문 앞 승리도 반쪽이 되오. 나루마다 신호를 세우시오.',
|
|
'정찰병들은 밤 신호와 낮 깃발을 새로 맞췄고, 강변 마을에는 낯선 배를 알리는 절차가 전해졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'post-river-signals',
|
|
label: '나루 신호를 세운다',
|
|
response: '나루마다 신호 순서가 정리되어 강동 정찰선의 움직임을 더 빨리 알 수 있게 되었습니다.',
|
|
bondExp: 34,
|
|
itemRewards: ['강동 정찰 기록 1']
|
|
},
|
|
{
|
|
id: 'calm-river-villages',
|
|
label: '강변 마을을 안심시킨다',
|
|
response: '마량은 마을 어른들에게 군령을 설명했고, 관우군의 후방 민심이 조금 더 안정되었습니다.',
|
|
bondExp: 32,
|
|
gold: 920,
|
|
itemRewards: ['콩 12']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jing-rear-warehouse-ledgers',
|
|
title: '강릉 창고 장부 대조',
|
|
location: '강릉 동쪽 창고',
|
|
availableAfterBattleIds: [campBattleIds.fortySecond],
|
|
bondId: 'guan-yu__mi-zhu_rear',
|
|
description: '관우와 미축이 강릉 창고의 장부와 상선 출입 표식을 대조하며, 위장 척후가 남긴 빈틈을 찾습니다.',
|
|
lines: [
|
|
'미축: 숫자는 거짓말을 못하지만, 숫자를 적는 사람은 흔들릴 수 있습니다. 장부의 빈칸이 수상합니다.',
|
|
'관우: 창고 문을 지키는 병사만 탓할 일이 아니오. 절차를 바꾸고 다시 익히게 하시오.',
|
|
'창고 관리들은 상선 표식과 곡식 자루 수를 다시 맞췄고, 병사들은 밤 출입 순서를 새로 배웠습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'audit-warehouse-marks',
|
|
label: '상선 표식을 대조한다',
|
|
response: '상선 표식이 정리되어 강동 척후가 같은 길로 다시 숨어들기 어려워졌습니다.',
|
|
bondExp: 36,
|
|
itemRewards: ['강릉 창고 장부 1']
|
|
},
|
|
{
|
|
id: 'seal-night-storehouses',
|
|
label: '야간 창고를 봉한다',
|
|
response: '밤 창고 출입이 제한되며 군량 손실이 줄고, 병사들의 경계 순서가 분명해졌습니다.',
|
|
bondExp: 34,
|
|
gold: 940,
|
|
itemRewards: ['콩 14']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'jing-rear-river-rumors',
|
|
title: '강변 소문 수집',
|
|
location: '강릉 동쪽 나루 마을',
|
|
availableAfterBattleIds: [campBattleIds.fortySecond],
|
|
bondId: 'guan-yu__ma-liang_rear',
|
|
description: '관우와 마량이 나루 마을을 돌아보며, 강동의 문서와 상선이 남긴 말의 흐름을 모읍니다.',
|
|
lines: [
|
|
'마량: 배는 물길을 따라 오지만, 소문은 사람의 마음을 따라 먼저 들어옵니다.',
|
|
'관우: 성문 앞에서 이긴 뒤 백성의 귀를 잃으면 큰일이오. 작은 말도 모두 적어 두시오.',
|
|
'마을 어른들은 낯선 상인의 말투와 가격 변화를 조심스럽게 전했고, 정찰병은 나루 신호를 다시 맞췄습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'map-rumor-routes',
|
|
label: '소문의 길을 표시한다',
|
|
response: '마량은 소문이 퍼지는 길을 지도에 적었고, 강동의 다음 침투 후보지가 조금 더 선명해졌습니다.',
|
|
bondExp: 36,
|
|
itemRewards: ['강동 정찰 기록 1']
|
|
},
|
|
{
|
|
id: 'reassure-river-elders',
|
|
label: '마을 어른을 안심시킨다',
|
|
response: '관우군은 백성을 압박하지 않겠다고 약속했고, 나루 마을의 경계심이 조금 누그러졌습니다.',
|
|
bondExp: 34,
|
|
gold: 980,
|
|
itemRewards: ['상처약 5']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'gongan-collapse-gate-ledgers',
|
|
title: '공안 성문 군령 대조',
|
|
location: '공안 서문 군영',
|
|
availableAfterBattleIds: [campBattleIds.fortyThird],
|
|
bondId: 'mi-zhu__huang-quan_collapse',
|
|
description: '미축과 황권이 공안 성문 군령과 창고 장부를 대조하며, 강동 설객이 흔든 빈틈을 찾습니다.',
|
|
lines: [
|
|
'황권: 성문을 여닫은 기록과 창고 출입 기록이 서로 맞지 않습니다. 누군가 군령을 핑계로 빈칸을 만들었습니다.',
|
|
'미축: 장부를 다시 쓰면 숫자는 맞출 수 있지만, 이번에는 누가 왜 숨겼는지를 보아야 합니다.',
|
|
'성문 관리들은 침묵했지만, 선박 명부와 야간 군령 사이의 작은 차이가 드러났습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'trace-gate-orders',
|
|
label: '성문 군령을 추적한다',
|
|
response: '성문 군령의 경로가 정리되어 공안 병사들이 강동의 거짓 명령에 흔들릴 가능성이 줄었습니다.',
|
|
bondExp: 38,
|
|
itemRewards: ['공안 성문 기록 1']
|
|
},
|
|
{
|
|
id: 'protect-witnesses',
|
|
label: '증언자를 보호한다',
|
|
response: '황권은 성급한 문책을 막고 증언자를 보호했으며, 미축은 숨은 장부를 더 안전하게 확보했습니다.',
|
|
bondExp: 36,
|
|
gold: 1060,
|
|
itemRewards: ['상처약 6']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'gongan-collapse-hidden-fords',
|
|
title: '갈대밭 물목 조사',
|
|
location: '공안 남쪽 갈대 나루',
|
|
availableAfterBattleIds: [campBattleIds.fortyThird],
|
|
bondId: 'zhao-yun__wang-ping_collapse',
|
|
description: '조운과 왕평이 강동 수군이 숨어든 갈대밭 물목을 조사하며, 다음 고립 전투의 차단 경로를 찾습니다.',
|
|
lines: [
|
|
'왕평: 이 물목은 큰 배가 못 지나갑니다. 그러나 작은 배와 백의 병사라면 밤에 충분히 넘어올 수 있습니다.',
|
|
'조운: 말이 닿지 않는 곳은 발로 익히면 되오. 다음에 저들이 오면 이 길부터 막겠소.',
|
|
'정찰병들은 갈대밭 사이에 작은 표식을 세우고, 나루마다 다른 횃불 신호를 정했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'map-hidden-fords',
|
|
label: '숨은 물목을 지도에 남긴다',
|
|
response: '왕평은 물목 지도를 완성했고, 조운의 기동대는 다음 전투에서 먼저 차단할 길을 알게 되었습니다.',
|
|
bondExp: 38,
|
|
itemRewards: ['갈대밭 물목도 1']
|
|
},
|
|
{
|
|
id: 'train-ferry-blockade',
|
|
label: '나루 차단 훈련을 한다',
|
|
response: '조운의 기동대가 나루 차단 훈련을 마치자, 공안 남쪽 경계가 한층 빨라졌습니다.',
|
|
bondExp: 36,
|
|
gold: 1040,
|
|
itemRewards: ['탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'maicheng-north-gate-scout',
|
|
title: '맥성 북문 돌파로 정찰',
|
|
location: '맥성 북문 둔덕',
|
|
availableAfterBattleIds: [campBattleIds.fortyFourth],
|
|
bondId: 'guan-yu__wang-ping_maicheng',
|
|
description: '관우와 왕평이 맥성 북문 밖의 둔덕, 마른 해자, 다시 닫힐 포위선을 살피며 다음 돌파 지점을 정합니다.',
|
|
lines: [
|
|
'왕평: 이 둔덕에 깃발을 세우면 성안 병사들이 밖의 길을 볼 수 있습니다.',
|
|
'관우: 보이는 길이 있어야 마음도 따르겠지. 다만 적도 이 깃발을 먼저 노릴 것이오.',
|
|
'정찰병들은 둔덕과 해자 사이에 작은 표식을 묻고, 돌파 뒤 다시 모일 위치를 정했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'raise-north-gate-marker',
|
|
label: '북문 표식을 세운다',
|
|
response: '맥성 북문 밖 집결 표식이 세워져, 포위망을 빠져나온 병사들이 다시 모일 길을 알게 되었습니다.',
|
|
bondExp: 40,
|
|
itemRewards: ['맥성 북문 지도 1']
|
|
},
|
|
{
|
|
id: 'brace-dry-moat-line',
|
|
label: '마른 해자 방어선을 정한다',
|
|
response: '왕평은 마른 해자의 굴곡을 따라 예비 방어선을 정했고, 관우군은 돌파 실패 때 버틸 길을 얻었습니다.',
|
|
bondExp: 38,
|
|
gold: 1120,
|
|
itemRewards: ['상처약 6']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'maicheng-reed-ford-cache',
|
|
title: '갈대 물목 보급 은닉',
|
|
location: '맥성 동쪽 갈대 물목',
|
|
availableAfterBattleIds: [campBattleIds.fortyFourth],
|
|
bondId: 'zhao-yun__ma-dai_maicheng',
|
|
description: '조운과 마대가 맥성 동쪽 갈대 물목에 작은 보급품을 숨기고, 위군 기병을 흔들 차단로를 정리합니다.',
|
|
lines: [
|
|
'조운: 길을 알더라도 오래 버티려면 물과 약이 필요하오.',
|
|
'마대: 갈대 아래 낮은 땅은 적이 쉽게 뒤지지 않습니다. 다만 말이 빠지지 않게 표시를 작게 남겨야 합니다.',
|
|
'기병들은 갈대 물목에 낮은 표식을 남기고, 추격대가 들어올 때 말머리를 돌릴 지점을 익혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'hide-reed-supplies',
|
|
label: '갈대밭에 보급을 숨긴다',
|
|
response: '갈대 물목에 상처약과 탁주가 숨겨져, 다음 전투의 장기전 대비가 조금 나아졌습니다.',
|
|
bondExp: 38,
|
|
itemRewards: ['상처약 5', '탁주 3']
|
|
},
|
|
{
|
|
id: 'cut-cavalry-turning-path',
|
|
label: '기병 회전로를 끊는다',
|
|
response: '마대는 위군 기병이 말머리를 돌릴 길을 미리 끊었고, 조운은 빠져나갈 좁은 길을 표시했습니다.',
|
|
bondExp: 36,
|
|
gold: 1080,
|
|
itemRewards: ['갈대 물목 표식 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yiling-river-camp-lines',
|
|
title: '이릉 강변 군막 간격 점검',
|
|
location: '이릉 서쪽 강변 진영',
|
|
availableAfterBattleIds: [campBattleIds.fortyFifth],
|
|
bondId: 'zhuge-liang__huang-quan_yiling',
|
|
description: '제갈량과 황권이 강변 진영의 군막 간격, 물길, 불길이 번질 수 있는 마른 풀밭을 점검합니다.',
|
|
lines: [
|
|
'황권: 군막이 강을 따라 길게 늘어서면 보급은 편하지만, 불길을 끊을 지점이 줄어듭니다.',
|
|
'제갈량: 적이 싸우지 않고 기다릴 때는, 우리가 스스로 약해지는 모양을 먼저 살펴야 합니다.',
|
|
'병사들은 군막 사이의 마른 풀을 걷어 내고, 물동이를 두는 지점을 새로 표시했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'clear-dry-grass-lines',
|
|
label: '마른 풀밭을 걷어낸다',
|
|
response: '강변 군막 사이의 마른 풀이 치워졌고, 화공 위험을 낮출 작은 방화선이 생겼습니다.',
|
|
bondExp: 40,
|
|
itemRewards: ['강변 진영 말뚝 1']
|
|
},
|
|
{
|
|
id: 'recount-river-supply',
|
|
label: '강변 보급을 재점검한다',
|
|
response: '황권은 보급 수량을 다시 세고, 제갈량은 물길마다 예비 병력을 세울 위치를 정했습니다.',
|
|
bondExp: 38,
|
|
gold: 1160,
|
|
itemRewards: ['상처약 6']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yiling-forward-scouts',
|
|
title: '동진로 전초 정찰',
|
|
location: '이릉 동쪽 숲길',
|
|
availableAfterBattleIds: [campBattleIds.fortyFifth],
|
|
bondId: 'zhao-yun__ma-chao_yiling',
|
|
description: '조운과 마초가 동쪽 숲길의 빈 공간과 오군의 유인 흔적을 살피며 기병 신호를 정합니다.',
|
|
lines: [
|
|
'마초: 적이 비운 숲길은 달리기 좋지만, 너무 좋은 길은 대개 함정입니다.',
|
|
'조운: 길을 의심하되 멈추지는 않겠소. 우리가 먼저 흔들리면 뒤의 보병도 늦어집니다.',
|
|
'정찰 기병들은 숲길 가장자리에 작은 천 조각을 묶어, 돌아올 때 놓치지 않을 신호를 남겼습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-cavalry-signals',
|
|
label: '기병 신호를 남긴다',
|
|
response: '동진로의 숲길마다 작은 기병 신호가 남아, 촉한 선봉이 깊이 들어가도 서로의 위치를 더 쉽게 확인하게 되었습니다.',
|
|
bondExp: 38,
|
|
itemRewards: ['이릉 동진 지도 1']
|
|
},
|
|
{
|
|
id: 'test-empty-forest-road',
|
|
label: '빈 숲길을 시험한다',
|
|
response: '조운과 마초는 일부러 빈 길을 시험해 보며, 오군이 숨기려는 매복 지점을 몇 곳 찾아냈습니다.',
|
|
bondExp: 36,
|
|
gold: 1120,
|
|
itemRewards: ['탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yiling-firebreak-lines',
|
|
title: '화공 방화선 수습',
|
|
location: '이릉 불탄 군막',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
bondId: 'zhuge-liang__ma-liang_yiling-fire',
|
|
description: '제갈량과 마량이 불탄 군막 사이에 남은 방화선을 정리하고, 다음 전투에서 반복하지 말아야 할 배치를 기록합니다.',
|
|
lines: [
|
|
'마량: 여기서 군막과 마른 풀이 너무 가까웠습니다. 물동이는 있었지만 불길을 끊을 빈 땅이 부족했습니다.',
|
|
'제갈량: 이 기록은 패배의 부끄러움이 아니라 다음 병사를 살리는 약입니다.',
|
|
'병사들은 재가 된 군막 터에 낮은 말뚝을 세우고, 다시는 같은 간격으로 진을 치지 않도록 표시했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-firebreak-spacing',
|
|
label: '방화선 간격을 새긴다',
|
|
response: '방화선 간격 표식이 정리되어, 다음 군영 배치에서 화공 대비가 더 분명해졌습니다.',
|
|
bondExp: 40,
|
|
itemRewards: ['화공 방화선 표식 1']
|
|
},
|
|
{
|
|
id: 'recover-camp-records',
|
|
label: '군막 기록을 수습한다',
|
|
response: '마량은 타다 남은 배치 기록을 건졌고, 제갈량은 불길이 번진 순서를 다시 맞추었습니다.',
|
|
bondExp: 38,
|
|
gold: 1220,
|
|
itemRewards: ['상처약 6']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'yiling-fire-baidi-retreat-route',
|
|
title: '백제성 퇴로 정찰',
|
|
location: '서쪽 협곡 물길',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
bondId: 'zhao-yun__wang-ping_yiling-fire',
|
|
description: '조운과 왕평이 백제성으로 향하는 협곡 물길을 다시 훑고, 흩어진 병사들이 모일 표식을 남깁니다.',
|
|
lines: [
|
|
'왕평: 강가의 낮은 돌을 따라가면 밤에도 길을 잃지 않습니다. 다만 추격 기병이 들어오면 먼저 막히는 곳입니다.',
|
|
'조운: 내가 뒤를 끊고, 그대는 앞의 표식을 맡으시오. 병사들이 길을 보면 다시 걸을 수 있소.',
|
|
'정찰대는 협곡 입구마다 작은 천 조각을 묶고, 백제성으로 향하는 물길을 낮게 표시했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'place-baidi-route-markers',
|
|
label: '백제성 표식을 남긴다',
|
|
response: '백제성으로 향하는 물길에 표식이 남아, 흩어진 병사들이 서쪽으로 모일 가능성이 높아졌습니다.',
|
|
bondExp: 39,
|
|
itemRewards: ['백제성 퇴로 지도 1']
|
|
},
|
|
{
|
|
id: 'delay-wu-pursuit',
|
|
label: '오군 추격로를 늦춘다',
|
|
response: '조운은 좁은 길목에 작은 장애물을 놓았고, 왕평은 우회로를 지워 추격대의 속도를 늦추었습니다.',
|
|
bondExp: 37,
|
|
gold: 1240,
|
|
itemRewards: ['탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'baidi-regency-council',
|
|
title: '섭정 의정 정리',
|
|
location: '백제성 내전',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
availableDuringSteps: ['baidi-entrustment-camp'],
|
|
bondId: 'zhuge-liang__ma-liang_yiling-fire',
|
|
description: '제갈량과 마량이 백제성 내전에서 유탁 이후의 조정 순서와 장수들의 역할을 정리합니다.',
|
|
lines: [
|
|
'마량: 장수들이 모두 슬픔 속에 있습니다. 첫 의정에서 책임을 묻기보다 각자의 자리를 먼저 알려야 합니다.',
|
|
'제갈량: 나라가 흔들릴 때는 말의 순서가 군령이 됩니다. 백제성에서 나갈 첫 문서를 가볍게 쓸 수 없습니다.',
|
|
'내전의 등불 아래, 촉한의 다음 명령서와 장수 배치안이 조용히 정리되었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-regency-order',
|
|
label: '섭정 명령서를 쓴다',
|
|
response: '섭정 명령서가 준비되어, 백제성 이후 조정과 군영이 한 목소리로 움직일 기반이 생겼습니다.',
|
|
bondExp: 42,
|
|
gold: 1280,
|
|
itemRewards: ['섭정 의정 초안 1']
|
|
},
|
|
{
|
|
id: 'assign-officer-places',
|
|
label: '장수들의 자리를 나눈다',
|
|
response: '마량은 장수별 임무를 정리했고, 제갈량은 급히 움직일 부대와 성 안에 남을 부대를 구분했습니다.',
|
|
bondExp: 40,
|
|
itemRewards: ['상처약 7']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'baidi-gate-recovery',
|
|
title: '백제성 성문 수습',
|
|
location: '백제성 서문',
|
|
availableAfterBattleIds: [campBattleIds.fortySixth],
|
|
availableDuringSteps: ['baidi-entrustment-camp'],
|
|
bondId: 'zhao-yun__wang-ping_yiling-fire',
|
|
description: '조운과 왕평이 백제성 서문에서 돌아오는 패잔병과 부상병의 흐름을 나누고, 추격 정찰을 막습니다.',
|
|
lines: [
|
|
'왕평: 서문으로 한꺼번에 몰리면 부상병이 먼저 눌립니다. 낮은 길과 높은 길을 나누어 들여야 합니다.',
|
|
'조운: 내가 성 밖을 맡겠소. 추격 정찰이 섞여 들어오지 못하게 끝까지 살피겠소.',
|
|
'서문 앞에는 부상병, 기병, 군량 수레가 따로 들어오는 표식이 세워졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-wounded-gate',
|
|
label: '부상병 통로를 나눈다',
|
|
response: '부상병 통로가 따로 열려, 백제성 안의 혼란이 줄고 병력 회복 준비가 빨라졌습니다.',
|
|
bondExp: 40,
|
|
itemRewards: ['상처약 8']
|
|
},
|
|
{
|
|
id: 'screen-wu-scouts',
|
|
label: '오군 정찰을 걸러낸다',
|
|
response: '조운과 왕평은 성문 밖 검문선을 세워, 오군 척후가 패잔병 틈에 섞이는 일을 막았습니다.',
|
|
bondExp: 38,
|
|
gold: 1260,
|
|
itemRewards: ['탁주 5']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'nanzhong-village-reassurance',
|
|
title: '남중 마을 회유',
|
|
location: '남중 산마을',
|
|
availableAfterBattleIds: [campBattleIds.fortySeventh],
|
|
bondId: 'ma-liang__wang-ping_nanzhong',
|
|
description: '마량과 왕평이 산마을을 돌며 반란군과 마을 사람을 구분하고, 다음 진군로의 길잡이를 부탁합니다.',
|
|
lines: [
|
|
'마량: 군이 지나간 뒤에도 밭이 남아야 마을이 다시 우리 편이 됩니다.',
|
|
'왕평: 길잡이를 억지로 데려가면 다음 길은 모두 거짓이 됩니다. 먼저 지켜 준다는 표시가 필요합니다.',
|
|
'마을 앞 작은 사당에 촉한군의 보호 표식과 길 안내 깃발이 함께 세워졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'promise-village-protection',
|
|
label: '보호 표식을 세운다',
|
|
response: '마을 사람들은 군량 징발 대신 보호 표식이 먼저 세워지는 것을 보고 조금씩 길을 알려 주었습니다.',
|
|
bondExp: 42,
|
|
itemRewards: ['남중 길잡이 표식 1']
|
|
},
|
|
{
|
|
id: 'return-stored-grain',
|
|
label: '빼앗긴 곡식을 돌려준다',
|
|
response: '왕평은 창고에서 찾은 곡식을 되돌렸고, 마량은 반란군과 마을 사람의 이름을 구분해 적었습니다.',
|
|
bondExp: 40,
|
|
gold: 1180,
|
|
itemRewards: ['콩 9']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'nanzhong-supply-road-ledger',
|
|
title: '남중 보급로 장부',
|
|
location: '남중 산길 창고',
|
|
availableAfterBattleIds: [campBattleIds.fortySeventh],
|
|
bondId: 'zhuge-liang__huang-quan_nanzhong',
|
|
description: '제갈량과 황권이 산길 창고에서 군량, 포로, 회유 대상의 장부를 나누어 남중 안정의 다음 순서를 정합니다.',
|
|
lines: [
|
|
'황권: 이 창고에는 반란군의 군량과 마을의 생계가 함께 섞여 있습니다.',
|
|
'제갈량: 가져갈 것과 돌려줄 것을 나누는 일이 다음 싸움의 절반입니다.',
|
|
'창고 안의 죽간에는 토벌할 수장, 달랠 촌장, 보호할 마을의 이름이 따로 적혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-grain-ledgers',
|
|
label: '군량 장부를 나눈다',
|
|
response: '황권은 촉한군이 쓸 군량과 마을에 돌려줄 곡식을 나누어, 병사들의 약탈을 막을 기준을 세웠습니다.',
|
|
bondExp: 42,
|
|
gold: 1360,
|
|
itemRewards: ['남중 산길 지도 1']
|
|
},
|
|
{
|
|
id: 'mark-amnesty-targets',
|
|
label: '회유 대상을 표시한다',
|
|
response: '제갈량은 끝까지 반란을 부추긴 수장과 달랠 수 있는 촌장을 분리해, 다음 전투의 목표를 더 선명하게 만들었습니다.',
|
|
bondExp: 40,
|
|
itemRewards: ['마을 회유 문서 2']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'menghuo-captive-camp-reassurance',
|
|
title: '포로 수용소 안정',
|
|
location: '남중 포로 수용소',
|
|
availableAfterBattleIds: [campBattleIds.fortyEighth],
|
|
bondId: 'huang-quan__wang-ping_menghuo',
|
|
description: '황권과 왕평이 포로 수용소를 돌며 반란군, 끌려온 주민, 길잡이를 구분하고 석방 순서를 정합니다.',
|
|
lines: [
|
|
'왕평: 묶인 사람들 중에는 병사보다 마을 사람이 많습니다. 모두 적으로 두면 다음 산길이 닫힙니다.',
|
|
'황권: 명부를 세 갈래로 나누겠습니다. 죄를 물을 자, 돌려보낼 자, 길 안내를 부탁할 자를 섞지 않겠습니다.',
|
|
'수용소의 말뚝에는 처벌보다 먼저 귀환 순서를 알리는 작은 패가 걸렸습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'release-dragged-villagers',
|
|
label: '끌려온 주민을 돌려보낸다',
|
|
response: '왕평은 돌아갈 산길을 안내했고, 황권은 주민들의 이름을 명부에서 따로 지워 원한을 줄였습니다.',
|
|
bondExp: 44,
|
|
itemRewards: ['남중 포로 명부 1']
|
|
},
|
|
{
|
|
id: 'sort-rebel-captives',
|
|
label: '반란군 포로를 분리한다',
|
|
response: '황권은 반란군 포로를 수장별로 나누었고, 왕평은 서로 다른 마을 사람이 함께 묶이지 않도록 조정했습니다.',
|
|
bondExp: 42,
|
|
gold: 1420,
|
|
itemRewards: ['상처약 9']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'menghuo-southern-market-reopen',
|
|
title: '남중 장터 재개',
|
|
location: '남중 산마을 장터',
|
|
availableAfterBattleIds: [campBattleIds.fortyEighth],
|
|
bondId: 'zhuge-liang__ma-liang_menghuo',
|
|
description: '제갈량과 마량이 전투 뒤 닫힌 장터를 다시 열어, 촉한군이 약탈군이 아니라 질서를 되돌리는 군대임을 보입니다.',
|
|
lines: [
|
|
'마량: 장터가 닫힌 채로 오래가면 마을 사람들은 산으로 숨고, 반란군은 다시 그 틈을 파고들 것입니다.',
|
|
'제갈량: 군량은 값을 치르고 사십시오. 오늘 장터에서 보이는 태도가 다음 전장의 말보다 빠르게 퍼집니다.',
|
|
'장터에는 촉한군의 보호 표식과 물품 가격표가 함께 걸렸습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'pay-for-supplies',
|
|
label: '군량값을 치른다',
|
|
response: '제갈량은 군량값을 먼저 지급하게 했고, 마량은 그 사실을 마을 어른들 앞에서 분명히 알렸습니다.',
|
|
bondExp: 44,
|
|
itemRewards: ['콩 12']
|
|
},
|
|
{
|
|
id: 'post-market-rules',
|
|
label: '장터 군령을 게시한다',
|
|
response: '마량은 약탈 금지와 보호 구역을 적은 군령을 걸었고, 제갈량은 이를 어기는 병사를 엄히 다스리겠다고 밝혔습니다.',
|
|
bondExp: 42,
|
|
itemRewards: ['맹획 회유 서신 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'second-capture-release-ledger',
|
|
title: '석방 장부 확인',
|
|
location: '남중 포로장',
|
|
availableAfterBattleIds: [campBattleIds.fortyNinth],
|
|
bondId: 'zhuge-liang__huang-quan_second-capture',
|
|
description: '제갈량과 황권이 두 번째로 붙잡힌 맹획을 다시 놓아 보내기 전, 포로와 군량 장부를 공개해 군율을 확인합니다.',
|
|
lines: [
|
|
'황권: 이번에는 포로들이 직접 명부를 보게 하겠습니다. 숨기는 것이 없다는 사실이 돌아가는 길의 말이 됩니다.',
|
|
'제갈량: 좋습니다. 맹획도 보게 하십시오. 우리가 사람을 얻으려는지 땅을 빼앗으려는지, 그가 직접 판단하게 해야 합니다.',
|
|
'포로장 앞에는 석방 순서와 군량 지급량이 적힌 목패가 세워졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'open-captive-ledger',
|
|
label: '장부를 공개한다',
|
|
response: '황권은 포로 명부를 공개했고, 제갈량은 석방과 처벌의 기준이 군령에 따른 것임을 알렸습니다.',
|
|
bondExp: 46,
|
|
itemRewards: ['포로 안심 장부 1']
|
|
},
|
|
{
|
|
id: 'ration-returnees',
|
|
label: '귀환 군량을 나눈다',
|
|
response: '제갈량은 돌아가는 포로에게 최소한의 군량을 지급하게 했고, 황권은 지출을 빠짐없이 장부에 남겼습니다.',
|
|
bondExp: 44,
|
|
gold: 1480,
|
|
itemRewards: ['콩 14']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'second-capture-village-route',
|
|
title: '귀환 산길 보호',
|
|
location: '남중 산마을 길목',
|
|
availableAfterBattleIds: [campBattleIds.fortyNinth],
|
|
bondId: 'ma-liang__wang-ping_second-capture',
|
|
description: '마량과 왕평이 맹획이 돌아갈 길목의 마을을 돌며 촉한군의 보호 표식과 우회로를 정리합니다.',
|
|
lines: [
|
|
'왕평: 이 길을 군대가 밟으면 밭이 무너집니다. 조금 돌아가더라도 낮은 둑길을 써야 합니다.',
|
|
'마량: 마을이 지켜졌다는 기억이 다음 회유의 시작입니다. 군령을 길목마다 붙이겠습니다.',
|
|
'마을 어른들은 촉한군이 남긴 보호 표식과 우회로 표식을 함께 확인했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'protect-field-detour',
|
|
label: '밭길 우회로를 정한다',
|
|
response: '왕평은 병사들이 밟지 말아야 할 밭길을 표시했고, 마량은 우회 명령을 마을 사람 앞에서 읽었습니다.',
|
|
bondExp: 44,
|
|
itemRewards: ['남중 산길 지도 1']
|
|
},
|
|
{
|
|
id: 'post-amnesty-boards',
|
|
label: '회유 목패를 세운다',
|
|
response: '마량은 약탈 금지와 보호 약속을 적은 목패를 세웠고, 왕평은 맹획이 지나갈 길목을 비워 두었습니다.',
|
|
bondExp: 46,
|
|
itemRewards: ['칠종칠금 회유문 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'third-capture-clan-council',
|
|
title: '호족 회유 회의',
|
|
location: '남중 임시 회의장',
|
|
availableAfterBattleIds: [campBattleIds.fiftieth],
|
|
bondId: 'zhuge-liang__ma-liang_third-capture',
|
|
description: '제갈량과 마량이 세 번째 생포 뒤 흔들리는 호족들에게 보낼 설득문과 다음 회의의 우선순위를 정합니다.',
|
|
lines: [
|
|
'마량: 대래동주와 아회남의 말이 달라졌습니다. 누구를 먼저 달래느냐에 따라 다음 산채의 문이 달라질 것입니다.',
|
|
'제갈량: 항복을 구걸하지는 않습니다. 다만 길을 남겨 두어 스스로 돌아오게 해야 합니다.',
|
|
'회의장 한쪽에는 호족별 이름표와 회유 문서가 나란히 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prioritize-wavering-clans',
|
|
label: '흔들리는 호족을 먼저 본다',
|
|
response: '마량은 아회남과 주변 촌장에게 보낼 문서를 먼저 정리했고, 제갈량은 그들에게 보낼 사자를 골랐습니다.',
|
|
bondExp: 48,
|
|
itemRewards: ['남중 호족 설득문 1']
|
|
},
|
|
{
|
|
id: 'separate-hardliners',
|
|
label: '강경파를 분리한다',
|
|
response: '제갈량은 끝까지 선동하는 호족과 달랠 수 있는 호족을 나누었고, 마량은 다음 회의의 질문 목록을 적었습니다.',
|
|
bondExp: 46,
|
|
gold: 1520,
|
|
itemRewards: ['세 번째 회유 장부 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'third-capture-open-routes',
|
|
title: '열어 둔 산길',
|
|
location: '남중 산채 길목',
|
|
availableAfterBattleIds: [campBattleIds.fiftieth],
|
|
bondId: 'huang-quan__wang-ping_third-capture',
|
|
description: '황권과 왕평이 세 번의 전투 동안 닫지 않은 귀환로를 정리해, 다음 전투의 회유 목표로 삼을 길을 고릅니다.',
|
|
lines: [
|
|
'왕평: 이 길을 열어 두면 호족 일부가 싸움을 멈추고 물러날 수 있습니다. 대신 포위는 느슨해집니다.',
|
|
'황권: 그렇다면 그 길로 나간 자가 다시 창을 들지 않도록 장부와 보증이 필요합니다.',
|
|
'산채 길목에는 닫을 길과 열어 둘 길이 서로 다른 색으로 표시되었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'leave-amnesty-path',
|
|
label: '회유 퇴로를 남긴다',
|
|
response: '왕평은 마을로 돌아갈 낮은 길을 표시했고, 황권은 그 길로 돌아간 자의 이름을 따로 적기로 했습니다.',
|
|
bondExp: 46,
|
|
itemRewards: ['남중 산길 지도 1']
|
|
},
|
|
{
|
|
id: 'guard-captive-road',
|
|
label: '포로 귀환로를 지킨다',
|
|
response: '황권은 포로 귀환 명부를 갱신했고, 왕평은 병사들이 그 길을 막지 않도록 표식을 세웠습니다.',
|
|
bondExp: 48,
|
|
itemRewards: ['상처약 10']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fourth-capture-ledger-seal',
|
|
title: '보증 장부 봉인',
|
|
location: '남중 포로장',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFirst],
|
|
bondId: 'huang-quan__wang-ping_fourth-capture',
|
|
description: '황권과 왕평이 네 번째 생포 뒤 회유 퇴로를 지나간 호족과 포로의 이름을 따로 봉인합니다.',
|
|
lines: [
|
|
'황권: 이 장부는 항복한 자를 벌하기 위한 것이 아니라, 약속을 지키기 위한 증거입니다.',
|
|
'왕평: 길목마다 같은 표식을 세우겠습니다. 병사도 호족도 이 선을 넘지 말아야 합니다.',
|
|
'포로장 한가운데에는 봉인된 장부와 회유 퇴로 표식이 함께 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-clan-guarantees',
|
|
label: '호족 보증을 봉한다',
|
|
response: '황권은 보증 장부를 봉했고, 왕평은 퇴로 표식을 각 길목에 나누어 보냈습니다.',
|
|
bondExp: 50,
|
|
itemRewards: ['호족 보증 장부 1']
|
|
},
|
|
{
|
|
id: 'mark-returning-families',
|
|
label: '돌아간 가족을 표시한다',
|
|
response: '왕평은 귀환한 가족들의 마을을 표시했고, 황권은 병사들이 그 마을을 징발하지 못하게 했습니다.',
|
|
bondExp: 48,
|
|
gold: 1680,
|
|
itemRewards: ['회유 퇴로 표식 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fourth-capture-fire-forts',
|
|
title: '화덕 요새의 불씨',
|
|
location: '남중 화덕 요새',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFirst],
|
|
bondId: 'ma-chao__ma-dai_fourth-capture',
|
|
description: '마초와 마대가 등갑병의 화덕 요새를 살펴, 다음 전투에서 불씨가 마을로 번지지 않게 정리합니다.',
|
|
lines: [
|
|
'마초: 화덕은 무기로도 쓰이고 겁주는 표식으로도 쓰이는군.',
|
|
'마대: 불씨를 모두 꺼 버리면 산마을의 밤도 위험합니다. 쓸 곳과 막을 곳을 나누겠습니다.',
|
|
'요새 주변에는 꺼진 화덕과 남겨 둘 신호불이 따로 표시되었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'separate-war-fires',
|
|
label: '전투 화덕을 분리한다',
|
|
response: '마초는 전투용 화덕을 치웠고, 마대는 마을 신호불만 남겨 병사들이 혼동하지 않게 했습니다.',
|
|
bondExp: 46,
|
|
itemRewards: ['상처약 12']
|
|
},
|
|
{
|
|
id: 'train-fire-breaks',
|
|
label: '방화선을 훈련한다',
|
|
response: '마대는 숲과 마을 사이의 방화선을 표시했고, 마초는 기병이 불길을 피해 우회하는 길을 익혔습니다.',
|
|
bondExp: 48,
|
|
itemRewards: ['탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fifth-capture-hardliner-ledger',
|
|
title: '강경파 분리 장부',
|
|
location: '남중 포로장',
|
|
availableAfterBattleIds: [campBattleIds.fiftySecond],
|
|
bondId: 'zhuge-liang__huang-quan_fifth-capture',
|
|
description: '제갈량과 황권이 다섯 번째 생포 뒤 강경파와 회유파의 이름을 나누어 기록합니다.',
|
|
lines: [
|
|
'황권: 같은 남중 사람이라도 오늘부터 장부의 의미가 달라집니다.',
|
|
'제갈량: 끝까지 싸운 자를 적는 일보다, 돌아선 자를 지키는 일이 먼저입니다.',
|
|
'포로장 한편에는 강경파 분리 장부와 돌아선 호족의 보증문이 나뉘어 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-hardliner-list',
|
|
label: '강경파 명부를 봉한다',
|
|
response: '황권은 강경파 명부를 따로 봉했고, 제갈량은 남은 호족에게 다시 석방의 조건을 알렸습니다.',
|
|
bondExp: 52,
|
|
itemRewards: ['강경파 분리 장부 1']
|
|
},
|
|
{
|
|
id: 'protect-amnesty-witnesses',
|
|
label: '회유 증인을 보호한다',
|
|
response: '제갈량은 회유 길을 통과한 증인을 보호하게 했고, 황권은 그 이름을 보증 장부에 더했습니다.',
|
|
bondExp: 50,
|
|
gold: 1760,
|
|
itemRewards: ['남중 회유 깃발 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'fifth-capture-amnesty-banners',
|
|
title: '회유 깃발 정비',
|
|
location: '남중 죽림 길목',
|
|
availableAfterBattleIds: [campBattleIds.fiftySecond],
|
|
bondId: 'ma-liang__wang-ping_fifth-capture',
|
|
description: '마량과 왕평이 다음 전투에서 물러날 호족이 알아볼 회유 깃발과 길목 표식을 정비합니다.',
|
|
lines: [
|
|
'마량: 글을 읽지 못하는 자도 알아볼 표식이 필요합니다.',
|
|
'왕평: 낮은 길에는 흰 깃발, 군용길에는 검은 표식을 세우겠습니다.',
|
|
'죽림 길목마다 회유 깃발과 군용 표식이 서로 다른 방향을 가리키기 시작했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'raise-amnesty-banners',
|
|
label: '회유 깃발을 세운다',
|
|
response: '마량은 짧은 회유문을 깃발에 달았고, 왕평은 물러날 길목에 같은 표식을 반복해 세웠습니다.',
|
|
bondExp: 50,
|
|
itemRewards: ['콩 12']
|
|
},
|
|
{
|
|
id: 'separate-war-paths',
|
|
label: '군용길을 분리한다',
|
|
response: '왕평은 군대가 지나갈 길을 따로 표시했고, 마량은 그 길이 마을로 이어지지 않게 문구를 고쳤습니다.',
|
|
bondExp: 48,
|
|
itemRewards: ['상처약 12']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sixth-capture-trust-ledger',
|
|
title: '남중 신뢰 장부',
|
|
location: '깊은 계곡 포로장',
|
|
availableAfterBattleIds: [campBattleIds.fiftyThird],
|
|
bondId: 'huang-quan__wang-ping_sixth-capture',
|
|
description: '황권과 왕평이 여섯 번째 생포 뒤 마을 신뢰와 계곡 퇴로를 함께 기록한 장부를 정리합니다.',
|
|
lines: [
|
|
'황권: 이번 장부에는 누가 물러났는지만 적지 않습니다. 우리가 어느 길을 지켰는지도 함께 남겨야 합니다.',
|
|
'왕평: 길을 지켰다는 증거가 있으면, 다음에 촌장이 나서서 말할 수 있습니다.',
|
|
'포로장 한편에는 마을 이름, 길표식, 지켜 낸 퇴로가 한 장부에 묶였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-trust-ledger',
|
|
label: '신뢰 장부를 봉한다',
|
|
response: '황권은 신뢰 장부를 봉했고, 왕평은 같은 표식을 계곡 길목마다 나누어 세웠습니다.',
|
|
bondExp: 54,
|
|
itemRewards: ['남중 신뢰 장부 1']
|
|
},
|
|
{
|
|
id: 'send-ledger-witnesses',
|
|
label: '증인을 마을로 보낸다',
|
|
response: '왕평은 길을 아는 병사를 붙였고, 황권은 촌장들이 직접 확인할 수 있도록 증인을 보냈습니다.',
|
|
bondExp: 52,
|
|
gold: 1840,
|
|
itemRewards: ['계곡 회유 표식 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'sixth-capture-final-persuasion',
|
|
title: '마지막 설득 준비',
|
|
location: '남중 회의 천막',
|
|
availableAfterBattleIds: [campBattleIds.fiftyThird],
|
|
bondId: 'zhuge-liang__ma-liang_sixth-capture',
|
|
description: '제갈량과 마량이 칠종칠금의 마지막 문턱에서 맹획과 남중 촌장들에게 건넬 말을 준비합니다.',
|
|
lines: [
|
|
'마량: 여섯 번의 싸움이 모두 겁박으로 보이면 마지막 설득은 닿지 않을 것입니다.',
|
|
'제갈량: 그러니 우리가 지킨 마을, 풀어 준 포로, 남겨 둔 길을 먼저 말해야 합니다.',
|
|
'회의 천막에는 전투 기록보다 마을 이름과 사람들의 말이 더 많이 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'prepare-final-oath',
|
|
label: '마지막 회유문을 준비한다',
|
|
response: '마량은 짧고 분명한 문장을 골랐고, 제갈량은 맹획 앞에서 꺼낼 순서를 조용히 정했습니다.',
|
|
bondExp: 54,
|
|
itemRewards: ['탁주 5']
|
|
},
|
|
{
|
|
id: 'invite-village-speakers',
|
|
label: '촌장 증언을 모은다',
|
|
response: '제갈량은 촌장들을 억지로 부르지 말라 했고, 마량은 스스로 나선 사람의 말을 먼저 적었습니다.',
|
|
bondExp: 52,
|
|
itemRewards: ['상처약 14']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'final-capture-pacification-ledger',
|
|
title: '남중 평정 장부',
|
|
location: '남중 회의장',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
bondId: 'zhuge-liang__huang-quan_final-capture',
|
|
description: '제갈량과 황권이 맹획의 항복 뒤 남중 각 마을에 남길 약속과 촉한군의 금지 군령을 정리합니다.',
|
|
lines: [
|
|
'황권: 이제 이 장부는 포로의 이름이 아니라 지켜야 할 약속의 목록이 되었습니다.',
|
|
'제갈량: 남중을 얻었다고 해서 남중을 마음대로 써도 된다는 뜻은 아닙니다.',
|
|
'회의장 중앙에는 남중 평정 장부와 촉한군 금지 군령이 함께 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-pacification-ledger',
|
|
label: '평정 장부를 봉한다',
|
|
response: '황권은 평정 장부를 봉했고, 제갈량은 남중 마을 징발 금지 군령을 함께 낭독하게 했습니다.',
|
|
bondExp: 58,
|
|
itemRewards: ['남중 평정 장부 1']
|
|
},
|
|
{
|
|
id: 'send-council-copies',
|
|
label: '회의장 사본을 보낸다',
|
|
response: '제갈량은 촌장들이 직접 확인할 사본을 나누었고, 황권은 각 사본에 약속의 기준을 적었습니다.',
|
|
bondExp: 56,
|
|
gold: 1960,
|
|
itemRewards: ['상처약 16']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'final-capture-menghuo-oath',
|
|
title: '맹획 항복 서약',
|
|
location: '남중 대천막',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
bondId: 'ma-liang__wang-ping_final-capture',
|
|
description: '마량과 왕평이 맹획의 항복 서약을 촌장들이 이해할 말과 길표식으로 풀어 남깁니다.',
|
|
lines: [
|
|
'마량: 맹획의 항복문이 너무 높은 말로만 쓰이면, 마을 사람들은 다시 남의 약속처럼 여길 것입니다.',
|
|
'왕평: 길표식도 함께 남기겠습니다. 어느 길로 군대가 떠나고, 어느 길로 장터가 다시 열리는지 보여야 합니다.',
|
|
'대천막 밖에는 촉한군이 떠날 길과 남중 사람들이 오갈 길이 서로 다른 표식으로 나뉘었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'record-menghuo-oath',
|
|
label: '맹획 서약을 적는다',
|
|
response: '마량은 맹획의 항복 서약을 짧게 다시 적었고, 왕평은 마을 사람들이 볼 길목에 같은 표식을 세웠습니다.',
|
|
bondExp: 56,
|
|
itemRewards: ['맹획 항복 서약 1']
|
|
},
|
|
{
|
|
id: 'open-market-road',
|
|
label: '장터길을 다시 연다',
|
|
response: '왕평은 장터로 이어지는 낮은 길을 열었고, 마량은 촌장들이 직접 장터 재개를 알리게 했습니다.',
|
|
bondExp: 54,
|
|
itemRewards: ['콩 16', '탁주 4']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-prep-hanzhong-granary',
|
|
title: '한중 창고 점검',
|
|
location: '한중 군량 창고',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
availableDuringSteps: ['northern-campaign-prep-camp'],
|
|
bondId: 'zhuge-liang__huang-quan_final-capture',
|
|
description: '제갈량과 황권이 북벌의 첫 출진 전에 한중 창고, 수레, 보급 기한을 다시 점검합니다.',
|
|
lines: [
|
|
'황권: 남중 평정 장부가 끝났다고 곧장 북쪽으로 달리면 군량이 먼저 말라 버립니다.',
|
|
'제갈량: 북벌은 뜻만으로 움직이지 않습니다. 뜻을 오래 버티게 하는 창고가 있어야 합니다.',
|
|
'한중 창고에는 수레 표식과 군량 날짜가 새로 적힌 장부가 펼쳐졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'count-northern-wagons',
|
|
label: '수레와 군량을 다시 센다',
|
|
response: '황권은 수레 수와 군량 날짜를 맞췄고, 제갈량은 먼저 보낼 물자와 남길 물자를 구분했습니다.',
|
|
bondExp: 58,
|
|
gold: 2140,
|
|
itemRewards: ['콩 20', '상처약 12']
|
|
},
|
|
{
|
|
id: 'reserve-field-rations',
|
|
label: '예비 군량을 따로 둔다',
|
|
response: '제갈량은 전공을 서두르는 병사들에게 예비 군량의 의미를 설명했고, 황권은 대기 부대 몫을 따로 봉했습니다.',
|
|
bondExp: 56,
|
|
itemRewards: ['탁주 6', '상처약 16']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-prep-memorial-table',
|
|
title: '출사표 초안',
|
|
location: '승상부 서안',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFourth],
|
|
availableDuringSteps: ['northern-campaign-prep-camp'],
|
|
bondId: 'zhao-yun__zhuge-liang',
|
|
description: '제갈량이 북쪽으로 나아갈 뜻을 정리하고, 조운이 그 뜻을 실제 출진 순서로 옮길 준비를 합니다.',
|
|
lines: [
|
|
'제갈량: 글은 군을 움직이지 못하지만, 군이 왜 움직이는지는 잊지 않게 합니다.',
|
|
'조운: 그 뜻이 분명하면 장수들도 무리한 공을 다투지 않을 것입니다.',
|
|
'서안 위에는 출사표의 초안과 첫 출진 편성표가 함께 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'draft-memorial-oath',
|
|
label: '출사표 초안을 다듬는다',
|
|
response: '제갈량은 군주의 은혜와 한실의 뜻을 다시 적었고, 조운은 그 글이 장수들의 출진 순서와 어긋나지 않게 살폈습니다.',
|
|
bondExp: 58,
|
|
itemRewards: ['북벌 고시문 1']
|
|
},
|
|
{
|
|
id: 'match-sortie-order',
|
|
label: '출진 순서를 맞춘다',
|
|
response: '조운은 선발대와 본대의 거리를 정했고, 제갈량은 무리한 돌파보다 오래 버티는 행군표를 먼저 승인했습니다.',
|
|
bondExp: 56,
|
|
gold: 1980,
|
|
itemRewards: ['콩 18']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-first-qishan-ledger',
|
|
title: '기산 출진 장부',
|
|
location: '기산 입구 보급막',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFifth],
|
|
bondId: 'huang-quan__ma-liang_northern-first',
|
|
description: '황권과 마량이 기산 출진로 확보 후 남은 군량, 마을 약속, 부상병 이동 경로를 하나의 장부로 정리합니다.',
|
|
lines: [
|
|
'황권: 이번 장부는 숫자만 적으면 안 됩니다. 어느 마을에 무엇을 약속했는지도 함께 남겨야 합니다.',
|
|
'마량: 장부가 곧 약속이 되도록 쓰겠습니다. 병사도 백성도 같은 글을 보고 안심해야 합니다.',
|
|
'보급막에는 군량 수레의 수량과 마을별 약속이 함께 적힌 새 장부가 놓였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-qishan-ledger',
|
|
label: '출진 장부를 봉한다',
|
|
response: '황권은 장부의 수량을 확인했고, 마량은 마지막 장에 마을별 약속을 다시 적어 봉했습니다.',
|
|
bondExp: 60,
|
|
itemRewards: ['기산 출진 장부 1']
|
|
},
|
|
{
|
|
id: 'send-ledger-copies',
|
|
label: '마을에 사본을 보낸다',
|
|
response: '마량은 마을 대표에게 사본을 보내고, 황권은 그 사본과 같은 물자 표식을 보급대에 나누어 주었습니다.',
|
|
bondExp: 58,
|
|
gold: 2180,
|
|
itemRewards: ['콩 20', '상처약 12']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-first-hanzhong-marker',
|
|
title: '한중 보급 표식',
|
|
location: '한중로 갈림목',
|
|
availableAfterBattleIds: [campBattleIds.fiftyFifth],
|
|
bondId: 'zhuge-liang__zhao-yun_northern-first',
|
|
description: '제갈량과 조운이 한중에서 기산으로 이어지는 보급 표식을 다시 세워, 다음 전장에서도 길을 잃지 않도록 준비합니다.',
|
|
lines: [
|
|
'제갈량: 표식은 병사에게 말없이 명령을 내리는 장수와 같습니다. 흐릿하면 군대가 흔들립니다.',
|
|
'조운: 제가 먼저 지나가며 확인하겠습니다. 말발굽이 닿는 곳마다 뒤따를 병사들이 볼 표식을 남기겠습니다.',
|
|
'갈림목에는 촉한의 보급대가 알아볼 수 있는 표식과 작은 깃발이 새로 세워졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'raise-hanzhong-marker',
|
|
label: '보급 표식을 새로 세운다',
|
|
response: '조운은 갈림목마다 깃발을 세웠고, 제갈량은 표식마다 행군 순서와 회군로를 함께 적었습니다.',
|
|
bondExp: 60,
|
|
itemRewards: ['한중 보급 표식 1']
|
|
},
|
|
{
|
|
id: 'train-marker-runners',
|
|
label: '표식 전령을 훈련한다',
|
|
response: '제갈량은 표식을 옮기는 법을 정했고, 조운은 빠른 전령들이 위군의 눈을 피해 움직이는 훈련을 맡았습니다.',
|
|
bondExp: 58,
|
|
gold: 2060,
|
|
itemRewards: ['탁주 5']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-second-waterline-ledger',
|
|
title: '천수 물길 장부',
|
|
location: '천수 하곡 보급막',
|
|
availableAfterBattleIds: [campBattleIds.fiftySixth],
|
|
bondId: 'huang-quan__wang-ping_northern-second',
|
|
description: '황권과 왕평이 천수 방면의 물길, 군량 수레, 가정 고개까지 이어지는 진영 후보지를 하나의 장부로 정리합니다.',
|
|
lines: [
|
|
'황권: 길을 얻어도 물을 잃으면 군량은 반나절 만에 짐이 됩니다.',
|
|
'왕평: 고개 위가 좋아 보여도 아래 물길이 멀면 오래 버틸 수 없습니다.',
|
|
'보급막에는 물길, 수레 회전 지점, 진영 후보가 함께 표시된 천수 장부가 펼쳐졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'mark-tianshui-waterline',
|
|
label: '물길을 장부에 새긴다',
|
|
response: '황권은 수레가 멈출 곳을 적었고, 왕평은 그 옆에 진영을 세워서는 안 될 고지를 표시했습니다.',
|
|
bondExp: 62,
|
|
itemRewards: ['천수 물길 장부 1']
|
|
},
|
|
{
|
|
id: 'stock-ridge-water',
|
|
label: '고개 물자를 따로 쌓는다',
|
|
response: '왕평은 고개 아래 임시 창고를 정했고, 황권은 그곳에 보낼 물자를 본대 보급과 분리했습니다.',
|
|
bondExp: 60,
|
|
gold: 2260,
|
|
itemRewards: ['상처약 14', '콩 22']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-second-jiangwei-letter',
|
|
title: '강유에게 남긴 글',
|
|
location: '천수 마을 서고',
|
|
availableAfterBattleIds: [campBattleIds.fiftySixth],
|
|
bondId: 'ma-liang__fa-zheng_northern-second',
|
|
description: '마량과 법정이 강유의 평판과 천수 사람들의 말을 모아, 다음 전선에서 그를 설득할 실마리를 남깁니다.',
|
|
lines: [
|
|
'마량: 강유는 전장에서 졌지만, 병사와 백성에게 남긴 이름은 쉽게 꺾이지 않았습니다.',
|
|
'법정: 그러니 설득은 항복 권고가 아니라 판단의 틈을 보여 주는 말이어야 합니다.',
|
|
'서고에는 강유의 이름, 천수 사람들의 증언, 위군 내부의 의심이 조심스럽게 기록되었습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-jiangwei-letter',
|
|
label: '강유의 이름을 남긴다',
|
|
response: '마량은 강유에게 보낼 글을 봉했고, 법정은 그 글이 위군 군리의 손에 들어가도 뜻이 흐트러지지 않게 다듬었습니다.',
|
|
bondExp: 62,
|
|
itemRewards: ['강유 접촉 서한 1']
|
|
},
|
|
{
|
|
id: 'gather-tianshui-testimony',
|
|
label: '천수 증언을 모은다',
|
|
response: '마량은 마을 사람의 말을 적었고, 법정은 그 증언에서 강유가 흔들릴 만한 지점을 따로 표시했습니다.',
|
|
bondExp: 60,
|
|
gold: 2140,
|
|
itemRewards: ['탁주 6']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-third-jieting-map-room',
|
|
title: '가정 지도실',
|
|
location: '가정 하곡 지도막',
|
|
availableAfterBattleIds: [campBattleIds.fiftySeventh],
|
|
bondId: 'wang-ping__jiang-wei',
|
|
description: '왕평과 강유가 가정, 천수, 한중을 잇는 실제 길과 물길을 하나의 북방 지형책으로 다시 묶습니다.',
|
|
lines: [
|
|
'왕평: 이 고개는 지도보다 물길이 먼저입니다. 높은 곳보다 낮은 길이 더 오래 버팁니다.',
|
|
'강유: 천수 사람들은 이 길을 군대보다 먼저 알고 있었습니다. 제가 알고 있던 길을 모두 내놓겠습니다.',
|
|
'지도막에는 가정의 실패를 반복하지 않기 위한 낮은 길, 물길, 수레 회전 지점이 새로 그려졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'bind-jieting-tianshui-map',
|
|
label: '가정과 천수 지도를 묶는다',
|
|
response: '왕평은 고개마다 물길을 표시했고, 강유는 천수 사람들이 쓰는 길을 덧그려 북방 지형책을 보강했습니다.',
|
|
bondExp: 66,
|
|
itemRewards: ['북방 지형책 1']
|
|
},
|
|
{
|
|
id: 'mark-low-road-supply',
|
|
label: '낮은 보급로를 표시한다',
|
|
response: '강유는 위군이 잘 보지 못하는 낮은 길을 알려 주었고, 왕평은 그 길을 군대가 지나도 마을을 해치지 않도록 나누었습니다.',
|
|
bondExp: 64,
|
|
gold: 2360,
|
|
itemRewards: ['콩 24', '상처약 16']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-third-jiangwei-drill',
|
|
title: '강유 편성 훈련',
|
|
location: '가정 후방 훈련장',
|
|
availableAfterBattleIds: [campBattleIds.fiftySeventh],
|
|
bondId: 'zhuge-liang__jiang-wei',
|
|
description: '제갈량이 새로 합류한 강유를 다음 출전 선택지 안에 넣기 위해, 기존 장수들과 임무 구분을 정리합니다.',
|
|
lines: [
|
|
'제갈량: 새 장수가 들어오면 강해지는 만큼 편성은 더 어려워집니다. 누가 앞서고 누가 남는지 더 분명해야 합니다.',
|
|
'강유: 저는 천수의 길을 알지만 촉한의 호흡은 아직 배워야 합니다.',
|
|
'훈련장에는 강유의 기병 운용, 왕평의 지형 판단, 조운과 마대의 회수 임무가 나란히 적혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'set-jiangwei-sortie-role',
|
|
label: '강유의 출전 역할을 정한다',
|
|
response: '제갈량은 강유를 기동 정찰과 천수 길 안내에 먼저 배치했고, 병사들은 새 장수의 자리를 조금씩 받아들였습니다.',
|
|
bondExp: 66,
|
|
itemRewards: ['탁주 7']
|
|
},
|
|
{
|
|
id: 'compare-cavalry-roles',
|
|
label: '기병 역할을 비교한다',
|
|
response: '조운은 회수, 마대는 우회, 강유는 천수 길 안내로 역할이 나뉘었고, 다음 출전 선택은 더 흥미로운 고민이 되었습니다.',
|
|
bondExp: 64,
|
|
gold: 2280,
|
|
itemRewards: ['기병 편성표 1']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fourth-qishan-ledger',
|
|
title: '기산 퇴각 장부',
|
|
location: '기산 군량 막사',
|
|
availableAfterBattleIds: [campBattleIds.fiftyEighth],
|
|
bondId: 'zhuge-liang__jiang-wei_northern-fourth',
|
|
description: '제갈량과 강유가 기산 후퇴로에서 지킨 병력, 군량, 지도 표식을 다음 북벌 장부로 정리합니다.',
|
|
lines: [
|
|
'제갈량: 오늘 지킨 것은 승리의 이름이 아니라 다음 출정을 가능하게 할 숫자입니다.',
|
|
'강유: 천수의 길을 숨기지 않겠습니다. 장부 위에 남기면 제 말도 군의 약속이 될 것입니다.',
|
|
'군량 막사에는 후퇴하며 지킨 수레 수, 잃지 않은 병력, 다음에 다시 건널 낮은 길이 함께 적혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-qishan-retreat-ledger',
|
|
label: '퇴각 장부를 봉한다',
|
|
response: '제갈량은 퇴각 장부를 봉했고, 강유는 자신이 아는 천수 낮은 길을 다음 북벌의 참고로 남겼습니다.',
|
|
bondExp: 70,
|
|
itemRewards: ['기산 퇴각 장부 1']
|
|
},
|
|
{
|
|
id: 'assign-next-grain-route',
|
|
label: '다음 군량로를 배정한다',
|
|
response: '황권의 장부와 강유의 길 안내가 함께 묶였고, 다음 북방 보급로의 첫 후보가 정리되었습니다.',
|
|
bondExp: 66,
|
|
gold: 2480,
|
|
itemRewards: ['콩 28', '상처약 18']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-fifth-chencang-ledger',
|
|
title: '진창 공성 장부',
|
|
location: '진창 외곽 공성막',
|
|
availableAfterBattleIds: [campBattleIds.fiftyNinth],
|
|
bondId: 'huang-quan__ma-liang_northern-fifth',
|
|
description: '황권과 마량이 진창 공성전에서 소모한 군량, 지킨 수레, 병사들에게 남길 재정비 고시문을 하나의 장부로 묶습니다.',
|
|
lines: [
|
|
'황권: 성문이 열리지 않아도 장부는 남습니다. 어느 날까지 버틸 수 있었는지 알아야 다음 전선에서 멈출 수 있습니다.',
|
|
'마량: 병사들에게는 왜 돌아왔는지도 말해야 합니다. 진창은 물러선 이름이 아니라 다음 계산의 경계입니다.',
|
|
'공성막 안에는 학소의 성벽, 왕쌍의 추격로, 촉한군이 지킨 수레 수가 함께 적혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-chencang-siege-ledger',
|
|
label: '공성 장부를 봉한다',
|
|
response: '황권은 진창 공성 장부를 봉했고, 마량은 병사들이 낙담하지 않도록 재정비 고시문을 붙였습니다.',
|
|
bondExp: 72,
|
|
itemRewards: ['진창 공성 장부 1']
|
|
},
|
|
{
|
|
id: 'assign-chencang-regroup-route',
|
|
label: '재정비 보급로를 정한다',
|
|
response: '강유의 낮은 길, 왕평의 진영 표식, 황권의 군량 계산이 다음 북벌 보급로 후보로 묶였습니다.',
|
|
bondExp: 68,
|
|
gold: 2580,
|
|
itemRewards: ['콩 30', '상처약 20']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-sixth-commandery-ledger',
|
|
title: '무도·음평 장부막',
|
|
location: '무도 남쪽 산길 장부막',
|
|
availableAfterBattleIds: [campBattleIds.sixtieth],
|
|
bondId: 'wang-ping__huang-quan_northern-sixth',
|
|
description: '왕평과 황권이 무도와 음평의 산길 표식, 군량 수레 날짜, 마을 안심 약속을 하나의 북방 장부로 묶습니다.',
|
|
lines: [
|
|
'왕평: 여기서 고개 하나를 잘못 잡으면 무도와 음평이 서로를 돕지 못합니다.',
|
|
'황권: 그래서 군량 날짜를 고개 표식 옆에 붙였습니다. 길과 장부가 따로 움직이면 새 고을은 금세 부담이 됩니다.',
|
|
'장부막에는 두 고을의 수레 길, 마을 배분표, 다음 북벌 때 다시 열어야 할 산길이 함께 적혔습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-two-commandery-ledger',
|
|
label: '두 고을 장부를 봉한다',
|
|
response: '왕평은 고개 표식을 확인했고, 황권은 두 고을 군량 장부를 봉해 다음 북벌 보급의 기준으로 남겼습니다.',
|
|
bondExp: 74,
|
|
itemRewards: ['무도·음평 장부 1']
|
|
},
|
|
{
|
|
id: 'assign-commandery-reassurance',
|
|
label: '마을 안심 약속을 붙인다',
|
|
response: '마량의 고시문이 장부막 앞에 붙었고, 황권은 약속한 배분을 실제 군량표에 반영했습니다.',
|
|
bondExp: 70,
|
|
gold: 2680,
|
|
itemRewards: ['콩 32', '상처약 22']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-seventh-rain-granary-ledger',
|
|
title: '한중 우로 장부막',
|
|
location: '한중 북문 빗길 창고',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFirst],
|
|
bondId: 'li-yan__huang-quan_northern-seventh',
|
|
description: '이엄과 황권이 한중 창고, 무도·음평 수레 길, 비에 늦어진 고개 기록을 다음 북벌 장부로 묶습니다.',
|
|
lines: [
|
|
'황권: 창고는 지켰지만 비가 남긴 빈칸이 많습니다. 이 빈칸을 숨기면 다음 출정이 먼저 무너집니다.',
|
|
'이엄: 빈칸까지 장부에 넣겠습니다. 늦은 길을 늦었다고 적어야 다음 수레가 억울하게 몰리지 않습니다.',
|
|
'장부막에는 한중 북문과 무도·음평의 수레 날짜가 한 줄로 이어졌고, 병사들은 다음 북벌이 아직 끝나지 않았음을 조용히 확인했습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-hanzhong-rain-ledger',
|
|
label: '한중 우로 장부를 봉한다',
|
|
response: '황권은 한중 창고의 남은 군량을 봉했고, 이엄은 빗길 지연 기록을 붙여 다음 출정의 실제 기준으로 남겼습니다.',
|
|
bondExp: 78,
|
|
itemRewards: ['한중 우로 장부 1']
|
|
},
|
|
{
|
|
id: 'assign-commandery-relay-carts',
|
|
label: '두 고을 예비 수레를 배정한다',
|
|
response: '이엄은 무도와 음평에 예비 수레를 나누었고, 황권은 그 수레가 늦어질 때 대체할 고개 이름을 장부 옆에 적었습니다.',
|
|
bondExp: 72,
|
|
gold: 2760,
|
|
itemRewards: ['콩 34', '상처약 24']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eighth-lucheng-supply-table',
|
|
title: '노성 앞 보급 탁자',
|
|
location: '기산 재출정 군막',
|
|
availableAfterBattleIds: [campBattleIds.sixtySecond],
|
|
bondId: 'huang-quan__ma-liang_northern-eighth',
|
|
description: '황권과 마량이 노성 앞 전진로, 마을 안심 약속, 다음 북벌의 실제 군량 여유를 하나의 탁자 위에서 맞춥니다.',
|
|
lines: [
|
|
'마량: 전투가 이긴 뒤에 붙이는 고시문은 늦습니다. 마을은 군량이 지나간 자리를 먼저 봅니다.',
|
|
'황권: 그렇다면 장부의 첫 줄을 병사 몫이 아니라 지나갈 마을 몫으로 두겠습니다.',
|
|
'군막의 탁자에는 노성 앞 길과 회수 진영, 전방 마을의 약속이 한 장의 지도로 묶였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-lucheng-supply-ledger',
|
|
label: '기산 재출정 장부를 봉한다',
|
|
response: '황권은 노성 앞에서 쓴 군량과 남은 수레를 봉했고, 마량은 그 장부를 다음 고시문 기준으로 삼았습니다.',
|
|
bondExp: 82,
|
|
itemRewards: ['기산 재출정 장부 1']
|
|
},
|
|
{
|
|
id: 'reserve-front-village-grain',
|
|
label: '전방 마을 예비 군량을 남긴다',
|
|
response: '마량은 마을 이름을 하나씩 불렀고, 황권은 그 이름 옆에 실제로 남길 군량 숫자를 적었습니다.',
|
|
bondExp: 76,
|
|
gold: 2860,
|
|
itemRewards: ['콩 36', '상처약 26']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-ninth-lucheng-return-ledger',
|
|
title: '노성 회수 장부',
|
|
location: '노성 추격 군막',
|
|
availableAfterBattleIds: [campBattleIds.sixtyThird],
|
|
bondId: 'huang-quan__li-yan_northern-ninth',
|
|
description: '황권과 이엄이 노성 추격로에서 실제로 늦어진 수레, 남긴 군량, 다음 북벌의 속도를 하나의 장부로 맞춥니다.',
|
|
lines: [
|
|
'이엄: 이번에는 늦은 수레를 늦었다고 적었습니다. 숨긴 숫자는 다음 길에서 병사를 배신합니다.',
|
|
'황권: 좋습니다. 이 장부에는 이긴 길도, 늦어진 길도 같은 먹으로 남기겠습니다.',
|
|
'군막의 탁자에는 노성 추격로와 회수 표식, 예비 수레의 날짜가 한 장으로 묶였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-lucheng-pursuit-ledger',
|
|
label: '노성 추격 장부를 봉한다',
|
|
response: '황권은 노성 추격 장부를 봉했고, 이엄은 다음 출정의 수레 속도를 장부 첫 줄에 적었습니다.',
|
|
bondExp: 86,
|
|
itemRewards: ['노성 추격 장부 1']
|
|
},
|
|
{
|
|
id: 'reserve-return-road-supplies',
|
|
label: '회수로 예비 군량을 남긴다',
|
|
response: '이엄은 회수 표식마다 예비 군량을 남겼고, 황권은 그 군량이 마을 몫을 줄이지 않도록 다시 계산했습니다.',
|
|
bondExp: 80,
|
|
gold: 2940,
|
|
itemRewards: ['콩 38', '상처약 28']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-tenth-weishui-camp-ledger',
|
|
title: '위수 진영 장부',
|
|
location: '위수 남안 군막',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFourth],
|
|
bondId: 'zhuge-liang__huang-quan_northern-tenth',
|
|
description: '제갈량과 황권이 위수 남안 진영을 얼마나 오래 지킬지, 수레와 군량을 어디까지 붙일지 장부로 다시 맞춥니다.',
|
|
lines: [
|
|
'황권: 강가 진영은 얻은 순간부터 소모가 시작됩니다. 진영마다 버틸 날짜를 따로 정해야 합니다.',
|
|
'제갈량: 이긴 곳을 모두 붙들 수 없다면, 다음 명령은 남길 곳과 물릴 곳을 먼저 나누어야 합니다.',
|
|
'군막의 탁자에는 위수 물목, 예비 수레선, 꺼진 봉화 위치가 한 장의 장부로 묶였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-weishui-camp-ledger',
|
|
label: '위수 진영 장부를 봉한다',
|
|
response: '황권은 위수 진영 장부를 봉했고, 제갈량은 다음 북방 명령의 폭을 그 장부에 맞춰 줄였습니다.',
|
|
bondExp: 90,
|
|
itemRewards: ['위수 진영 장부 1']
|
|
},
|
|
{
|
|
id: 'store-riverbank-supplies',
|
|
label: '강안 예비 군량을 남긴다',
|
|
response: '위수 남안의 예비 창고에 콩과 상처약이 남았고, 병사들은 강가 안개 속에서도 돌아올 표식을 확인했습니다.',
|
|
bondExp: 84,
|
|
gold: 3020,
|
|
itemRewards: ['콩 40', '상처약 30']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-eleventh-northbank-forward-ledger',
|
|
title: '북안 전진 장부',
|
|
location: '위수 북안 군막',
|
|
availableAfterBattleIds: [campBattleIds.sixtyFifth],
|
|
bondId: 'wang-ping__huang-quan_northern-eleventh',
|
|
description: '왕평과 황권이 북안 전진 진영마다 정지선, 남길 군량, 물릴 날짜를 하나의 장부로 맞춥니다.',
|
|
lines: [
|
|
'왕평: 진영을 얻은 뒤에도 병사가 멈출 선을 모르면, 이긴 진영이 곧 갇힌 진영이 됩니다.',
|
|
'황권: 그렇다면 장부에는 진영 이름보다 먼저 멈출 선과 물릴 날짜를 적겠습니다.',
|
|
'군막의 탁자에는 북안 전진 진영, 회수 깃발, 강북 마을 군량이 한 장의 장부로 묶였습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-northbank-forward-ledger',
|
|
label: '북안 전진 장부를 봉한다',
|
|
response: '황권은 북안 전진 장부를 봉했고, 왕평은 각 진영의 정지선에 같은 문양의 깃발을 세웠습니다.',
|
|
bondExp: 94,
|
|
itemRewards: ['북안 전진 장부 1']
|
|
},
|
|
{
|
|
id: 'store-northbank-village-supplies',
|
|
label: '강북 마을 예비 군량을 남긴다',
|
|
response: '강북 마을마다 콩과 상처약이 남았고, 병사들은 돌아올 길이 지도뿐 아니라 실제 마을에도 남았다는 것을 확인했습니다.',
|
|
bondExp: 88,
|
|
gold: 3100,
|
|
itemRewards: ['콩 42', '상처약 32']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'northern-twelfth-wuzhang-return-ledger',
|
|
title: '오장원 귀환 장부',
|
|
location: '오장원 본영 군막',
|
|
availableAfterBattleIds: [campBattleIds.sixtySixth],
|
|
bondId: 'huang-quan__li-yan_northern-twelfth',
|
|
description: '황권과 이엄이 오장원 본영의 마지막 장부를 봉합니다. 전공보다 먼저 적힌 것은 돌아갈 부대의 순서와 남겨 둘 군량입니다.',
|
|
lines: [
|
|
'황권: 마지막 장부에는 점령한 곳보다 비워 두어야 할 길을 먼저 적겠습니다.',
|
|
'이엄: 군량 수레도 같은 순서로 묶겠습니다. 앞선 부대가 무너지지 않으려면 뒤의 수레가 길을 잃지 않아야 합니다.',
|
|
'군막의 낮은 등불 아래, 오장원 귀환 장부와 강유에게 전할 지도가 함께 봉해졌습니다.'
|
|
],
|
|
choices: [
|
|
{
|
|
id: 'seal-wuzhang-return-ledger',
|
|
label: '귀환 장부를 봉한다',
|
|
response: '장부의 마지막 줄에 오장원 귀환 장부라는 제목이 적혔고, 병사들은 자신의 부대가 돌아갈 차례를 확인했습니다.',
|
|
bondExp: 96,
|
|
itemRewards: ['오장원 귀환 장부 1']
|
|
},
|
|
{
|
|
id: 'store-final-camp-supplies',
|
|
label: '남은 군량을 나눈다',
|
|
response: '군량 수레의 남은 자루가 각 부대에 다시 나뉘었습니다. 긴 전투 뒤의 침묵 속에서도 돌아갈 길은 한 칸 더 분명해졌습니다.',
|
|
bondExp: 88,
|
|
gold: 3300,
|
|
itemRewards: ['콩 44', '상처약 34']
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
export class CampScene extends Phaser.Scene {
|
|
private campaign?: CampaignState;
|
|
private report?: FirstBattleReport;
|
|
private selectedUnitId = 'liu-bei';
|
|
private activeTab: CampTab = 'status';
|
|
private selectedDialogueId = campDialogues[0].id;
|
|
private selectedVisitId = campVisits[0].id;
|
|
private contentObjects: Phaser.GameObjects.GameObject[] = [];
|
|
private dialogueObjects: Phaser.GameObjects.GameObject[] = [];
|
|
private sortieObjects: Phaser.GameObjects.GameObject[] = [];
|
|
private saveSlotObjects: Phaser.GameObjects.GameObject[] = [];
|
|
private saveSlotConfirmObjects: Phaser.GameObjects.GameObject[] = [];
|
|
private pendingSaveSlot?: number;
|
|
private tabButtons: CampTabButtonView[] = [];
|
|
private visitedTabs = new Set<CampTab>();
|
|
private selectedSortieUnitIds: string[] = [];
|
|
private sortieFormationAssignments: SortieFormationAssignments = {};
|
|
private sortieItemAssignments: CampaignSortieItemAssignments = {};
|
|
private sortieFocusedUnitId = 'liu-bei';
|
|
private sortieRosterScroll = 0;
|
|
private sortiePlanFeedback = '';
|
|
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
|
|
private openSortiePrepOnCreate = false;
|
|
private skipIntroStoryForSortie = false;
|
|
|
|
constructor() {
|
|
super('CampScene');
|
|
}
|
|
|
|
init(data?: CampSceneData) {
|
|
this.openSortiePrepOnCreate = Boolean(data?.openSortiePrep);
|
|
this.skipIntroStoryForSortie = Boolean(data?.skipIntroStory);
|
|
}
|
|
|
|
create() {
|
|
this.contentObjects = [];
|
|
this.dialogueObjects = [];
|
|
this.sortieObjects = [];
|
|
this.saveSlotObjects = [];
|
|
this.saveSlotConfirmObjects = [];
|
|
this.pendingSaveSlot = undefined;
|
|
this.tabButtons = [];
|
|
this.visitedTabs = new Set(['status']);
|
|
this.campaign = getCampaignState();
|
|
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
|
this.ensureCurrentCampRecruitment();
|
|
this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei';
|
|
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
|
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
|
|
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments);
|
|
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id;
|
|
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
|
|
soundDirector.playMusic('militia-theme');
|
|
this.input.keyboard?.on('keydown-ESC', () => this.handleEscapeKey());
|
|
this.input.keyboard?.on('keydown', (event: KeyboardEvent) => this.handleSaveSlotKey(event));
|
|
loadBattleUiIcons(this, () => this.ensureCampAssets(() => this.drawCampScene()));
|
|
}
|
|
|
|
private drawCampScene() {
|
|
const { width, height } = this.scale;
|
|
this.add.image(width / 2, height / 2, 'story-militia').setDisplaySize(width * 1.08, height * 1.08).setAlpha(0.62);
|
|
this.add.rectangle(0, 0, width, height, 0x06090d, 0.54).setOrigin(0);
|
|
this.add.rectangle(0, 0, width, 84, 0x06090d, 0.52).setOrigin(0);
|
|
|
|
this.add.text(42, 28, this.currentCampTitle(), {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '30px',
|
|
color: '#f2e3bf',
|
|
fontStyle: '700',
|
|
stroke: '#05070a',
|
|
strokeThickness: 4
|
|
});
|
|
this.add.text(42, 64, this.reportSummary(), {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '14px',
|
|
color: '#d4dce6'
|
|
});
|
|
|
|
this.renderStaticButtons();
|
|
this.render();
|
|
if (this.openSortiePrepOnCreate) {
|
|
this.time.delayedCall(0, () => this.showSortiePrep());
|
|
}
|
|
}
|
|
|
|
private ensureCampAssets(onReady: () => void) {
|
|
const missingPortraits = this.currentUnits()
|
|
.map((unit) => {
|
|
const portraitKey = portraitByUnitId[unit.id];
|
|
return portraitKey ? portraitAssetEntriesForKey(portraitKey)[0] : undefined;
|
|
})
|
|
.filter((entry): entry is PortraitAssetEntry => entry !== undefined)
|
|
.filter((entry, index, entries) => entries.findIndex((candidate) => candidate.textureKey === entry.textureKey) === index)
|
|
.filter((entry) => !this.textures.exists(entry.textureKey));
|
|
const missingMilitia = !this.textures.exists('story-militia');
|
|
|
|
if (!missingMilitia && missingPortraits.length === 0) {
|
|
onReady();
|
|
return;
|
|
}
|
|
|
|
const loadingText = this.add.text(this.scale.width / 2, this.scale.height / 2, '군영을 정비하는 중...', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '22px',
|
|
color: '#f5e6b8',
|
|
stroke: '#05070a',
|
|
strokeThickness: 4
|
|
});
|
|
loadingText.setOrigin(0.5);
|
|
|
|
this.load.once('complete', () => {
|
|
loadingText.destroy();
|
|
onReady();
|
|
});
|
|
this.load.once('loaderror', (file: Phaser.Loader.File) => {
|
|
console.warn(`Failed to load camp asset ${file.key}`);
|
|
});
|
|
|
|
if (missingMilitia) {
|
|
this.load.image('story-militia', storyMilitiaUrl);
|
|
}
|
|
missingPortraits.forEach(({ textureKey, url }) => this.load.image(textureKey, url));
|
|
this.load.start();
|
|
}
|
|
|
|
private createFallbackReport(): FirstBattleReport {
|
|
const allies = firstBattleUnits.filter((unit) => unit.faction === 'ally');
|
|
return {
|
|
battleId: defaultBattleScenario.id,
|
|
battleTitle: defaultBattleScenario.title,
|
|
outcome: 'victory',
|
|
turnNumber: 1,
|
|
rewardGold: 300,
|
|
defeatedEnemies: 0,
|
|
totalEnemies: firstBattleUnits.filter((unit) => unit.faction === 'enemy').length,
|
|
objectives: [],
|
|
units: allies.map((unit) => JSON.parse(JSON.stringify(unit)) as UnitData),
|
|
bonds: firstBattleBonds.map((bond) => ({ ...bond, battleExp: 0 })),
|
|
itemRewards: [],
|
|
completedCampDialogues: [],
|
|
completedCampVisits: [],
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
private ensureCurrentCampRecruitment() {
|
|
const battleId = this.currentCampBattleId();
|
|
if (!this.campaign) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
battleId === campBattleIds.fortieth ||
|
|
battleId === campBattleIds.fortyFirst ||
|
|
battleId === campBattleIds.fortySecond ||
|
|
battleId === campBattleIds.fortyThird ||
|
|
battleId === campBattleIds.fortyFourth ||
|
|
battleId === campBattleIds.fortyFifth ||
|
|
battleId === campBattleIds.fortySixth ||
|
|
battleId === campBattleIds.fortySeventh ||
|
|
battleId === campBattleIds.fortyEighth ||
|
|
battleId === campBattleIds.fortyNinth ||
|
|
battleId === campBattleIds.fiftieth ||
|
|
battleId === campBattleIds.fiftyFirst ||
|
|
battleId === campBattleIds.fiftySecond ||
|
|
battleId === campBattleIds.fiftyThird ||
|
|
battleId === campBattleIds.fiftyFourth ||
|
|
battleId === campBattleIds.fiftyFifth ||
|
|
battleId === campBattleIds.fiftySixth ||
|
|
battleId === campBattleIds.fiftySeventh ||
|
|
battleId === campBattleIds.fiftyEighth ||
|
|
battleId === campBattleIds.fiftyNinth ||
|
|
battleId === campBattleIds.sixtieth ||
|
|
battleId === campBattleIds.sixtyFirst ||
|
|
battleId === campBattleIds.sixtySecond ||
|
|
battleId === campBattleIds.sixtyThird ||
|
|
battleId === campBattleIds.sixtyFourth ||
|
|
battleId === campBattleIds.sixtyFifth ||
|
|
battleId === campBattleIds.sixtySixth
|
|
) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits,
|
|
...hanzhongScoutRecruitUnits,
|
|
...hanzhongMainRecruitUnits,
|
|
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? jiangWeiRecruitUnits : [])
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds,
|
|
...hanzhongScoutRecruitBonds,
|
|
...hanzhongMainRecruitBonds,
|
|
...hanzhongDecisiveBonds,
|
|
...jingDefenseBonds,
|
|
...fanCastleBonds,
|
|
...hanRiverFloodBonds,
|
|
...fanCastleSiegeBonds,
|
|
...jingRearCrisisBonds,
|
|
...jingCollapseBonds,
|
|
...maichengIsolationBonds,
|
|
...yilingVanguardBonds,
|
|
...yilingFireBonds,
|
|
...nanzhongRecoveryBonds,
|
|
...mengHuoPacificationBonds,
|
|
...mengHuoSecondCaptureBonds,
|
|
...mengHuoThirdCaptureBonds,
|
|
...mengHuoFourthCaptureBonds,
|
|
...mengHuoFifthCaptureBonds,
|
|
...mengHuoSixthCaptureBonds,
|
|
...mengHuoFinalCaptureBonds,
|
|
...northernFirstBattleBonds,
|
|
...northernSecondBattleBonds,
|
|
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernThirdBattleBonds : []),
|
|
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? jiangWeiRecruitBonds : []),
|
|
...(battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernFourthBattleBonds : []),
|
|
...(battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernFifthBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernSixthBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernSeventhBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtySecond || battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernEighthBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtyThird || battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernNinthBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtyFourth || battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernTenthBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtyFifth || battleId === campBattleIds.sixtySixth ? northernEleventhBattleBonds : []),
|
|
...(battleId === campBattleIds.sixtySixth ? northernTwelfthBattleBonds : [])
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtyNinth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits,
|
|
...hanzhongScoutRecruitUnits,
|
|
...hanzhongMainRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds,
|
|
...hanzhongScoutRecruitBonds,
|
|
...hanzhongMainRecruitBonds,
|
|
...hanzhongDecisiveBonds,
|
|
...jingDefenseBonds,
|
|
...fanCastleBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtyEighth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits,
|
|
...hanzhongScoutRecruitUnits,
|
|
...hanzhongMainRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds,
|
|
...hanzhongScoutRecruitBonds,
|
|
...hanzhongMainRecruitBonds,
|
|
...hanzhongDecisiveBonds,
|
|
...jingDefenseBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtySeventh) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits,
|
|
...hanzhongScoutRecruitUnits,
|
|
...hanzhongMainRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds,
|
|
...hanzhongScoutRecruitBonds,
|
|
...hanzhongMainRecruitBonds,
|
|
...hanzhongDecisiveBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtySixth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits,
|
|
...hanzhongScoutRecruitUnits,
|
|
...hanzhongMainRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds,
|
|
...hanzhongScoutRecruitBonds,
|
|
...hanzhongMainRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtyFifth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits,
|
|
...hanzhongScoutRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds,
|
|
...hanzhongScoutRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtyFourth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits,
|
|
...hanzhongOpeningRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds,
|
|
...hanzhongOpeningRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtyThird) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits,
|
|
...chengduSurrenderRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds,
|
|
...chengduSurrenderRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.thirtySecond) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits,
|
|
...luoCastleProperRecruitUnits,
|
|
...chengduPressureRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds,
|
|
...luoCastleProperRecruitBonds,
|
|
...chengduPressureRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
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(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits,
|
|
...luoCastleRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds,
|
|
...luoCastleRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.twentyEighth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits,
|
|
...fuPassRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds,
|
|
...fuPassRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.twentySeventh) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits,
|
|
...yizhouRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds,
|
|
...yizhouRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.twentySixth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits,
|
|
...changshaRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds,
|
|
...changshaRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.twentyFifth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits,
|
|
...wulingRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds,
|
|
...wulingRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.twentyFourth) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[
|
|
...xuzhouRecruitUnits,
|
|
...caoBreakRecruitUnits,
|
|
...liuBiaoRecruitUnits,
|
|
...zhugeRecruitUnits,
|
|
...jingzhouRecruitUnits,
|
|
...guiyangRecruitUnits
|
|
],
|
|
[
|
|
...xuzhouRecruitBonds,
|
|
...caoBreakRecruitBonds,
|
|
...liuBiaoRecruitBonds,
|
|
...zhugeRecruitBonds,
|
|
...jingzhouRecruitBonds,
|
|
...guiyangRecruitBonds
|
|
]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (battleId === campBattleIds.twentyThird) {
|
|
this.campaign = ensureCampaignRosterUnits(
|
|
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits, ...zhugeRecruitUnits, ...jingzhouRecruitUnits],
|
|
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds, ...zhugeRecruitBonds, ...jingzhouRecruitBonds]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
return;
|
|
}
|
|
|
|
if (
|
|
battleId === campBattleIds.seventeenth ||
|
|
battleId === campBattleIds.eighteenth ||
|
|
battleId === campBattleIds.nineteenth ||
|
|
battleId === campBattleIds.twentieth ||
|
|
battleId === campBattleIds.twentyFirst ||
|
|
battleId === campBattleIds.twentySecond
|
|
) {
|
|
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],
|
|
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds]
|
|
);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
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)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.campaign = ensureCampaignRosterUnits(xuzhouRecruitUnits, xuzhouRecruitBonds);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
}
|
|
|
|
private reportSummary() {
|
|
if (!this.report) {
|
|
return '';
|
|
}
|
|
|
|
const achieved = this.report.objectives.filter((objective) => objective.achieved).length;
|
|
const gold = this.campaign?.gold ?? this.report.rewardGold;
|
|
return `${this.report.battleTitle} ${this.report.turnNumber}턴 승리 · 보유 군자금 ${gold} · 목표 보상 ${achieved}/${this.report.objectives.length} · 격파 ${this.report.defeatedEnemies}/${this.report.totalEnemies}`;
|
|
}
|
|
|
|
private currentCampTitle() {
|
|
const battleId = this.currentCampBattleId();
|
|
if (this.campaign?.step === 'shu-han-foundation-camp') {
|
|
return '촉한 건국 후 군영';
|
|
}
|
|
if (this.campaign?.step === 'hanzhong-king-camp') {
|
|
return '한중왕 즉위 준비 군영';
|
|
}
|
|
if (this.campaign?.step === 'baidi-entrustment-camp') {
|
|
return '백제성 유탁 후 군영';
|
|
}
|
|
if (this.campaign?.step === 'northern-campaign-prep-camp') {
|
|
return '북벌 준비 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtySixth) {
|
|
return '오장원 최종전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtyFifth) {
|
|
return '위수 북안 압박 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtyFourth) {
|
|
return '위수 진영전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtyThird) {
|
|
return '노성 추격 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtySecond) {
|
|
return '기산 재출정 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtyFirst) {
|
|
return '한중 우로 방어 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixtieth) {
|
|
return '무도·음평 확보 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftyNinth) {
|
|
return '진창 공성전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftyEighth) {
|
|
return '기산 후퇴로 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftySeventh) {
|
|
return '가정 배치 위기 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftySixth) {
|
|
return '천수 진군로 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftyFifth) {
|
|
return '기산 출진로 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftyFourth) {
|
|
return '남중 평정 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftyThird) {
|
|
return '칠종칠금 6차전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftySecond) {
|
|
return '칠종칠금 5차전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftyFirst) {
|
|
return '칠종칠금 4차전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fiftieth) {
|
|
return '칠종칠금 3차전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortyNinth) {
|
|
return '칠종칠금 2차전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortyEighth) {
|
|
return '맹획 본대전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortySeventh) {
|
|
return '남중 안정로 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortySixth) {
|
|
return '이릉 화공전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortyFifth) {
|
|
return '이릉 진격로 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortyFourth) {
|
|
return '맥성 고립전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortyThird) {
|
|
return '공안 성문 변고 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortySecond) {
|
|
return '강릉 나루 경계전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortyFirst) {
|
|
return '번성 공성전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fortieth) {
|
|
return '한수 수공 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtyNinth) {
|
|
return '번성 외곽 압박전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtyEighth) {
|
|
return '형주 방위 전초전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtySeventh) {
|
|
return '한중 결전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtySixth) {
|
|
return '정군산 전초전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtyFifth) {
|
|
return '양평관 정찰 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtyFourth) {
|
|
return '가맹관 대면전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtyThird) {
|
|
return '성도 항복 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtySecond) {
|
|
return '면죽관 압박전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtyFirst) {
|
|
return '낙성 본성 공략 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirtieth) {
|
|
return '낙봉파 매복전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentyNinth) {
|
|
return '낙성 외곽전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentyEighth) {
|
|
return '부수관 진입 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentySeventh) {
|
|
return '익주 원군로 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentySixth) {
|
|
return '장사 노장 대면 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentyFifth) {
|
|
return '무릉 산길 확보 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentyFourth) {
|
|
return '계양 설득전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentyThird) {
|
|
return '형주 남부 진입 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentySecond) {
|
|
return '적벽 화공전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentyFirst) {
|
|
return '적벽 전초전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twentieth) {
|
|
return '강동 사절 준비 군영';
|
|
}
|
|
if (battleId === campBattleIds.nineteenth) {
|
|
return '장판파 돌파 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.eighteenth) {
|
|
return '박망파 승전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.seventeenth) {
|
|
return '와룡 출려 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixteenth) {
|
|
return '형주 의탁 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fifteenth) {
|
|
return '원소 의탁 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.fourteenth) {
|
|
return '서주 재기 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.thirteenth) {
|
|
return '하비 결전 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.twelfth) {
|
|
return '하비 포위 군영';
|
|
}
|
|
if (battleId === campBattleIds.eleventh) {
|
|
return '허도 의탁 군영';
|
|
}
|
|
if (battleId === campBattleIds.tenth) {
|
|
return '서주 상실 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.ninth) {
|
|
return '서주 성문 군영';
|
|
}
|
|
if (battleId === campBattleIds.eighth) {
|
|
return '소패 방위 군영';
|
|
}
|
|
if (battleId === campBattleIds.seventh) {
|
|
return '서주 인수 준비 군영';
|
|
}
|
|
if (battleId === campBattleIds.fifth) {
|
|
return '공손찬 진영 군영';
|
|
}
|
|
if (battleId === campBattleIds.sixth) {
|
|
return '서주 원군 준비 군영';
|
|
}
|
|
if (battleId === campBattleIds.fourth) {
|
|
return '광종 토벌 후 군영';
|
|
}
|
|
if (battleId === campBattleIds.third) {
|
|
return '광종 구원로 군영';
|
|
}
|
|
if (battleId === campBattleIds.second) {
|
|
return '황건 잔당 추격 군영';
|
|
}
|
|
return '탁현 의용군 군영';
|
|
}
|
|
|
|
private currentCampBattleId() {
|
|
return this.campaign?.latestBattleId ?? this.report?.battleId ?? defaultBattleScenario.id;
|
|
}
|
|
|
|
private availableCampDialogues() {
|
|
const battleId = this.currentCampBattleId();
|
|
const battleDialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId));
|
|
const dialogues = this.filterCampEventsByStep(battleDialogues);
|
|
return dialogues.length > 0 ? dialogues : campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(defaultBattleScenario.id));
|
|
}
|
|
|
|
private completedAvailableDialogues() {
|
|
const completed = this.completedCampDialogues();
|
|
return this.availableCampDialogues().filter((dialogue) => completed.includes(dialogue.id));
|
|
}
|
|
|
|
private availableCampVisits() {
|
|
const battleId = this.currentCampBattleId();
|
|
return this.filterCampEventsByStep(campVisits.filter((visit) => visit.availableAfterBattleIds.includes(battleId)));
|
|
}
|
|
|
|
private completedAvailableVisits() {
|
|
const completed = this.completedCampVisits();
|
|
return this.availableCampVisits().filter((visit) => completed.includes(visit.id));
|
|
}
|
|
|
|
private filterCampEventsByStep<T extends { availableDuringSteps?: CampaignStep[] }>(events: T[]) {
|
|
const step = this.campaign?.step;
|
|
if (!step) {
|
|
return events.filter((event) => !event.availableDuringSteps);
|
|
}
|
|
const stepEvents = events.filter((event) => event.availableDuringSteps?.includes(step));
|
|
return stepEvents.length > 0 ? stepEvents : events.filter((event) => !event.availableDuringSteps);
|
|
}
|
|
|
|
private renderStaticButtons() {
|
|
this.addTabButton('장수', 'status', 576, 38);
|
|
this.addTabButton('대화', 'dialogue', 650, 38);
|
|
this.addTabButton('방문', 'visit', 724, 38);
|
|
this.addTabButton('보급', 'supplies', 798, 38);
|
|
this.addTabButton('장비', 'equipment', 872, 38);
|
|
this.addTabButton('연표', 'progress', 946, 38);
|
|
this.addCommandButton('저장', 1030, 38, 76, () => {
|
|
soundDirector.playSelect();
|
|
this.showCampSaveSlotPanel();
|
|
});
|
|
const flow = this.currentSortieFlow();
|
|
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '다음 이야기';
|
|
this.addCommandButton(storyButtonLabel, 1152, 38, 142, () => {
|
|
soundDirector.playSelect();
|
|
if (this.isFinalEpilogueFlow(flow)) {
|
|
this.startVictoryStory();
|
|
return;
|
|
}
|
|
this.showSortiePrep();
|
|
});
|
|
}
|
|
|
|
private showCampSaveSlotPanel() {
|
|
if (this.saveSlotObjects.length > 0) {
|
|
this.hideCampSaveSlotPanel();
|
|
return;
|
|
}
|
|
|
|
const depth = 48;
|
|
const width = 430;
|
|
const height = 252;
|
|
const left = this.scale.width - width - 36;
|
|
const top = 70;
|
|
const activeSlot = this.campaign?.activeSaveSlot ?? getCampaignState().activeSaveSlot;
|
|
const slots = listCampaignSaveSlots();
|
|
|
|
const panel = this.add.rectangle(left, top, width, height, 0x0f1720, 0.98);
|
|
panel.setOrigin(0);
|
|
panel.setDepth(depth);
|
|
panel.setStrokeStyle(2, palette.gold, 0.86);
|
|
panel.setInteractive();
|
|
this.saveSlotObjects.push(panel);
|
|
|
|
const title = this.add.text(left + 22, top + 16, '군영 저장 슬롯', this.textStyle(22, '#f2e3bf', true));
|
|
title.setDepth(depth + 1);
|
|
this.saveSlotObjects.push(title);
|
|
|
|
const guide = this.add.text(
|
|
left + 22,
|
|
top + 48,
|
|
'군영 진행과 편성을 슬롯에 저장합니다. · 1~3키 저장 · Esc 닫기',
|
|
this.textStyle(13, '#aeb7c2')
|
|
);
|
|
guide.setDepth(depth + 1);
|
|
this.saveSlotObjects.push(guide);
|
|
|
|
for (let slot = 1; slot <= campaignSaveSlotCount; slot += 1) {
|
|
const summary = slots.find((candidate) => candidate.slot === slot) ?? { slot, exists: false };
|
|
this.renderCampSaveSlotRow(summary, activeSlot, left + 18, top + 82 + (slot - 1) * 52, width - 36, depth + 1);
|
|
}
|
|
|
|
const close = this.add.text(left + width - 58, top + 18, '닫기', this.textStyle(14, '#d8b15f', true));
|
|
close.setDepth(depth + 2);
|
|
close.setInteractive({ useHandCursor: true });
|
|
close.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.hideCampSaveSlotPanel();
|
|
});
|
|
this.saveSlotObjects.push(close);
|
|
}
|
|
|
|
private renderCampSaveSlotRow(
|
|
summary: CampaignSaveSlotSummary,
|
|
activeSlot: number,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
depth: number
|
|
) {
|
|
const active = summary.slot === activeSlot;
|
|
const row = this.add.rectangle(x, y, width, 44, active ? 0x263a2d : 0x172230, active ? 0.98 : 0.94);
|
|
row.setOrigin(0);
|
|
row.setDepth(depth);
|
|
row.setStrokeStyle(1, active ? palette.gold : palette.blue, active ? 0.86 : 0.62);
|
|
this.saveSlotObjects.push(row);
|
|
|
|
const title = this.add.text(x + 14, y + 8, `슬롯 ${summary.slot}`, this.textStyle(15, '#f2e3bf', true));
|
|
title.setDepth(depth + 1);
|
|
this.saveSlotObjects.push(title);
|
|
|
|
const keycap = this.add.rectangle(x + 66, y + 22, 26, 20, active ? 0x314a38 : 0x263647, active ? 0.98 : 0.88);
|
|
keycap.setDepth(depth + 1);
|
|
keycap.setStrokeStyle(1, active ? palette.gold : palette.blue, active ? 0.76 : 0.52);
|
|
this.saveSlotObjects.push(keycap);
|
|
|
|
const keyText = this.add.text(x + 66, y + 13, String(summary.slot), this.textStyle(11, active ? '#f2e3bf' : '#cbd6e2', true));
|
|
keyText.setOrigin(0.5, 0);
|
|
keyText.setDepth(depth + 2);
|
|
this.saveSlotObjects.push(keyText);
|
|
|
|
const detail = this.add.text(
|
|
x + 92,
|
|
y + 7,
|
|
summary.exists ? this.campSaveSlotDetail(summary) : '비어 있음',
|
|
this.textStyle(12, summary.exists ? '#d4dce6' : '#87919c', summary.exists)
|
|
);
|
|
detail.setDepth(depth + 1);
|
|
this.saveSlotObjects.push(detail);
|
|
|
|
const meta = this.add.text(
|
|
x + 92,
|
|
y + 24,
|
|
summary.exists ? this.campSaveSlotMeta(summary) : '새 저장으로 사용',
|
|
this.textStyle(10, summary.exists ? '#9fb0bf' : '#d8b15f', true)
|
|
);
|
|
meta.setDepth(depth + 1);
|
|
this.saveSlotObjects.push(meta);
|
|
|
|
const saveLabel = this.add.text(x + width - 50, y + 13, active ? '현재' : '저장', this.textStyle(12, active ? '#a8ffd0' : '#ffdf7b', true));
|
|
saveLabel.setOrigin(0.5, 0);
|
|
saveLabel.setDepth(depth + 1);
|
|
this.saveSlotObjects.push(saveLabel);
|
|
|
|
const run = () => this.requestSaveCampToSlot(summary.slot);
|
|
[row, title, keycap, keyText, detail, meta, saveLabel].forEach((object) => {
|
|
object.setInteractive({ useHandCursor: true });
|
|
object.on('pointerdown', run);
|
|
});
|
|
row.on('pointerover', () => row.setFillStyle(active ? 0x314a38 : 0x263647, 0.98));
|
|
row.on('pointerout', () => row.setFillStyle(active ? 0x263a2d : 0x172230, active ? 0.98 : 0.94));
|
|
}
|
|
|
|
private campSaveSlotDetail(summary: CampaignSaveSlotSummary) {
|
|
const progressTitle = summary.progressTitle ?? summary.battleTitle ?? '군영 진행';
|
|
return this.compactText(`군자금 ${summary.gold ?? 0} · ${progressTitle}`, 28);
|
|
}
|
|
|
|
private campSaveSlotMeta(summary: CampaignSaveSlotSummary) {
|
|
const roster = summary.rosterCount ? ` · ${summary.rosterCount}명` : '';
|
|
const supplies = summary.inventoryCount ? ` · 보급 ${summary.inventoryCount}` : '';
|
|
return this.compactText(`${summary.progressMeta ?? '진행 저장'} · ${this.formatCampSaveUpdatedAt(summary.updatedAt)}${roster}${supplies}`, 44);
|
|
}
|
|
|
|
private formatCampSaveUpdatedAt(updatedAt?: string) {
|
|
if (!updatedAt) {
|
|
return '저장 시각 없음';
|
|
}
|
|
|
|
const date = new Date(updatedAt);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return '저장 시각 미상';
|
|
}
|
|
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
const hour = String(date.getHours()).padStart(2, '0');
|
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
return `${month}/${day} ${hour}:${minute}`;
|
|
}
|
|
|
|
private requestSaveCampToSlot(slot: number) {
|
|
const activeSlot = this.campaign?.activeSaveSlot ?? getCampaignState().activeSaveSlot;
|
|
const summary = listCampaignSaveSlots().find((candidate) => candidate.slot === slot);
|
|
if (summary?.exists && summary.slot !== activeSlot) {
|
|
this.showCampSaveOverwriteConfirm(summary);
|
|
return;
|
|
}
|
|
|
|
this.saveCampToSlot(slot);
|
|
}
|
|
|
|
private showCampSaveOverwriteConfirm(summary: CampaignSaveSlotSummary) {
|
|
this.hideCampSaveConfirm();
|
|
this.pendingSaveSlot = summary.slot;
|
|
|
|
const depth = 64;
|
|
const width = 386;
|
|
const height = 166;
|
|
const x = this.scale.width - width - 58;
|
|
const y = 158;
|
|
|
|
const shade = this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.34);
|
|
shade.setOrigin(0);
|
|
shade.setDepth(depth - 1);
|
|
shade.setInteractive();
|
|
this.saveSlotConfirmObjects.push(shade);
|
|
|
|
const panel = this.add.rectangle(x, y, width, height, 0x111a24, 0.99);
|
|
panel.setOrigin(0);
|
|
panel.setDepth(depth);
|
|
panel.setStrokeStyle(2, palette.gold, 0.9);
|
|
panel.setInteractive();
|
|
this.saveSlotConfirmObjects.push(panel);
|
|
|
|
const title = this.add.text(x + 22, y + 18, `슬롯 ${summary.slot} 덮어쓰기`, this.textStyle(20, '#f2e3bf', true));
|
|
title.setDepth(depth + 1);
|
|
this.saveSlotConfirmObjects.push(title);
|
|
|
|
const detail = this.add.text(
|
|
x + 22,
|
|
y + 50,
|
|
`${this.campSaveSlotDetail(summary)}\n${this.campSaveSlotMeta(summary)}`,
|
|
this.textStyle(12, '#b9c3cf')
|
|
);
|
|
detail.setDepth(depth + 1);
|
|
detail.setLineSpacing(5);
|
|
this.saveSlotConfirmObjects.push(detail);
|
|
|
|
const message = this.add.text(x + 22, y + 96, '현재 군영 진행으로 이 슬롯을 교체합니다.', this.textStyle(12, '#ffdf7b', true));
|
|
message.setDepth(depth + 1);
|
|
this.saveSlotConfirmObjects.push(message);
|
|
|
|
const hint = this.add.text(x + 22, y + 112, 'Enter 확정 · Esc 취소', this.textStyle(11, '#9fb0bf', true));
|
|
hint.setDepth(depth + 1);
|
|
this.saveSlotConfirmObjects.push(hint);
|
|
|
|
const confirm = this.add.text(x + width - 174, y + 126, '덮어쓰기', this.textStyle(14, '#f2e3bf', true));
|
|
confirm.setDepth(depth + 1);
|
|
confirm.setPadding(14, 5, 14, 5);
|
|
confirm.setBackgroundColor('#6d4420');
|
|
confirm.setInteractive({ useHandCursor: true });
|
|
confirm.on('pointerdown', () => this.confirmCampSaveOverwrite());
|
|
this.saveSlotConfirmObjects.push(confirm);
|
|
|
|
const cancel = this.add.text(x + width - 70, y + 126, '취소', this.textStyle(14, '#d8b15f', true));
|
|
cancel.setDepth(depth + 1);
|
|
cancel.setPadding(14, 5, 14, 5);
|
|
cancel.setBackgroundColor('#1a2630');
|
|
cancel.setInteractive({ useHandCursor: true });
|
|
cancel.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.hideCampSaveConfirm();
|
|
});
|
|
this.saveSlotConfirmObjects.push(cancel);
|
|
}
|
|
|
|
private confirmCampSaveOverwrite() {
|
|
const slot = this.pendingSaveSlot;
|
|
if (!slot) {
|
|
this.hideCampSaveConfirm();
|
|
return;
|
|
}
|
|
|
|
this.saveCampToSlot(slot);
|
|
}
|
|
|
|
private saveCampToSlot(slot: number) {
|
|
soundDirector.playSelect();
|
|
const returnToSortiePrep = this.sortieObjects.length > 0;
|
|
this.hideCampSaveSlotPanel();
|
|
this.campaign = saveCampaignState(getCampaignState(), slot);
|
|
if (returnToSortiePrep) {
|
|
this.showSortiePrep();
|
|
} else {
|
|
this.render();
|
|
}
|
|
this.showCampNotice(`슬롯 ${this.campaign.activeSaveSlot}에 군영 진행을 저장했습니다.`);
|
|
}
|
|
|
|
private handleSaveSlotKey(event: KeyboardEvent) {
|
|
if (this.saveSlotConfirmObjects.length > 0) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
this.confirmCampSaveOverwrite();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.saveSlotObjects.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const slot = Number(event.key);
|
|
if (!Number.isInteger(slot) || slot < 1 || slot > campaignSaveSlotCount) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
this.requestSaveCampToSlot(slot);
|
|
}
|
|
|
|
private hideCampSaveSlotPanel() {
|
|
this.hideCampSaveConfirm();
|
|
this.saveSlotObjects.forEach((object) => object.destroy());
|
|
this.saveSlotObjects = [];
|
|
}
|
|
|
|
private hideCampSaveConfirm() {
|
|
this.saveSlotConfirmObjects.forEach((object) => object.destroy());
|
|
this.saveSlotConfirmObjects = [];
|
|
this.pendingSaveSlot = undefined;
|
|
}
|
|
|
|
private handleEscapeKey() {
|
|
if (this.saveSlotConfirmObjects.length > 0) {
|
|
soundDirector.playSelect();
|
|
this.hideCampSaveConfirm();
|
|
return;
|
|
}
|
|
|
|
if (this.saveSlotObjects.length > 0) {
|
|
soundDirector.playSelect();
|
|
this.hideCampSaveSlotPanel();
|
|
return;
|
|
}
|
|
|
|
if (this.sortieObjects.length > 0) {
|
|
soundDirector.playSelect();
|
|
this.hideSortiePrep();
|
|
}
|
|
}
|
|
|
|
private addTabButton(label: string, tab: CampTab, x: number, y: number) {
|
|
const bg = this.add.rectangle(x, y, 76, 34, 0x182431, 0.94);
|
|
bg.setStrokeStyle(1, palette.blue, 0.62);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.activeTab = tab;
|
|
this.render();
|
|
});
|
|
|
|
const text = this.add.text(x, y, label, {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '16px',
|
|
color: '#f2e3bf',
|
|
fontStyle: '700'
|
|
});
|
|
text.setOrigin(0.5);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.activeTab = tab;
|
|
this.render();
|
|
});
|
|
this.tabButtons.push({ tab, bg, text });
|
|
this.updateTabButtons();
|
|
}
|
|
|
|
private updateTabButtons() {
|
|
this.tabButtons.forEach(({ tab, bg, text }) => {
|
|
const active = this.activeTab === tab;
|
|
bg.setFillStyle(active ? 0x25384a : 0x182431, active ? 0.98 : 0.94);
|
|
bg.setStrokeStyle(1, active ? palette.gold : palette.blue, active ? 0.86 : 0.62);
|
|
text.setColor(active ? '#fff2b8' : '#f2e3bf');
|
|
});
|
|
}
|
|
|
|
private addCommandButton(label: string, x: number, y: number, width: number, action: () => void) {
|
|
const bg = this.add.rectangle(x, y, width, 36, 0x1a2630, 0.96);
|
|
bg.setStrokeStyle(1, palette.gold, 0.8);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
|
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
|
|
bg.on('pointerdown', action);
|
|
|
|
const text = this.add.text(x, y, label, {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '16px',
|
|
color: '#f2e3bf',
|
|
fontStyle: '700'
|
|
});
|
|
text.setOrigin(0.5);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', action);
|
|
}
|
|
|
|
private render() {
|
|
this.hideCampSaveSlotPanel();
|
|
this.clearContent();
|
|
this.visitedTabs.add(this.activeTab);
|
|
this.updateTabButtons();
|
|
this.renderUnitColumn();
|
|
this.renderReportPanel();
|
|
|
|
if (this.activeTab === 'dialogue') {
|
|
this.renderDialoguePanel();
|
|
return;
|
|
}
|
|
|
|
if (this.activeTab === 'visit') {
|
|
this.renderVisitPanel();
|
|
return;
|
|
}
|
|
|
|
if (this.activeTab === 'supplies') {
|
|
this.renderSuppliesPanel();
|
|
return;
|
|
}
|
|
|
|
if (this.activeTab === 'equipment') {
|
|
this.renderEquipmentPanel();
|
|
return;
|
|
}
|
|
|
|
if (this.activeTab === 'progress') {
|
|
this.renderProgressPanel();
|
|
return;
|
|
}
|
|
|
|
this.renderStatusPanel();
|
|
}
|
|
|
|
private showSortiePrep() {
|
|
this.hideCampSaveSlotPanel();
|
|
this.hideSortiePrep();
|
|
this.campaign = getCampaignState();
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
|
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
|
|
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments);
|
|
this.ensureSortieFocus();
|
|
|
|
const depth = 40;
|
|
const width = 1188;
|
|
const height = 676;
|
|
const x = Math.floor((this.scale.width - width) / 2);
|
|
const y = Math.floor((this.scale.height - height) / 2);
|
|
const flow = this.currentSortieFlow();
|
|
const isBattleSortie = Boolean(flow.nextBattleId);
|
|
const checklist = this.sortieChecklist();
|
|
const prepTitle = isBattleSortie ? '출진 준비' : '군영 의정';
|
|
const prepSubtitle = isBattleSortie
|
|
? this.sortiePrepSubtitle(checklist)
|
|
: this.isFinalEpilogueFlow(flow)
|
|
? '마지막 군영 기록을 정리하고 언제든 엔딩 화면으로 돌아갈 수 있습니다.'
|
|
: '군영 대화와 장부를 정리한 뒤 다음 의정에 함께할 무장을 선택합니다.';
|
|
|
|
const shade = this.trackSortie(this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.68));
|
|
shade.setOrigin(0);
|
|
shade.setDepth(depth);
|
|
shade.setInteractive();
|
|
|
|
const panel = this.trackSortie(this.add.rectangle(x, y, width, height, 0x101820, 0.98));
|
|
panel.setOrigin(0);
|
|
panel.setDepth(depth + 1);
|
|
panel.setStrokeStyle(3, palette.gold, 0.92);
|
|
|
|
const title = this.trackSortie(this.add.text(x + 34, y + 28, prepTitle, this.textStyle(30, '#f2e3bf', true)));
|
|
title.setDepth(depth + 2);
|
|
const subtitle = this.trackSortie(
|
|
this.add.text(x + 34, y + 70, this.compactText(prepSubtitle, 72), this.textStyle(15, '#d4dce6'))
|
|
);
|
|
subtitle.setDepth(depth + 2);
|
|
this.renderSortieHeaderSummary(x + 690, y + 26, 464, 58, depth + 2);
|
|
|
|
this.renderSortieBriefing(x + 34, y + 104, 560, 168, depth + 2);
|
|
this.renderSortieChecklist(x + 612, y + 104, 542, 168, depth + 2, checklist);
|
|
this.renderSortieUnitSummary(x + 34, y + 292, 390, 286, depth + 2);
|
|
this.renderSortieFormationPlan(x + 442, y + 292, 350, 286, depth + 2);
|
|
this.renderSortieFocusPanel(x + 810, y + 292, 344, 286, depth + 2);
|
|
this.renderSortieRewardHint(x + 34, y + 596, 430, 52, depth + 2);
|
|
this.renderSortieReserveTraining(x + 480, y + 596, 330, 52, depth + 2);
|
|
|
|
this.addSortieButton('저장', x + width - 348, y + height - 42, 100, () => {
|
|
soundDirector.playSelect();
|
|
this.showCampSaveSlotPanel();
|
|
}, depth + 3);
|
|
this.addSortieButton('군영으로', x + width - 236, y + height - 42, 112, () => {
|
|
soundDirector.playSelect();
|
|
this.hideSortiePrep();
|
|
}, depth + 3);
|
|
const primaryActionLabel = isBattleSortie ? '출진' : this.isFinalEpilogueFlow(flow) ? '엔딩 보기' : '의정 진행';
|
|
this.addSortieButton(primaryActionLabel, x + width - 118, y + height - 42, 112, () => {
|
|
soundDirector.playSelect();
|
|
this.startVictoryStory();
|
|
}, depth + 3, true);
|
|
}
|
|
|
|
private renderSortieHeaderSummary(x: number, y: number, width: number, height: number, depth: number) {
|
|
const selectedUnits = this.selectedSortieUnits();
|
|
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
|
|
const reserveUnits = this.sortieAllies().filter((unit) => !selectedIds.has(unit.id));
|
|
const selectedNames = selectedUnits.map((unit) => unit.name).join(', ');
|
|
const recommendedClassLine = this.sortieRecommendedClassLine();
|
|
const maxUnits = this.sortieMaxUnits();
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.78));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.gold, 0.36);
|
|
|
|
this.renderSortieBattleUiIcon('leadership', x + 22, y + 20, 24, depth + 1, 0.95);
|
|
this.trackSortie(
|
|
this.add.text(
|
|
x + 44,
|
|
y + 10,
|
|
this.compactText(`${hasBattle ? '출전' : '동행'} ${selectedUnits.length}/${maxUnits}: ${selectedNames || '없음'}`, 35),
|
|
this.textStyle(13, '#f2e3bf', true)
|
|
)
|
|
).setDepth(depth + 1);
|
|
this.renderSortieBattleUiIcon('focus', x + 22, y + 44, 22, depth + 1, 0.74);
|
|
this.trackSortie(
|
|
this.add.text(
|
|
x + 44,
|
|
y + 35,
|
|
this.compactText(`${hasBattle ? '추천' : '정비'} ${recommendedClassLine} · 대기 ${reserveUnits.length}명`, 35),
|
|
this.textStyle(12, reserveUnits.length > 0 ? '#c8d2dd' : '#9fb0bf', true)
|
|
)
|
|
).setDepth(depth + 1);
|
|
}
|
|
|
|
private renderSortieBriefing(x: number, y: number, width: number, height: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.blue, 0.48);
|
|
|
|
const briefing = this.nextSortieBriefing();
|
|
const scenario = this.nextSortieScenario();
|
|
const hasBattle = Boolean(scenario);
|
|
const objective = scenario ? scenario.victoryConditionLabel : '군영 의정 진행';
|
|
const terrain = scenario ? this.sortieTerrainSummary(scenario) : '군영';
|
|
const enemyLabel = scenario ? '적' : '장면';
|
|
const enemies = scenario ? this.sortieEnemySummary(scenario) : '전투 없음';
|
|
const recommendedClasses = this.sortieRecommendedClassLine(scenario);
|
|
const objectiveLabel = hasBattle ? '목표' : '진행';
|
|
const terrainLabel = hasBattle ? '지형' : '장소';
|
|
const recommendationLabel = hasBattle ? '추천' : '동행';
|
|
this.trackSortie(this.add.text(x + 18, y + 12, briefing.eyebrow, this.textStyle(15, '#9fb0bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 18, y + 34, briefing.title, this.textStyle(22, '#f2e3bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(
|
|
this.add.text(x + 18, y + 65, briefing.description, {
|
|
...this.textStyle(14, '#d4dce6'),
|
|
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
|
lineSpacing: 3
|
|
})
|
|
).setDepth(depth + 1);
|
|
const chipY = y + height - 46;
|
|
const chipWidth = Math.floor((width - 48) / 4);
|
|
this.renderSortieInfoChip(objectiveLabel, objective, x + 18, chipY, chipWidth, depth + 1);
|
|
this.renderSortieInfoChip(terrainLabel, terrain, x + 24 + chipWidth, chipY, chipWidth, depth + 1);
|
|
this.renderSortieInfoChip(enemyLabel, enemies, x + 30 + chipWidth * 2, chipY, chipWidth, depth + 1);
|
|
this.renderSortieInfoChip(recommendationLabel, recommendedClasses, x + 36 + chipWidth * 3, chipY, chipWidth, depth + 1);
|
|
}
|
|
|
|
private renderSortieInfoChip(label: string, value: string, x: number, y: number, width: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, 34, 0x101820, 0.94));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, 0x53606c, 0.42);
|
|
this.trackSortie(this.add.text(x + 8, y + 4, label, this.textStyle(9, '#9fb0bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 8, y + 18, this.compactText(value, 13), this.textStyle(10, '#f2e3bf', true))).setDepth(depth + 1);
|
|
}
|
|
|
|
private renderSortieChecklist(x: number, y: number, width: number, height: number, depth: number, items = this.sortieChecklist()) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.gold, 0.5);
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
this.trackSortie(this.add.text(x + 18, y + 14, hasBattle ? '준비 체크' : '의정 체크', this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1);
|
|
|
|
const rowGap = Math.max(15, Math.floor((height - 50) / Math.max(1, items.length)));
|
|
const compact = rowGap < 20;
|
|
items.forEach((item, index) => {
|
|
const rowY = y + 42 + index * rowGap;
|
|
const color = item.complete ? '#a8ffd0' : item.priority === 'required' ? '#ff9d7d' : '#ffdf7b';
|
|
const status = item.complete ? '완료' : item.priority === 'required' ? '필수' : '권장';
|
|
this.trackSortie(this.add.text(x + 18, rowY, status, this.textStyle(compact ? 10 : 13, color, true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 68, rowY, item.label, this.textStyle(compact ? 11 : 14, '#d4dce6', true))).setDepth(depth + 1);
|
|
const detail = this.trackSortie(this.add.text(x + 184, rowY, this.compactText(item.detail, compact ? 36 : 42), this.textStyle(compact ? 10 : 12, item.complete ? '#9fb0bf' : '#d8b15f')));
|
|
detail.setDepth(depth + 1);
|
|
});
|
|
}
|
|
|
|
private renderSortieUnitSummary(x: number, y: number, width: number, height: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.blue, 0.48);
|
|
const availableAllies = this.sortieAllies();
|
|
const rosterUnits = this.sortieRosterUnits();
|
|
const displayAllies = this.sortieRosterDisplayUnits(rosterUnits);
|
|
const selectedCount = this.selectedSortieUnitIds.length;
|
|
const maxUnits = this.sortieMaxUnits();
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const selectedLabel = hasBattle ? '출전' : '동행';
|
|
const reserveCount = Math.max(0, availableAllies.length - selectedCount);
|
|
const rosterView = this.sortieRosterViewState(rosterUnits.length, height);
|
|
const { denseRoster, rowGap, rowHeight, listTopOffset, listFooterHeight, maxVisibleRows, maxScroll } = rosterView;
|
|
this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll);
|
|
const visibleAllies = displayAllies.slice(this.sortieRosterScroll, this.sortieRosterScroll + maxVisibleRows);
|
|
const handleRosterWheel = (
|
|
_pointer: Phaser.Input.Pointer,
|
|
_deltaX: number,
|
|
deltaY: number,
|
|
_deltaZ: number,
|
|
event?: { stopPropagation: () => void }
|
|
) => {
|
|
event?.stopPropagation();
|
|
const direction = Math.sign(deltaY);
|
|
if (direction === 0 || maxScroll <= 0) {
|
|
return;
|
|
}
|
|
this.updateSortieRosterScroll(this.sortieRosterScroll + direction * 3, maxScroll);
|
|
};
|
|
if (maxScroll > 0) {
|
|
bg.setInteractive({ useHandCursor: false });
|
|
bg.on('wheel', handleRosterWheel);
|
|
}
|
|
this.trackSortie(
|
|
this.add.text(x + 18, y + 14, `보유 무장 ${rosterUnits.length} · ${selectedLabel} ${selectedCount}/${maxUnits}`, this.textStyle(17, '#f2e3bf', true))
|
|
).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + width - 18, y + 17, `대기 ${reserveCount}`, this.textStyle(11, '#9fb0bf', true))).setOrigin(1, 0).setDepth(depth + 1);
|
|
|
|
visibleAllies.forEach((unit, index) => {
|
|
const rowY = y + listTopOffset + index * rowGap;
|
|
const selected = this.isSortieSelected(unit.id);
|
|
const required = this.isRequiredSortieUnit(unit.id);
|
|
const recommendation = this.sortieRecommendation(unit.id);
|
|
const summary = this.sortieUnitTacticalSummary(unit);
|
|
const availability = this.sortieUnitAvailability(unit);
|
|
const status = this.unitRosterStatus(unit);
|
|
const recommendedReserve = Boolean(recommendation && !selected);
|
|
const disabled = !availability.available;
|
|
const rowColor = disabled ? 0x12161b : selected ? 0x172a22 : recommendedReserve ? 0x241f17 : 0x151b24;
|
|
const rowAlpha = disabled ? 0.62 : selected ? 0.96 : 0.84;
|
|
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, rowColor, rowAlpha));
|
|
row.setOrigin(0);
|
|
row.setDepth(depth + 1);
|
|
row.setStrokeStyle(1, disabled ? 0x53606c : selected ? palette.green : recommendedReserve ? palette.gold : 0x53606c, disabled ? 0.22 : selected ? 0.7 : recommendedReserve ? 0.54 : 0.36);
|
|
row.setInteractive({ useHandCursor: true });
|
|
row.on('wheel', handleRosterWheel);
|
|
row.on('pointerover', () => {
|
|
this.sortieFocusedUnitId = unit.id;
|
|
});
|
|
row.on('pointerdown', () => {
|
|
if (!availability.available) {
|
|
this.sortieFocusedUnitId = unit.id;
|
|
this.showCampNotice(`${unit.name}: ${availability.reason}`);
|
|
this.showSortiePrep();
|
|
return;
|
|
}
|
|
this.toggleSortieUnit(unit.id);
|
|
});
|
|
|
|
this.renderSortieBattleUiIcon(this.unitClassIconKey(unit), x + 36, rowY + (denseRoster ? 5 : 10), denseRoster ? 17 : 24, depth + 2, disabled ? 0.42 : selected ? 1 : 0.68);
|
|
const nameColor = disabled ? '#68727e' : selected ? '#f2e3bf' : recommendedReserve ? '#d8b15f' : '#c8d2dd';
|
|
this.trackSortie(this.add.text(x + 58, rowY - 1, `${unit.name} Lv${unit.level}`, this.textStyle(denseRoster ? 10 : 13, nameColor, true))).setDepth(depth + 2);
|
|
const statusColor = status.tone === 'disabled' ? '#77818c' : status.tone === 'selected' ? '#a8ffd0' : status.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
|
const statusText = this.trackSortie(this.add.text(x + width - 28, rowY - 1, status.label, this.textStyle(denseRoster ? 8 : 10, statusColor, true)));
|
|
statusText.setOrigin(1, 0);
|
|
statusText.setDepth(depth + 2);
|
|
this.trackSortie(this.add.text(x + 58, rowY + (denseRoster ? 10 : 17), this.compactText(`${unit.className} · HP ${unit.hp}/${unit.maxHp}`, denseRoster ? 32 : 23), this.textStyle(denseRoster ? 8 : 10, disabled ? '#68727e' : '#d4dce6', true))).setDepth(depth + 2);
|
|
if (denseRoster) {
|
|
return;
|
|
}
|
|
{
|
|
this.drawSortieBar(x + 218, rowY + 22, 58, 6, unit.hp / unit.maxHp, disabled ? 0x53606c : selected ? palette.green : palette.gold, depth + 2);
|
|
this.renderSortieEquipmentIcons(unit, x + 292, rowY + 24, !disabled && selected, depth + 2, 16, 12, 20);
|
|
}
|
|
const secondLine = disabled ? availability.reason : recommendation && !selected ? this.sortieRecommendationHint(recommendation, unit, 22) : summary.bondLine;
|
|
this.trackSortie(this.add.text(x + 58, rowY + 32, this.compactText(secondLine, 36), this.textStyle(9, disabled ? '#68727e' : selected ? '#a8ffd0' : recommendedReserve ? '#ffdf7b' : '#87919c', selected || recommendedReserve))).setDepth(depth + 2);
|
|
});
|
|
|
|
if (maxScroll > 0) {
|
|
const rangeEnd = this.sortieRosterScroll + visibleAllies.length;
|
|
const info = this.trackSortie(
|
|
this.add.text(
|
|
x + width - 18,
|
|
y + height - 20,
|
|
`${this.sortieRosterScroll + 1}-${rangeEnd}/${rosterUnits.length} · 휠로 이동`,
|
|
this.textStyle(11, '#9fb0bf', true)
|
|
)
|
|
);
|
|
info.setOrigin(1, 0);
|
|
info.setDepth(depth + 1);
|
|
|
|
const trackX = x + width - 12;
|
|
const trackY = y + listTopOffset;
|
|
const trackHeight = height - listFooterHeight;
|
|
const scrollTrack = this.trackSortie(this.add.rectangle(trackX, trackY, 4, trackHeight, 0x05070a, 0.78));
|
|
scrollTrack.setOrigin(0, 0);
|
|
scrollTrack.setDepth(depth + 1);
|
|
const thumbHeight = Math.max(24, trackHeight * (maxVisibleRows / rosterUnits.length));
|
|
const thumbY = trackY + (trackHeight - thumbHeight) * (this.sortieRosterScroll / maxScroll);
|
|
const scrollThumb = this.trackSortie(this.add.rectangle(trackX, thumbY, 4, thumbHeight, palette.gold, 0.84));
|
|
scrollThumb.setOrigin(0, 0);
|
|
scrollThumb.setDepth(depth + 2);
|
|
}
|
|
}
|
|
|
|
private sortieRosterDisplayUnits(allies: UnitData[]) {
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
const order = new Map(allies.map((unit, index) => [unit.id, index]));
|
|
return [...allies].sort((a, b) => {
|
|
const aSelected = selected.has(a.id);
|
|
const bSelected = selected.has(b.id);
|
|
if (aSelected !== bSelected) {
|
|
return Number(bSelected) - Number(aSelected);
|
|
}
|
|
const aAvailable = this.sortieUnitAvailability(a).available;
|
|
const bAvailable = this.sortieUnitAvailability(b).available;
|
|
if (aAvailable !== bAvailable) {
|
|
return Number(bAvailable) - Number(aAvailable);
|
|
}
|
|
const aRecommended = Boolean(this.sortieRecommendation(a.id));
|
|
const bRecommended = Boolean(this.sortieRecommendation(b.id));
|
|
if (aRecommended !== bRecommended) {
|
|
return Number(bRecommended) - Number(aRecommended);
|
|
}
|
|
const aRequired = this.isRequiredSortieUnit(a.id);
|
|
const bRequired = this.isRequiredSortieUnit(b.id);
|
|
if (aRequired !== bRequired) {
|
|
return Number(bRequired) - Number(aRequired);
|
|
}
|
|
return (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0);
|
|
});
|
|
}
|
|
|
|
private updateSortieRosterScroll(nextScroll: number, maxScroll: number) {
|
|
const clamped = Phaser.Math.Clamp(Math.round(nextScroll), 0, maxScroll);
|
|
if (clamped === this.sortieRosterScroll) {
|
|
return;
|
|
}
|
|
this.sortieRosterScroll = clamped;
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private sortieRosterViewState(totalUnits: number, height = 286) {
|
|
const denseRoster = totalUnits > 12;
|
|
const rowGap = denseRoster ? 22 : totalUnits > 7 ? 30 : totalUnits > 5 ? 38 : totalUnits > 4 ? 43 : 48;
|
|
const rowHeight = denseRoster ? 20 : totalUnits > 7 ? 24 : rowGap - 6;
|
|
const listTopOffset = denseRoster ? 46 : 50;
|
|
const listFooterHeight = denseRoster ? 58 : 68;
|
|
const naturalVisibleRows = Math.max(1, Math.floor((height - listTopOffset - 10 - rowHeight) / rowGap) + 1);
|
|
const scrollVisibleRows = Math.max(1, Math.floor((height - listFooterHeight) / rowGap));
|
|
const maxVisibleRows = totalUnits <= naturalVisibleRows ? totalUnits : scrollVisibleRows;
|
|
const maxScroll = Math.max(0, totalUnits - maxVisibleRows);
|
|
return {
|
|
denseRoster,
|
|
rowGap,
|
|
rowHeight,
|
|
listTopOffset,
|
|
listFooterHeight,
|
|
maxVisibleRows,
|
|
maxScroll,
|
|
scroll: Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll)
|
|
};
|
|
}
|
|
|
|
private renderSortieFormationPlan(x: number, y: number, width: number, height: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.gold, 0.5);
|
|
|
|
const plan = this.sortiePlanSummary();
|
|
const scenario = this.nextSortieScenario();
|
|
const hasBattle = Boolean(scenario);
|
|
const canRecommend = this.canApplyRecommendedSortiePlan(scenario);
|
|
this.trackSortie(this.add.text(x + 18, y + 14, hasBattle ? '출전 슬롯' : '동행 명단', this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1);
|
|
this.renderSortiePanelButton(hasBattle ? '추천 편성' : '추천 동행', x + width - 104, y + 10, 86, 22, canRecommend, false, () => this.applyRecommendedSortiePlan(), depth + 1);
|
|
this.trackSortie(
|
|
this.add.text(
|
|
x + 18,
|
|
y + 38,
|
|
hasBattle
|
|
? `${plan.selectedCount}/${plan.maxCount}명 · 지형 ${plan.terrainGrade} · ${this.sortieRecommendedClassLine()}`
|
|
: `${plan.selectedCount}/${plan.maxCount}명 · 군영 의정 · ${this.sortieRecommendedClassLine()}`,
|
|
this.textStyle(11, '#d8b15f', true)
|
|
)
|
|
).setDepth(depth + 1);
|
|
|
|
this.renderSortieDeploymentMap(x + 16, y + 58, width - 32, 62, depth + 1);
|
|
this.renderSortieSlotCards(x + 16, y + 126, width - 32, 104, plan, depth + 1);
|
|
const planFeedback = this.sortiePlanFeedback || plan.warningLine;
|
|
this.trackSortie(
|
|
this.add.text(
|
|
x + 16,
|
|
y + 236,
|
|
this.compactText(planFeedback, 43),
|
|
this.textStyle(11, this.sortiePlanFeedback ? '#a8ffd0' : plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true)
|
|
)
|
|
).setDepth(depth + 1);
|
|
|
|
this.renderSortieRoleQuickButtons(x + 16, y + 256, width - 32, 22, depth + 1);
|
|
}
|
|
|
|
private renderSortieSlotCards(x: number, y: number, width: number, height: number, plan: SortiePlanSummary, depth: number) {
|
|
const selectedUnits = this.selectedSortieUnits();
|
|
const maxCount = plan.maxCount;
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const columns = 2;
|
|
const gap = 8;
|
|
const rows = Math.max(1, Math.ceil(maxCount / columns));
|
|
const slotWidth = Math.floor((width - gap) / columns);
|
|
const slotHeight = Math.min(30, Math.floor((height - gap * (rows - 1)) / rows));
|
|
|
|
Array.from({ length: maxCount }, (_, index) => {
|
|
const unit = selectedUnits[index];
|
|
const role = unit ? this.sortieFormationRole(unit) : undefined;
|
|
const slotX = x + (index % columns) * (slotWidth + gap);
|
|
const slotY = y + Math.floor(index / columns) * (slotHeight + gap);
|
|
const filled = Boolean(unit);
|
|
const focused = Boolean(unit && this.sortieFocusedUnitId === unit.id);
|
|
const bg = this.trackSortie(this.add.rectangle(slotX, slotY, slotWidth, slotHeight, filled ? 0x172a22 : 0x111922, filled ? 0.94 : 0.76));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(focused ? 2 : 1, focused ? palette.gold : filled ? palette.green : 0x53606c, focused ? 0.92 : filled ? 0.58 : 0.34);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerdown', () => {
|
|
if (!unit) {
|
|
this.showCampNotice(`왼쪽 명단에서 ${hasBattle ? '출전' : '동행'}할 무장을 선택하세요.`);
|
|
return;
|
|
}
|
|
this.sortieFocusedUnitId = unit.id;
|
|
soundDirector.playSelect();
|
|
this.showSortiePrep();
|
|
});
|
|
|
|
const labelColor = filled ? '#f2e3bf' : '#7f8994';
|
|
this.trackSortie(this.add.text(slotX + 8, slotY + 5, `${index + 1}`, this.textStyle(10, labelColor, true))).setDepth(depth + 1);
|
|
if (unit && role) {
|
|
this.renderSortieBattleUiIcon(this.formationRoleIconKey(role), slotX + 28, slotY + slotHeight / 2, 18, depth + 1, 0.96);
|
|
const roleLabel = this.sortieFormationRoleLabel(role, hasBattle);
|
|
this.trackSortie(this.add.text(slotX + 42, slotY + 4, this.compactText(unit.name, 8), this.textStyle(11, '#f2e3bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(slotX + 42, slotY + 17, roleLabel, this.textStyle(8, '#9fb0bf', true))).setDepth(depth + 1);
|
|
} else {
|
|
this.trackSortie(this.add.text(slotX + 28, slotY + 8, hasBattle ? '빈 슬롯' : '빈 동행', this.textStyle(10, '#7f8994', true))).setDepth(depth + 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
private renderSortieRoleQuickButtons(x: number, y: number, width: number, height: number, depth: number) {
|
|
const focusedUnit = this.sortieFocusedUnit();
|
|
const enabled = Boolean(focusedUnit && this.isSortieSelected(focusedUnit.id));
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const gap = 6;
|
|
const buttonWidth = Math.floor((width - gap * (sortieFormationSlotDefinitions.length - 1)) / sortieFormationSlotDefinitions.length);
|
|
sortieFormationSlotDefinitions.forEach((slot, index) => {
|
|
const buttonX = x + index * (buttonWidth + gap);
|
|
const active = Boolean(focusedUnit && this.sortieFormationRole(focusedUnit) === slot.role);
|
|
const roleCopy = this.sortieFormationRoleCopy(slot.role, hasBattle);
|
|
const bg = this.trackSortie(this.add.rectangle(buttonX, y, buttonWidth, height, active ? 0x263a2d : 0x111922, enabled ? 0.94 : 0.58));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, active ? palette.green : 0x53606c, active ? 0.78 : 0.36);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerdown', () => this.assignFocusedSortieRole(slot.role));
|
|
this.trackSortie(this.add.text(buttonX + buttonWidth / 2, y + 5, roleCopy.label, this.textStyle(9, active ? '#a8ffd0' : enabled ? '#c8d2dd' : '#77818c', true))).setOrigin(0.5, 0).setDepth(depth + 1);
|
|
});
|
|
}
|
|
|
|
private renderSortieRoleSlots(x: number, y: number, width: number, height: number, plan: SortiePlanSummary, depth: number) {
|
|
const gap = 8;
|
|
const slotWidth = Math.floor((width - gap) / 2);
|
|
const slotHeight = Math.floor((height - 6) / 2);
|
|
const focusedUnit = this.sortieFocusedUnit();
|
|
const focusedUnitSelected = Boolean(focusedUnit && this.isSortieSelected(focusedUnit.id));
|
|
plan.formationSlots.forEach((slot, index) => {
|
|
const column = index % 2;
|
|
const row = Math.floor(index / 2);
|
|
const slotX = x + column * (slotWidth + gap);
|
|
const slotY = y + row * (slotHeight + 6);
|
|
const filled = slot.unitNames.length > 0;
|
|
const focusedRole = focusedUnitSelected && focusedUnit ? this.sortieFormationRole(focusedUnit) === slot.role : false;
|
|
const bg = this.trackSortie(this.add.rectangle(slotX, slotY, slotWidth, slotHeight, filled ? 0x172a22 : 0x111922, filled ? 0.94 : 0.78));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(focusedRole ? 2 : 1, focusedRole ? palette.gold : filled ? palette.green : 0x53606c, focusedRole ? 0.92 : filled ? 0.58 : 0.34);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerdown', () => this.assignFocusedSortieRole(slot.role));
|
|
bg.on('pointerover', () => {
|
|
if (focusedUnitSelected) {
|
|
bg.setFillStyle(filled ? 0x22342b : 0x182330, 0.96);
|
|
}
|
|
});
|
|
bg.on('pointerout', () => {
|
|
bg.setFillStyle(filled ? 0x172a22 : 0x111922, filled ? 0.94 : 0.78);
|
|
});
|
|
this.renderSortieBattleUiIcon(this.formationRoleIconKey(slot.role), slotX + 14, slotY + slotHeight / 2, 20, depth + 1, filled ? 0.98 : 0.58);
|
|
this.trackSortie(this.add.text(slotX + 30, slotY + 2, `${slot.label} ${slot.unitNames.length}`, this.textStyle(11, focusedRole ? '#ffdf7b' : filled ? '#f2e3bf' : '#9fb0bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(slotX + 30, slotY + 15, this.compactText(slot.unitNames.join(', ') || slot.description, 17), this.textStyle(9, filled ? '#c8d2dd' : '#7f8994', filled))).setDepth(depth + 1);
|
|
});
|
|
}
|
|
|
|
private renderSortieFocusPanel(x: number, y: number, width: number, height: number, depth: number) {
|
|
const unit = this.sortieFocusedUnit();
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.gold, 0.42);
|
|
|
|
if (!unit) {
|
|
this.trackSortie(this.add.text(x + 12, y + 18, '선택 가능한 무장이 없습니다.', this.textStyle(12, '#9fb0bf'))).setDepth(depth + 1);
|
|
return;
|
|
}
|
|
|
|
const status = this.unitRosterStatus(unit);
|
|
const availability = this.sortieUnitAvailability(unit);
|
|
const recommendation = this.sortieRecommendation(unit.id);
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const roleLabel = this.sortieFormationRoleLabel(this.sortieFormationRole(unit), hasBattle);
|
|
const terrainScore = this.sortieTerrainScore(unit, this.nextSortieScenario());
|
|
const terrainGrade = this.sortieTerrainGrade(terrainScore);
|
|
const statusColor = status.tone === 'disabled' ? '#77818c' : status.tone === 'selected' ? '#a8ffd0' : status.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
|
const selected = this.isSortieSelected(unit.id);
|
|
const reason = !availability.available
|
|
? availability.reason
|
|
: recommendation
|
|
? this.sortieRecommendationHint(recommendation, unit, 26)
|
|
: selected
|
|
? hasBattle
|
|
? `${roleLabel} 배치 중입니다. 역할 버튼으로 시작 위치 성향을 바꿀 수 있습니다.`
|
|
: `${roleLabel} 성향으로 동행 중입니다. 역할 버튼으로 군영 기록 성향을 바꿀 수 있습니다.`
|
|
: hasBattle
|
|
? '명단이나 버튼을 눌러 출전 슬롯에 배치할 수 있습니다.'
|
|
: '명단이나 버튼을 눌러 다음 장면 동행 명단에 넣을 수 있습니다.';
|
|
|
|
this.trackSortie(this.add.text(x + 18, y + 14, '선택 무장 상세', this.textStyle(17, '#f2e3bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + width - 16, y + 16, status.label, this.textStyle(11, statusColor, true))).setOrigin(1, 0).setDepth(depth + 1);
|
|
this.renderSortieBattleUiIcon(this.unitClassIconKey(unit), x + 34, y + 58, 42, depth + 1, availability.available ? 1 : 0.5);
|
|
this.trackSortie(this.add.text(x + 66, y + 42, `${unit.name} Lv${unit.level}`, this.textStyle(18, availability.available ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 66, y + 68, `${unit.className} · ${getUnitClass(unit.classKey).role}`, this.textStyle(12, availability.available ? '#d4dce6' : '#77818c', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + width - 16, y + 68, `HP ${unit.hp}/${unit.maxHp}`, this.textStyle(12, unit.hp < unit.maxHp ? '#ffdf7b' : '#a8ffd0', true))).setOrigin(1, 0).setDepth(depth + 1);
|
|
this.drawSortieBar(x + 66, y + 88, width - 92, 7, unit.hp / unit.maxHp, availability.available ? palette.green : 0x53606c, depth + 1);
|
|
|
|
const statY = y + 108;
|
|
const chipWidth = 72;
|
|
this.renderSortieStatChip('might', '무', unit.stats.might, x + 18, statY, chipWidth, depth + 1);
|
|
this.renderSortieStatChip('intelligence', '지', unit.stats.intelligence, x + 96, statY, chipWidth, depth + 1);
|
|
this.renderSortieStatChip('leadership', '통', unit.stats.leadership, x + 174, statY, chipWidth, depth + 1);
|
|
this.renderSortieStatChip('move', '이', unit.move, x + 252, statY, chipWidth, depth + 1);
|
|
|
|
this.trackSortie(this.add.text(x + 18, y + 137, `지형 ${terrainGrade} ${terrainScore} · ${this.sortieTerrainLine(unit)}`, this.textStyle(11, '#d8b15f', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 18, y + 157, this.compactText(reason, 38), this.textStyle(11, recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : availability.available ? '#c8d2dd' : '#87919c', true))).setDepth(depth + 1);
|
|
|
|
equipmentSlots.forEach((slot, index) => {
|
|
const rowY = y + 174 + index * 20;
|
|
const state = unit.equipment[slot];
|
|
const item = getItem(state.itemId);
|
|
const iconX = x + 28;
|
|
this.renderSortieBattleUiIcon(this.itemIconKeyForSortie(slot), iconX, rowY + 8, 18, depth + 1, availability.available ? 0.96 : 0.5);
|
|
this.trackSortie(this.add.text(x + 46, rowY + 2, `${equipmentSlotLabels[slot]} · ${item.name} Lv${state.level}`, this.textStyle(11, '#f2e3bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 174, rowY + 2, this.compactText(this.itemBonusText(item), 14), this.textStyle(10, '#9fb0bf', true))).setDepth(depth + 1);
|
|
this.drawSortieBar(x + 46, rowY + 15, 104, 5, state.exp / equipmentExpToNext(state.level), slot === 'weapon' ? palette.gold : slot === 'armor' ? palette.blue : palette.green, depth + 1);
|
|
const canSwap = this.equipmentInventoryEntries(slot).some((entry) => entry.item.id !== item.id);
|
|
this.renderSortiePanelButton(canSwap ? '교체' : '없음', x + width - 58, rowY, 40, 17, canSwap, false, () => this.swapUnitEquipment(unit.id, slot, undefined, true), depth + 1);
|
|
});
|
|
|
|
this.renderSortieSupplyAssignment(unit, x + 18, y + 236, width - 36, depth + 1);
|
|
|
|
const actionLabel = selected
|
|
? this.isRequiredSortieUnit(unit.id)
|
|
? hasBattle
|
|
? '필수 출전'
|
|
: '필수 동행'
|
|
: hasBattle
|
|
? '출전 해제'
|
|
: '동행 해제'
|
|
: hasBattle
|
|
? '출전 등록'
|
|
: '동행 등록';
|
|
this.renderSortiePanelButton(actionLabel, x + 18, y + height - 28, 108, 22, availability.available, selected, () => this.toggleSortieUnit(unit.id), depth + 1);
|
|
const footerText = selected
|
|
? hasBattle
|
|
? `${roleLabel} · 역할은 중앙 버튼으로 변경`
|
|
: `${roleLabel} · 의정 역할은 중앙 버튼으로 변경`
|
|
: `${this.campaign?.reserveTrainingAssignments[unit.id] ? '개별' : '기본'} ${this.reserveTrainingFocusDefinitionForUnit(unit.id).label} 예정`;
|
|
this.trackSortie(this.add.text(x + 140, y + height - 24, footerText, this.textStyle(10, selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 1);
|
|
}
|
|
|
|
private renderSortieBattleUiIcon(iconKey: BattleUiIconKey, x: number, y: number, size: number, depth: number, alpha = 1) {
|
|
if (!this.textures.exists(battleUiIconTextureKey)) {
|
|
const fallback = this.trackSortie(this.add.circle(x, y, size / 2, palette.gold, 0.18));
|
|
fallback.setDepth(depth);
|
|
return;
|
|
}
|
|
|
|
const icon = this.trackSortie(this.add.image(x, y, battleUiIconTextureKey, battleUiIconFrames[iconKey]));
|
|
icon.setDisplaySize(size, size);
|
|
icon.setAlpha(alpha);
|
|
icon.setDepth(depth);
|
|
}
|
|
|
|
private unitClassIconKey(unit: UnitData): BattleUiIconKey {
|
|
switch (unit.classKey) {
|
|
case 'lord':
|
|
return 'leadership';
|
|
case 'infantry':
|
|
return 'armor';
|
|
case 'spearman':
|
|
return 'spear';
|
|
case 'cavalry':
|
|
return 'horse';
|
|
case 'archer':
|
|
return 'bow';
|
|
case 'strategist':
|
|
return 'strategy';
|
|
case 'quartermaster':
|
|
return 'heal';
|
|
case 'rebelLeader':
|
|
return 'axe';
|
|
case 'bandit':
|
|
case 'yellowTurban':
|
|
default:
|
|
return 'sword';
|
|
}
|
|
}
|
|
|
|
private formationRoleIconKey(role: SortieFormationRole): BattleUiIconKey {
|
|
if (role === 'front') {
|
|
return 'armor';
|
|
}
|
|
if (role === 'flank') {
|
|
return 'horse';
|
|
}
|
|
if (role === 'support') {
|
|
return 'strategy';
|
|
}
|
|
return 'focus';
|
|
}
|
|
|
|
private sortieFormationRoleCopy(role: SortieFormationRole, hasBattle = Boolean(this.nextSortieScenario())) {
|
|
const definitions = hasBattle ? sortieFormationSlotDefinitions : campCompanionRoleDefinitions;
|
|
return definitions.find((slot) => slot.role === role) ?? definitions[definitions.length - 1];
|
|
}
|
|
|
|
private sortieFormationRoleLabel(role: SortieFormationRole, hasBattle = Boolean(this.nextSortieScenario())) {
|
|
return this.sortieFormationRoleCopy(role, hasBattle).label;
|
|
}
|
|
|
|
private sortieRecommendationHint(recommendation: SortieRecommendation, unit?: UnitData, maxReasonLength = 20) {
|
|
const role = recommendation.role ?? (unit ? this.defaultSortieFormationRole(unit) : undefined);
|
|
const roleLabel = role ? this.sortieFormationRoleLabel(role) : '추천';
|
|
return `${roleLabel} · ${this.compactText(this.conciseSortieText(recommendation.reason), maxReasonLength)}`;
|
|
}
|
|
|
|
private conciseSortieText(value: string) {
|
|
const firstSentence = value.replace(/\s+/g, ' ').trim().split(/[.。]/)[0] ?? value;
|
|
return firstSentence
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/입니다$/, '')
|
|
.replace(/합니다$/, '함')
|
|
.replace(/맡습니다$/, '맡음')
|
|
.replace(/좋습니다$/, '좋음')
|
|
.replace(/수 있습니다$/, '가능')
|
|
.trim();
|
|
}
|
|
|
|
private renderSortieStatChip(iconKey: BattleUiIconKey, label: string, value: number, x: number, y: number, width: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, 17, 0x0a1017, 0.84));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, 0x53606c, 0.34);
|
|
this.renderSortieBattleUiIcon(iconKey, x + 9, y + 8, 14, depth + 1, 0.9);
|
|
this.trackSortie(this.add.text(x + 18, y + 3, `${label}${value}`, this.textStyle(9, '#f2e3bf', true))).setDepth(depth + 1);
|
|
}
|
|
|
|
private renderSortieEquipmentIcons(unit: UnitData, x: number, y: number, selected: boolean, depth: number, frameSize = 18, iconSize = 14, spacing = 23) {
|
|
equipmentSlots.forEach((slot, index) => {
|
|
const state = unit.equipment[slot];
|
|
const item = getItem(state.itemId);
|
|
const iconFrame = this.trackSortie(this.add.rectangle(x + index * spacing, y, frameSize, frameSize, selected && item.rank === 'treasure' ? 0x2c2630 : 0x0a1017, 0.92));
|
|
iconFrame.setStrokeStyle(1, item.rank === 'treasure' ? palette.gold : 0x53606c, selected ? 0.62 : 0.34);
|
|
iconFrame.setDepth(depth);
|
|
const icon = this.trackSortie(this.add.image(x + index * spacing, y, `item-${item.id}`));
|
|
icon.setDisplaySize(iconSize, iconSize);
|
|
icon.setAlpha(selected ? 1 : 0.52);
|
|
icon.setDepth(depth + 1);
|
|
});
|
|
}
|
|
|
|
private renderSortiePanelButton(
|
|
label: string,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
enabled: boolean,
|
|
active: boolean,
|
|
action: () => void,
|
|
depth: number
|
|
) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, active ? 0x263a2d : 0x1a2630, enabled ? 0.96 : 0.58));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, active ? palette.green : enabled ? palette.gold : 0x53606c, active ? 0.72 : enabled ? 0.62 : 0.32);
|
|
if (enabled) {
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerover', () => bg.setFillStyle(active ? 0x314738 : 0x283947, 0.98));
|
|
bg.on('pointerout', () => bg.setFillStyle(active ? 0x263a2d : 0x1a2630, enabled ? 0.96 : 0.58));
|
|
bg.on('pointerdown', action);
|
|
}
|
|
const text = this.trackSortie(this.add.text(x + width / 2, y + 5, label, this.textStyle(10, enabled ? '#f2e3bf' : '#87919c', true)));
|
|
text.setOrigin(0.5, 0);
|
|
text.setDepth(depth + 1);
|
|
if (enabled) {
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', action);
|
|
}
|
|
}
|
|
|
|
private renderSortieSupplyAssignment(unit: UnitData, x: number, y: number, width: number, depth: number) {
|
|
const selected = this.isSortieSelected(unit.id);
|
|
this.trackSortie(this.add.text(x, y + 3, '보급', this.textStyle(10, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth);
|
|
const buttonWidth = Math.floor((width - 42) / campSupplies.length);
|
|
campSupplies.forEach((supply, index) => {
|
|
const buttonX = x + 42 + index * buttonWidth;
|
|
const assigned = this.sortieAssignedSupplyCount(unit.id, supply.usableId);
|
|
const availableForUnit = this.sortieAvailableSupplyForUnit(unit.id, supply);
|
|
const enabled = selected && (assigned > 0 || availableForUnit > 0);
|
|
const bg = this.trackSortie(this.add.rectangle(buttonX, y, buttonWidth - 5, 20, assigned > 0 ? 0x263a2d : 0x0e151d, enabled ? 0.94 : 0.58));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, assigned > 0 ? palette.green : enabled ? palette.gold : 0x53606c, assigned > 0 ? 0.78 : enabled ? 0.5 : 0.28);
|
|
this.trackSortie(
|
|
this.add.text(
|
|
buttonX + (buttonWidth - 5) / 2,
|
|
y + 5,
|
|
`${supply.title} ${assigned}`,
|
|
this.textStyle(9, assigned > 0 ? '#a8ffd0' : enabled ? '#c8d2dd' : '#77818c', true)
|
|
)
|
|
).setOrigin(0.5, 0).setDepth(depth + 1);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerdown', () => this.toggleSortieSupplyAssignment(unit.id, supply.usableId));
|
|
});
|
|
}
|
|
|
|
private itemIconKeyForSortie(slot: EquipmentSlot): BattleUiIconKey {
|
|
if (slot === 'armor') {
|
|
return 'armor';
|
|
}
|
|
if (slot === 'accessory') {
|
|
return 'accessory';
|
|
}
|
|
return 'sword';
|
|
}
|
|
|
|
private sortieUnitTacticalSummary(unit: UnitData): SortieUnitTacticalSummary {
|
|
return {
|
|
statLine: `무 ${unit.stats.might} 지 ${unit.stats.intelligence} 통 ${unit.stats.leadership} 민 ${unit.stats.agility} 운 ${unit.stats.luck}`,
|
|
equipmentLine: this.sortieEquipmentLine(unit),
|
|
bondLine: this.sortieBondLine(unit),
|
|
terrainLine: this.sortieTerrainLine(unit)
|
|
};
|
|
}
|
|
|
|
private sortieEquipmentLine(unit: UnitData) {
|
|
return equipmentSlots
|
|
.map((slot) => {
|
|
const state = unit.equipment[slot];
|
|
const item = getItem(state.itemId);
|
|
return `${item.name} Lv${state.level}`;
|
|
})
|
|
.join(' · ');
|
|
}
|
|
|
|
private sortieBondLine(unit: UnitData) {
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
const activeBonds = this.currentBonds()
|
|
.filter((bond) => bond.unitIds.includes(unit.id))
|
|
.map((bond) => {
|
|
const partnerId = bond.unitIds.find((unitId) => unitId !== unit.id) ?? bond.unitIds[0];
|
|
return {
|
|
bond,
|
|
partnerId,
|
|
partnerName: this.unitName(partnerId),
|
|
partnerSelected: selected.has(partnerId)
|
|
};
|
|
})
|
|
.sort((a, b) => Number(b.partnerSelected) - Number(a.partnerSelected) || b.bond.level - a.bond.level || b.bond.exp - a.bond.exp);
|
|
|
|
const deployedBond = activeBonds.find((entry) => entry.partnerSelected);
|
|
if (deployedBond) {
|
|
return `공명 ${deployedBond.partnerName} Lv${deployedBond.bond.level}`;
|
|
}
|
|
|
|
const strongestBond = activeBonds[0];
|
|
if (strongestBond) {
|
|
return `대기 공명 ${strongestBond.partnerName} Lv${strongestBond.bond.level}`;
|
|
}
|
|
|
|
return '공명 관계 없음';
|
|
}
|
|
|
|
private sortieTerrainLine(unit: UnitData) {
|
|
const scenario = this.nextSortieScenario();
|
|
if (!scenario) {
|
|
return `지형 의정 · ${getUnitClass(unit.classKey).role}`;
|
|
}
|
|
|
|
const terrainCounts = this.passableTerrainCounts(scenario);
|
|
const unitClass = getUnitClass(unit.classKey);
|
|
const entries = Object.entries(terrainCounts) as [TerrainType, number][];
|
|
const total = entries.reduce((sum, [, count]) => sum + count, 0);
|
|
if (total <= 0) {
|
|
return `${unitClass.role}`;
|
|
}
|
|
|
|
const weightedScore =
|
|
entries.reduce((sum, [terrain, count]) => sum + (unitClass.terrainRatings[terrain] ?? 0) * count, 0) / total;
|
|
const grade = weightedScore >= 110 ? '최적' : weightedScore >= 102 ? '유리' : weightedScore >= 95 ? '보통' : '주의';
|
|
const bestTerrains = entries
|
|
.sort((a, b) => (unitClass.terrainRatings[b[0]] ?? 0) - (unitClass.terrainRatings[a[0]] ?? 0) || b[1] - a[1])
|
|
.slice(0, 2)
|
|
.map(([terrain]) => terrainRules[terrain].label)
|
|
.join('/');
|
|
|
|
return `지형 ${grade}${bestTerrains ? ` · ${bestTerrains}` : ''}`;
|
|
}
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
counts[terrain] = (counts[terrain] ?? 0) + 1;
|
|
});
|
|
});
|
|
this.terrainCountCache.set(scenario.id, counts);
|
|
return counts;
|
|
}
|
|
|
|
private sortieTerrainSummary(scenario: BattleScenarioDefinition) {
|
|
const entries = Object.entries(this.passableTerrainCounts(scenario)) as [TerrainType, number][];
|
|
const topTerrains = entries
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 3)
|
|
.map(([terrain]) => terrainRules[terrain].label);
|
|
return topTerrains.join('/') || '미정';
|
|
}
|
|
|
|
private sortieEnemySummary(scenario: BattleScenarioDefinition) {
|
|
const counts = new Map<string, number>();
|
|
scenario.units
|
|
.filter((unit) => unit.faction === 'enemy')
|
|
.forEach((unit) => counts.set(unit.className, (counts.get(unit.className) ?? 0) + 1));
|
|
return [...counts.entries()]
|
|
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'ko'))
|
|
.slice(0, 3)
|
|
.map(([label, count]) => `${label} ${count}`)
|
|
.join(', ') || '미정';
|
|
}
|
|
|
|
private nextSortieScenario() {
|
|
const nextBattleId = this.currentSortieFlow().nextBattleId;
|
|
return nextBattleId ? getBattleScenario(nextBattleId) : undefined;
|
|
}
|
|
|
|
private nextSortieRule(scenario = this.nextSortieScenario()) {
|
|
if (scenario) {
|
|
const scenarioRule = this.sortieRuleFromScenario(scenario);
|
|
return scenarioRule ?? sortieRulesByBattleId[scenario.id] ?? defaultSortieRule;
|
|
}
|
|
const step = this.campaign?.step;
|
|
const flowStep = this.currentSortieFlow().campaignStep;
|
|
return (step && sortieRulesByCampaignStep[step]) || (flowStep && sortieRulesByCampaignStep[flowStep]) || defaultCampSortieRule;
|
|
}
|
|
|
|
private sortieRuleFromScenario(scenario: BattleScenarioDefinition): SortieRuleDefinition | undefined {
|
|
if (!scenario.sortie) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
maxUnits: scenario.sortie.sortieLimit,
|
|
requiredUnitIds: scenario.sortie.requiredUnits,
|
|
excludedUnitIds: scenario.sortie.excludedUnits,
|
|
recommended: scenario.sortie.recommendedUnits,
|
|
recommendedClasses: scenario.sortie.recommendedClasses,
|
|
deploymentSlots: scenario.sortie.deploymentSlots,
|
|
note: scenario.sortie.note
|
|
};
|
|
}
|
|
|
|
private requiredSortieUnitIdsFor(scenario = this.nextSortieScenario()) {
|
|
return new Set(this.nextSortieRule(scenario).requiredUnitIds ?? defaultRequiredSortieUnitIds);
|
|
}
|
|
|
|
private isRequiredSortieUnit(unitId: string, scenario = this.nextSortieScenario()) {
|
|
return this.requiredSortieUnitIdsFor(scenario).has(unitId);
|
|
}
|
|
|
|
private sortieMaxUnits(scenario = this.nextSortieScenario()) {
|
|
return Math.min(this.nextSortieRule(scenario).maxUnits, this.sortieAllies().length);
|
|
}
|
|
|
|
private sortieRecommendation(unitId: string, scenario = this.nextSortieScenario()) {
|
|
return this.nextSortieRule(scenario).recommended.find((entry) => entry.unitId === unitId);
|
|
}
|
|
|
|
private sortieRecommendedClasses(scenario = this.nextSortieScenario()) {
|
|
return this.nextSortieRule(scenario).recommendedClasses ?? [];
|
|
}
|
|
|
|
private sortieRecommendedClassLine(scenario = this.nextSortieScenario()) {
|
|
const recommendations = this.sortieRecommendedClasses(scenario);
|
|
return recommendations.length > 0 ? recommendations.map((entry) => entry.label).join(' / ') : '균형 편성';
|
|
}
|
|
|
|
private coversRecommendedClasses(units: UnitData[], scenario = this.nextSortieScenario()) {
|
|
const recommendations = this.sortieRecommendedClasses(scenario);
|
|
if (recommendations.length === 0) {
|
|
return true;
|
|
}
|
|
return recommendations.every((entry) => units.some((unit) => entry.classKeys.includes(unit.classKey)));
|
|
}
|
|
|
|
private ensureSortieFocus() {
|
|
const allies = this.sortieAllies();
|
|
if (allies.some((unit) => unit.id === this.sortieFocusedUnitId)) {
|
|
return;
|
|
}
|
|
|
|
const selectedIds = new Set(this.selectedSortieUnitIds);
|
|
const recommended = this.nextSortieRule()
|
|
.recommended
|
|
.find((entry) => allies.some((unit) => unit.id === entry.unitId));
|
|
this.sortieFocusedUnitId =
|
|
recommended?.unitId ??
|
|
allies.find((unit) => selectedIds.has(unit.id))?.id ??
|
|
allies[0]?.id ??
|
|
'liu-bei';
|
|
}
|
|
|
|
private sortieFocusedUnit() {
|
|
return this.sortieAllies().find((unit) => unit.id === this.sortieFocusedUnitId) ?? this.selectedSortieUnits()[0] ?? this.sortieAllies()[0];
|
|
}
|
|
|
|
private sortieFocusedUnitSummary(): SortieFocusedUnitSummary | null {
|
|
const unit = this.sortieFocusedUnit();
|
|
if (!unit) {
|
|
return null;
|
|
}
|
|
const summary = this.sortieUnitTacticalSummary(unit);
|
|
const recommendation = this.sortieRecommendation(unit.id);
|
|
const scenario = this.nextSortieScenario();
|
|
const terrainScore = this.sortieTerrainScore(unit, scenario);
|
|
const assignedReserveTrainingFocus = this.campaign?.reserveTrainingAssignments[unit.id];
|
|
const reserveTrainingFocus = this.reserveTrainingFocusDefinitionForUnit(unit.id);
|
|
return {
|
|
id: unit.id,
|
|
name: unit.name,
|
|
selected: this.isSortieSelected(unit.id),
|
|
required: this.isRequiredSortieUnit(unit.id),
|
|
recommended: Boolean(recommendation),
|
|
recommendationReason: recommendation?.reason ?? null,
|
|
className: unit.className,
|
|
classRole: getUnitClass(unit.classKey).role,
|
|
formationRole: this.sortieFormationRole(unit),
|
|
reserveTrainingAssigned: Boolean(assignedReserveTrainingFocus),
|
|
reserveTrainingFocusId: reserveTrainingFocus.id,
|
|
reserveTrainingFocusLabel: reserveTrainingFocus.label,
|
|
terrainScore,
|
|
terrainGrade: this.sortieTerrainGrade(terrainScore),
|
|
statLine: summary.statLine,
|
|
equipmentLine: summary.equipmentLine,
|
|
bondLine: summary.bondLine,
|
|
terrainLine: summary.terrainLine,
|
|
equipment: equipmentSlots.map((slot) => {
|
|
const state = unit.equipment[slot];
|
|
const item = getItem(state.itemId);
|
|
return {
|
|
slot,
|
|
label: equipmentSlotLabels[slot],
|
|
itemName: item.name,
|
|
level: state.level,
|
|
exp: state.exp,
|
|
next: equipmentExpToNext(state.level),
|
|
rank: item.rank,
|
|
bonusText: this.itemBonusText(item)
|
|
};
|
|
})
|
|
};
|
|
}
|
|
|
|
private renderSortieDeploymentMap(x: number, y: number, width: number, height: number, depth: number) {
|
|
const scenario = this.nextSortieScenario();
|
|
const frame = this.trackSortie(this.add.rectangle(x, y, width, height, 0x070b10, 0.9));
|
|
frame.setOrigin(0);
|
|
frame.setDepth(depth);
|
|
frame.setStrokeStyle(1, palette.blue, 0.48);
|
|
|
|
if (!scenario) {
|
|
this.trackSortie(this.add.text(x + 14, y + 20, '군영 동행 확인', this.textStyle(12, '#d8b15f', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 14, y + 38, '전투 배치 없이 의정에 함께할 무장을 정리합니다.', this.textStyle(11, '#9fb0bf'))).setDepth(depth + 1);
|
|
return;
|
|
}
|
|
|
|
const mapWidth = scenario.map.width;
|
|
const mapHeight = scenario.map.height;
|
|
const cell = Math.min((width - 16) / mapWidth, (height - 16) / mapHeight);
|
|
const originX = x + Math.floor((width - mapWidth * cell) / 2);
|
|
const originY = y + Math.floor((height - mapHeight * cell) / 2);
|
|
const graphics = this.trackSortie(this.add.graphics());
|
|
graphics.setDepth(depth + 1);
|
|
|
|
scenario.map.terrain.forEach((row, rowIndex) => {
|
|
row.forEach((terrain, columnIndex) => {
|
|
const rule = terrainRules[terrain];
|
|
graphics.fillStyle(rule.color, terrain === 'river' || terrain === 'cliff' ? 0.82 : 0.58);
|
|
graphics.fillRect(originX + columnIndex * cell, originY + rowIndex * cell, Math.ceil(cell), Math.ceil(cell));
|
|
});
|
|
});
|
|
|
|
const deploymentPlan = this.sortieDeploymentPlanForScenario(scenario);
|
|
scenario.units.forEach((unit) => {
|
|
if (unit.faction !== 'enemy') {
|
|
return;
|
|
}
|
|
const dotX = originX + (unit.x + 0.5) * cell;
|
|
const dotY = originY + (unit.y + 0.5) * cell;
|
|
const dot = this.trackSortie(this.add.circle(dotX, dotY, 2.2, 0xd35f4a, 0.72));
|
|
dot.setDepth(depth + 2);
|
|
});
|
|
|
|
deploymentPlan.forEach((entry) => {
|
|
const dotX = originX + (entry.x + 0.5) * cell;
|
|
const dotY = originY + (entry.y + 0.5) * cell;
|
|
const required = entry.unitId ? this.isRequiredSortieUnit(entry.unitId, scenario) : false;
|
|
const dot = this.trackSortie(this.add.circle(dotX, dotY, required ? 4.2 : 3.7, this.sortieRoleDotColor(entry.role), 0.96));
|
|
dot.setDepth(depth + 3);
|
|
dot.setStrokeStyle(required ? 1.6 : 1, required ? palette.gold : 0x05070a, required ? 0.92 : 0.82);
|
|
});
|
|
|
|
const legend = [
|
|
{ label: '전열', color: this.sortieRoleDotColor('front') },
|
|
{ label: '돌파', color: this.sortieRoleDotColor('flank') },
|
|
{ label: '후원', color: this.sortieRoleDotColor('support') },
|
|
{ label: '적', color: 0xd35f4a }
|
|
];
|
|
let legendX = x + 10;
|
|
legend.forEach((entry) => {
|
|
const dot = this.trackSortie(this.add.circle(legendX + 4, y + height - 10, 3.2, entry.color, 0.96));
|
|
dot.setDepth(depth + 2);
|
|
this.trackSortie(this.add.text(legendX + 10, y + height - 16, entry.label, this.textStyle(9, '#c8d2dd', true))).setDepth(depth + 2);
|
|
legendX += 45;
|
|
});
|
|
}
|
|
|
|
private sortieDeploymentPlanForScenario(scenario: BattleScenarioDefinition) {
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
const deployedUnits = scenario.units.filter((unit) => unit.faction === 'ally' && selected.has(unit.id));
|
|
return createSortieDeploymentPlan(
|
|
deployedUnits,
|
|
scenario.units,
|
|
this.selectedSortieUnitIds,
|
|
this.sortieFormationAssignments,
|
|
(unit) => this.sortieRecommendation(unit.id, scenario)?.role ?? this.defaultSortieFormationRole(unit),
|
|
this.nextSortieRule(scenario).deploymentSlots
|
|
);
|
|
}
|
|
|
|
private sortieRoleDotColor(role: SortieFormationRole) {
|
|
if (role === 'front') {
|
|
return palette.green;
|
|
}
|
|
if (role === 'flank') {
|
|
return palette.gold;
|
|
}
|
|
if (role === 'support') {
|
|
return palette.blue;
|
|
}
|
|
return 0x9fb0bf;
|
|
}
|
|
|
|
private sortiePlanSummary(): SortiePlanSummary {
|
|
const allAllies = this.sortieAllies();
|
|
const selectedUnits = this.selectedSortieUnits();
|
|
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
|
|
const scenario = this.nextSortieScenario();
|
|
const rule = this.nextSortieRule(scenario);
|
|
const flow = this.currentSortieFlow();
|
|
const reserveFocus = this.reserveTrainingFocusDefinition();
|
|
const availableIds = new Set(allAllies.map((unit) => unit.id));
|
|
const recommended = rule.recommended.filter((entry) => availableIds.has(entry.unitId));
|
|
const missingRecommended = recommended.filter((entry) => !selectedIds.has(entry.unitId));
|
|
const missingClassRecommendations = (rule.recommendedClasses ?? []).filter(
|
|
(entry) => !selectedUnits.some((unit) => entry.classKeys.includes(unit.classKey))
|
|
);
|
|
const recruitedUnits = allAllies.filter((unit) => !foundingSortieUnitIds.has(unit.id));
|
|
const selectedRecruitedCount = recruitedUnits.filter((unit) => selectedIds.has(unit.id)).length;
|
|
const reserveUnits = allAllies.filter((unit) => !selectedIds.has(unit.id));
|
|
const reserveNames = reserveUnits.map((unit) => unit.name).join(', ');
|
|
const individualReserveTrainingCount = reserveUnits.filter((unit) => this.campaign?.reserveTrainingAssignments[unit.id]).length;
|
|
const terrainScores = selectedUnits.map((unit) => this.sortieTerrainScore(unit, scenario));
|
|
const terrainScore = terrainScores.length
|
|
? Math.round(terrainScores.reduce((sum, score) => sum + score, 0) / terrainScores.length)
|
|
: 0;
|
|
const terrainGrade = this.sortieTerrainGrade(terrainScore);
|
|
const activeBondCount = this.currentBonds().filter((bond) => bond.unitIds.every((unitId) => selectedIds.has(unitId))).length;
|
|
const recommendedSelectedCount = recommended.filter((entry) => selectedIds.has(entry.unitId)).length;
|
|
const roleCounts = selectedUnits.reduce(
|
|
(counts, unit) => {
|
|
counts[this.sortieFormationRole(unit)] += 1;
|
|
return counts;
|
|
},
|
|
{ front: 0, flank: 0, support: 0, reserve: 0 } as Record<SortieFormationRole, number>
|
|
);
|
|
const hasBattle = Boolean(scenario);
|
|
const formationSlots = sortieFormationSlotDefinitions.map((slot) => ({
|
|
...this.sortieFormationRoleCopy(slot.role, hasBattle),
|
|
unitNames: selectedUnits.filter((unit) => this.sortieFormationRole(unit) === slot.role).map((unit) => unit.name)
|
|
}));
|
|
const warnings: string[] = [];
|
|
const maxCount = this.sortieMaxUnits(scenario);
|
|
|
|
if (selectedUnits.length < maxCount) {
|
|
warnings.push(`빈 슬롯 ${maxCount - selectedUnits.length}개가 남아 있습니다.`);
|
|
}
|
|
if (missingRecommended.length > 0) {
|
|
warnings.push(`추천 무장 ${missingRecommended.map((entry) => this.unitName(entry.unitId)).join(', ')} 대기 중입니다.`);
|
|
}
|
|
if (missingClassRecommendations.length > 0) {
|
|
warnings.push(`추천 병종 ${missingClassRecommendations.map((entry) => entry.label).join(', ')} 부족.`);
|
|
}
|
|
if (hasBattle) {
|
|
if (roleCounts.front <= 0) {
|
|
warnings.push('전열 담당이 없어 길목을 버티기 어렵습니다.');
|
|
}
|
|
if (roleCounts.flank <= 0) {
|
|
warnings.push('돌파 담당이 없어 추격과 측면 압박이 느릴 수 있습니다.');
|
|
}
|
|
if (roleCounts.support <= 0) {
|
|
warnings.push('후원 담당이 없어 회복, 보급, 원거리 견제가 약합니다.');
|
|
}
|
|
} else {
|
|
if (roleCounts.front <= 0) {
|
|
warnings.push('대표 동행이 없어 의정의 중심이 약합니다.');
|
|
}
|
|
if (roleCounts.flank <= 0) {
|
|
warnings.push('현장 증언을 맡을 동행이 부족합니다.');
|
|
}
|
|
if (roleCounts.support <= 0) {
|
|
warnings.push('보좌 기록을 맡을 동행이 부족합니다.');
|
|
}
|
|
}
|
|
if (activeBondCount <= 0 && selectedUnits.length > 1) {
|
|
warnings.push('함께하는 공명 조합이 없습니다.');
|
|
}
|
|
if (hasBattle && terrainScore > 0 && terrainScore < 98) {
|
|
warnings.push('선택 무장의 평균 지형 적성이 낮습니다.');
|
|
}
|
|
const roleLine = hasBattle
|
|
? `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support} · 예비 ${roleCounts.reserve}`
|
|
: `대표 ${roleCounts.front} · 현장 ${roleCounts.flank} · 보좌 ${roleCounts.support} · 기록 ${roleCounts.reserve}`;
|
|
|
|
return {
|
|
selectedCount: selectedUnits.length,
|
|
maxCount,
|
|
terrainScore,
|
|
terrainGrade,
|
|
activeBondCount,
|
|
recruitedCount: recruitedUnits.length,
|
|
selectedRecruitedCount,
|
|
reserveCount: reserveUnits.length,
|
|
recommendedSelectedCount,
|
|
recommendedTotal: recommended.length,
|
|
deploymentLine: roleLine,
|
|
recommendationLine: recommended.length > 0
|
|
? `추천 무장 ${recommendedSelectedCount}/${recommended.length} · 병종 ${this.sortieRecommendedClassLine(scenario)}`
|
|
: `추천 병종 ${this.sortieRecommendedClassLine(scenario)}`,
|
|
recruitedLine:
|
|
recruitedUnits.length > 0
|
|
? `합류 무장 ${selectedRecruitedCount}/${recruitedUnits.length} · 대기 ${reserveUnits.length}`
|
|
: '합류 무장 없음',
|
|
reserveLine: reserveUnits.length > 0 ? `대기 무장: ${reserveNames}` : scenario ? '전원 출전 중입니다.' : '전원 동행 중입니다.',
|
|
reserveTrainingLine:
|
|
reserveUnits.length > 0
|
|
? `대기 훈련 ${reserveFocus.label}${individualReserveTrainingCount > 0 ? ` · 개별 ${individualReserveTrainingCount}명` : ''}: 경험 +${reserveFocus.expGained}, 장비 +${reserveFocus.equipmentExpGained}${reserveFocus.bondExpGained ? `, 공명 +${reserveFocus.bondExpGained}` : ''}`
|
|
: '대기 훈련 대상 없음',
|
|
reserveTrainingPreviewLine: reserveUnits.length > 0
|
|
? this.reserveTrainingPreviewLine(reserveUnits, reserveFocus)
|
|
: '대기 훈련 대상 없음',
|
|
reserveTrainingFocusId: reserveFocus.id,
|
|
reserveTrainingFocusLabel: reserveFocus.label,
|
|
reserveTrainingExpPreview: reserveFocus.expGained,
|
|
reserveTrainingEquipmentPreview: reserveFocus.equipmentExpGained,
|
|
reserveTrainingBondPreview: reserveFocus.bondExpGained,
|
|
objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : `${flow.title} · 군영 의정`,
|
|
formationSlots,
|
|
warningLine: warnings[0] ?? (hasBattle ? '배치 균형 양호' : '동행 균형 양호'),
|
|
warnings
|
|
};
|
|
}
|
|
|
|
private sortieFormationRole(unit: UnitData, scenario = this.nextSortieScenario()): SortieFormationRole {
|
|
return this.sortieFormationAssignments[unit.id] ?? this.sortieRecommendation(unit.id, scenario)?.role ?? this.defaultSortieFormationRole(unit);
|
|
}
|
|
|
|
private defaultSortieFormationRole(unit: UnitData): SortieFormationRole {
|
|
if (unit.classKey === 'cavalry') {
|
|
return 'flank';
|
|
}
|
|
if (unit.classKey === 'archer' || unit.classKey === 'strategist' || unit.classKey === 'quartermaster') {
|
|
return 'support';
|
|
}
|
|
if (unit.classKey === 'lord' || unit.classKey === 'infantry' || unit.classKey === 'spearman') {
|
|
return 'front';
|
|
}
|
|
return 'reserve';
|
|
}
|
|
|
|
private assignFocusedSortieRole(role: SortieFormationRole) {
|
|
const unit = this.sortieFocusedUnit();
|
|
if (!unit) {
|
|
return;
|
|
}
|
|
|
|
this.sortieFocusedUnitId = unit.id;
|
|
if (!this.isSortieSelected(unit.id)) {
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
this.showCampNotice(`${unit.name}을 먼저 ${hasBattle ? '출전 편성' : '동행 명단'}에 포함해야 역할을 바꿀 수 있습니다.`);
|
|
return;
|
|
}
|
|
|
|
this.sortieFormationAssignments = {
|
|
...this.sortieFormationAssignments,
|
|
[unit.id]: role
|
|
};
|
|
const roleLabel = this.sortieFormationRoleLabel(role);
|
|
this.sortiePlanFeedback = `${unit.name} 역할 변경 · ${roleLabel}`;
|
|
this.persistSortieSelection();
|
|
soundDirector.playSelect();
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private applyRecommendedSortiePlan() {
|
|
const scenario = this.nextSortieScenario();
|
|
const hasBattle = Boolean(scenario);
|
|
if (!this.canApplyRecommendedSortiePlan(scenario)) {
|
|
this.showCampNotice(hasBattle ? '추천 편성 후보가 없습니다.' : '추천 동행 후보가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const rule = this.nextSortieRule(scenario);
|
|
const allies = this.sortieAllies();
|
|
const availableById = new Map(allies.map((unit) => [unit.id, unit]));
|
|
const orderedIds: string[] = [];
|
|
const addUnit = (unitId: string) => {
|
|
if (availableById.has(unitId) && !orderedIds.includes(unitId)) {
|
|
orderedIds.push(unitId);
|
|
}
|
|
};
|
|
|
|
(rule.requiredUnitIds ?? defaultRequiredSortieUnitIds).forEach(addUnit);
|
|
rule.recommended.forEach((entry) => addUnit(entry.unitId));
|
|
(rule.recommendedClasses ?? []).forEach((entry) => {
|
|
allies
|
|
.filter((unit) => entry.classKeys.includes(unit.classKey))
|
|
.forEach((unit) => addUnit(unit.id));
|
|
});
|
|
allies.forEach((unit) => addUnit(unit.id));
|
|
|
|
const maxUnits = this.sortieMaxUnits(scenario);
|
|
const nextSelectedIds = orderedIds.slice(0, maxUnits);
|
|
const recommendationById = new Map(rule.recommended.map((entry) => [entry.unitId, entry]));
|
|
const nextAssignments: SortieFormationAssignments = {};
|
|
nextSelectedIds.forEach((unitId) => {
|
|
const unit = availableById.get(unitId);
|
|
if (!unit) {
|
|
return;
|
|
}
|
|
nextAssignments[unitId] = recommendationById.get(unitId)?.role ?? this.defaultSortieFormationRole(unit);
|
|
});
|
|
|
|
this.selectedSortieUnitIds = nextSelectedIds;
|
|
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(nextAssignments);
|
|
this.sortieFocusedUnitId = nextSelectedIds[0] ?? this.sortieFocusedUnitId;
|
|
this.sortiePlanFeedback = this.recommendedSortieFeedback(nextSelectedIds, nextAssignments, hasBattle);
|
|
this.persistSortieSelection();
|
|
soundDirector.playSelect();
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private canApplyRecommendedSortiePlan(scenario = this.nextSortieScenario()) {
|
|
const rule = this.nextSortieRule(scenario);
|
|
const requiredCount = (rule.requiredUnitIds ?? defaultRequiredSortieUnitIds).length;
|
|
const recommendedCount = rule.recommended.length + (rule.recommendedClasses?.length ?? 0);
|
|
return this.sortieAllies().length > 0 && this.sortieMaxUnits(scenario) > 0 && requiredCount + recommendedCount > 0;
|
|
}
|
|
|
|
private recommendedSortieFeedback(unitIds: string[], assignments: SortieFormationAssignments, hasBattle = Boolean(this.nextSortieScenario())) {
|
|
const roleCounts = unitIds.reduce(
|
|
(counts, unitId) => {
|
|
counts[assignments[unitId] ?? 'reserve'] += 1;
|
|
return counts;
|
|
},
|
|
{ front: 0, flank: 0, support: 0, reserve: 0 } as Record<SortieFormationRole, number>
|
|
);
|
|
const names = unitIds.slice(0, 3).map((id) => this.unitName(id)).join(', ');
|
|
const extra = unitIds.length > 3 ? ` 외 ${unitIds.length - 3}명` : '';
|
|
const roleLine = hasBattle
|
|
? `전열 ${roleCounts.front} / 돌파 ${roleCounts.flank} / 후원 ${roleCounts.support}`
|
|
: `대표 ${roleCounts.front} / 현장 ${roleCounts.flank} / 보좌 ${roleCounts.support}`;
|
|
return `${hasBattle ? '추천 편성' : '추천 동행'} 적용 · ${names}${extra} · ${roleLine}`;
|
|
}
|
|
|
|
private sortieTerrainScore(unit: UnitData, scenario: BattleScenarioDefinition | undefined) {
|
|
if (!scenario) {
|
|
return 100;
|
|
}
|
|
const terrainCounts = this.passableTerrainCounts(scenario);
|
|
const entries = Object.entries(terrainCounts) as [TerrainType, number][];
|
|
const total = entries.reduce((sum, [, count]) => sum + count, 0);
|
|
if (total <= 0) {
|
|
return 100;
|
|
}
|
|
const unitClass = getUnitClass(unit.classKey);
|
|
return Math.round(entries.reduce((sum, [terrain, count]) => sum + (unitClass.terrainRatings[terrain] ?? 0) * count, 0) / total);
|
|
}
|
|
|
|
private sortieTerrainGrade(score: number) {
|
|
if (score >= 110) {
|
|
return '최적';
|
|
}
|
|
if (score >= 102) {
|
|
return '유리';
|
|
}
|
|
if (score >= 95) {
|
|
return '보통';
|
|
}
|
|
return score > 0 ? '주의' : '미정';
|
|
}
|
|
|
|
private sortieRecoveryTargets(thresholdRatio = 0.7) {
|
|
return this.currentUnits()
|
|
.filter((unit) => unit.faction === 'ally' && unit.hp > 0 && unit.maxHp > 0 && unit.hp / unit.maxHp < thresholdRatio)
|
|
.sort((a, b) => a.hp / a.maxHp - b.hp / b.maxHp || a.name.localeCompare(b.name));
|
|
}
|
|
|
|
private sortieRecoveryHint() {
|
|
const targets = this.sortieRecoveryTargets();
|
|
if (targets.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
const names = targets.slice(0, 2).map((unit) => `${unit.name} ${unit.hp}/${unit.maxHp}`).join(', ');
|
|
const extra = targets.length > 2 ? ` 외 ${targets.length - 2}명` : '';
|
|
const supplyCount = campSupplies.reduce((total, supply) => total + this.inventoryAmount(supply.label), 0);
|
|
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
|
|
const supplyText = supplyCount > 0 ? `보급 ${assignedSupplyCount}/${supplyCount}` : '보급 없음';
|
|
return `정비: ${names}${extra} 회복 권장 · ${supplyText}`;
|
|
}
|
|
|
|
private renderSortieRewardHint(x: number, y: number, width: number, height: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.gold, 0.4);
|
|
const availableDialogues = this.availableCampDialogues();
|
|
const completedDialogues = this.completedAvailableDialogues().length;
|
|
const availableVisits = this.availableCampVisits();
|
|
const completedVisits = this.completedAvailableVisits().length;
|
|
const selectedUnits = this.selectedSortieUnits();
|
|
const selectedPreview = selectedUnits.slice(0, 3).map((unit) => unit.name).join(', ');
|
|
const selectedSummary = selectedUnits.length > 0 ? `${selectedUnits.length}명 (${selectedPreview}${selectedUnits.length > 3 ? ' 외' : ''})` : '없음';
|
|
const supplyCount = campSupplies.reduce((total, supply) => total + this.inventoryAmount(supply.label), 0);
|
|
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
|
|
const equipmentCount = this.equipmentInventoryEntries().reduce((total, entry) => total + entry.amount, 0);
|
|
const recordCount = this.nonEquipmentInventoryLabels().length;
|
|
const inventorySummary = `보급 ${assignedSupplyCount}/${supplyCount} · 장비 ${equipmentCount} · 기록 ${recordCount}`;
|
|
const reward = this.currentSortieFlow().rewardHint;
|
|
const sortieNote = this.nextSortieRule().note;
|
|
const recoveryHint = this.sortieRecoveryHint();
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const readyLine = [
|
|
`대화 ${completedDialogues}/${availableDialogues.length}`,
|
|
`방문 ${completedVisits}/${availableVisits.length}`,
|
|
`${hasBattle ? '출전' : '동행'} ${selectedSummary}`,
|
|
`보유 ${inventorySummary}`
|
|
].join(' · ');
|
|
const rewardLine = recoveryHint ? `${reward} · ${recoveryHint}` : `${reward} · ${readyLine}`;
|
|
this.trackSortie(this.add.text(x + 16, y + 8, '편성 메모', this.textStyle(11, '#d8b15f', true))).setDepth(depth + 1);
|
|
this.trackSortie(this.add.text(x + 86, y + 8, this.compactText(this.conciseSortieText(sortieNote), 30), this.textStyle(12, '#f2e3bf', true))).setDepth(depth + 1);
|
|
this.trackSortie(
|
|
this.add.text(
|
|
x + 16,
|
|
y + 30,
|
|
this.compactText(rewardLine, 48),
|
|
this.textStyle(12, '#d4dce6')
|
|
)
|
|
).setDepth(depth + 1);
|
|
}
|
|
|
|
private renderSortieReserveTraining(x: number, y: number, width: number, height: number, depth: number) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, palette.blue, 0.4);
|
|
|
|
const focusedReserveUnit = this.focusedReserveTrainingUnit();
|
|
const focus = focusedReserveUnit ? this.reserveTrainingFocusDefinitionForUnit(focusedReserveUnit.id) : this.reserveTrainingFocusDefinition();
|
|
const reserveUnits = this.reserveTrainingPreviewUnits();
|
|
const reserveCount = reserveUnits.length;
|
|
this.trackSortie(this.add.text(x + 12, y + 7, focusedReserveUnit ? '개별 훈련' : '대기 훈련', this.textStyle(12, '#f2e3bf', true))).setDepth(depth + 1);
|
|
const focusPreview = focusedReserveUnit
|
|
? `${focusedReserveUnit.name} · ${focus.label}${this.campaign?.reserveTrainingAssignments[focusedReserveUnit.id] ? '' : ' 기본'}`
|
|
: reserveCount > 0
|
|
? this.reserveTrainingPreviewLine(reserveUnits, focus)
|
|
: '대기 없음';
|
|
this.trackSortie(
|
|
this.add.text(
|
|
x + width - 12,
|
|
y + 7,
|
|
this.compactText(focusPreview, 34),
|
|
this.textStyle(11, reserveCount > 0 ? '#d8b15f' : '#9fb0bf', true)
|
|
)
|
|
).setOrigin(1, 0).setDepth(depth + 1);
|
|
|
|
const buttonWidth = Math.floor((width - 40) / campaignReserveTrainingFocusDefinitions.length);
|
|
campaignReserveTrainingFocusDefinitions.forEach((option, index) => {
|
|
const buttonX = x + 12 + index * (buttonWidth + 8);
|
|
const selected = option.id === focus.id;
|
|
const button = this.trackSortie(this.add.rectangle(buttonX, y + 28, buttonWidth, 20, selected ? 0x263a2d : 0x0e151d, selected ? 0.98 : 0.86));
|
|
button.setOrigin(0);
|
|
button.setDepth(depth + 1);
|
|
button.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.78 : 0.42);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerdown', () => this.setReserveTrainingFocusOption(option.id));
|
|
|
|
const optionText = `${option.label} ${option.expGained}/${option.equipmentExpGained}${option.bondExpGained ? `/${option.bondExpGained}` : ''}`;
|
|
const label = this.trackSortie(this.add.text(buttonX + buttonWidth / 2, y + 33, this.compactText(optionText, 10), this.textStyle(9, selected ? '#a8ffd0' : '#c8d2dd', true)));
|
|
label.setOrigin(0.5, 0);
|
|
label.setDepth(depth + 2);
|
|
label.setInteractive({ useHandCursor: true });
|
|
label.on('pointerdown', () => this.setReserveTrainingFocusOption(option.id));
|
|
});
|
|
}
|
|
|
|
private focusedReserveTrainingUnit() {
|
|
const unit = this.sortieFocusedUnit();
|
|
return unit && !this.isSortieSelected(unit.id) ? unit : undefined;
|
|
}
|
|
|
|
private reserveTrainingFocusDefinitionForUnit(unitId: string): CampaignReserveTrainingFocusDefinition {
|
|
const focusId = this.campaign?.reserveTrainingAssignments[unitId];
|
|
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === focusId) ?? this.reserveTrainingFocusDefinition();
|
|
}
|
|
|
|
private reserveTrainingPreviewUnits() {
|
|
const selectedIds = new Set(this.selectedSortieUnits().map((unit) => unit.id));
|
|
return this.sortieAllies().filter((unit) => !selectedIds.has(unit.id));
|
|
}
|
|
|
|
private reserveTrainingPreviewLine(reserveUnits: UnitData[], focus: CampaignReserveTrainingFocusDefinition) {
|
|
const previewNames = reserveUnits.slice(0, 2).map((unit) => unit.name).join(', ');
|
|
const remaining = reserveUnits.length > 2 ? ` 외 ${reserveUnits.length - 2}명` : '';
|
|
const individualCount = reserveUnits.filter((unit) => this.campaign?.reserveTrainingAssignments[unit.id]).length;
|
|
const gainLine = individualCount > 0
|
|
? `개별 ${individualCount}명 · 기본 ${focus.label}`
|
|
: `경험 +${focus.expGained} · 장비 +${focus.equipmentExpGained}${focus.bondExpGained ? ` · 공명 +${focus.bondExpGained}` : ''}`;
|
|
return `${previewNames}${remaining} · ${gainLine}`;
|
|
}
|
|
|
|
private nextSortieBriefing() {
|
|
const flow = this.currentSortieFlow();
|
|
return {
|
|
eyebrow: flow.eyebrow,
|
|
title: flow.title,
|
|
description: flow.description
|
|
};
|
|
}
|
|
|
|
private sortiePrepSubtitle(checklist: SortieChecklistItem[]) {
|
|
const nextRequired = checklist.find((item) => !item.complete && item.priority === 'required');
|
|
const nextRecommended = checklist.find((item) => !item.complete);
|
|
const next = nextRequired ?? nextRecommended;
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
if (!next) {
|
|
return hasBattle
|
|
? '정비가 끝났습니다. 전열과 보급을 확인한 뒤 출진하십시오.'
|
|
: '의정 준비가 끝났습니다. 동행과 기록을 확인한 뒤 진행하십시오.';
|
|
}
|
|
|
|
const prefix = next.priority === 'required' ? '필수' : '권장';
|
|
return `${prefix}: ${next.label} - ${next.detail}`;
|
|
}
|
|
|
|
private currentSortieFlow() {
|
|
return getSortieFlow(this.campaign?.latestBattleId, this.campaign?.step);
|
|
}
|
|
|
|
private reserveTrainingFocusDefinition(): CampaignReserveTrainingFocusDefinition {
|
|
const focusId = this.campaign?.reserveTrainingFocus ?? defaultCampaignReserveTrainingFocusId;
|
|
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === focusId) ?? campaignReserveTrainingFocusDefinitions[0];
|
|
}
|
|
|
|
private setReserveTrainingFocus(focusId: CampaignReserveTrainingFocusId) {
|
|
this.campaign = setCampaignReserveTrainingFocus(focusId);
|
|
soundDirector.playSelect();
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private setReserveTrainingFocusOption(focusId: CampaignReserveTrainingFocusId) {
|
|
const focusedReserveUnit = this.focusedReserveTrainingUnit();
|
|
if (!focusedReserveUnit) {
|
|
this.setReserveTrainingFocus(focusId);
|
|
return;
|
|
}
|
|
|
|
const assignedFocus = this.campaign?.reserveTrainingAssignments[focusedReserveUnit.id];
|
|
const nextFocusId = assignedFocus === focusId ? undefined : focusId;
|
|
this.campaign = setCampaignReserveTrainingAssignment(focusedReserveUnit.id, nextFocusId);
|
|
const focusLabel = nextFocusId
|
|
? campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === nextFocusId)?.label ?? ''
|
|
: this.reserveTrainingFocusDefinition().label;
|
|
this.showCampNotice(`${focusedReserveUnit.name} ${nextFocusId ? '개별 훈련' : '기본 훈련'} · ${focusLabel}`);
|
|
soundDirector.playSelect();
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private sortieChecklist(): SortieChecklistItem[] {
|
|
const units = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
|
const sortieAllies = this.sortieAllies();
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
const requiredUnitIds = [...this.requiredSortieUnitIdsFor()].filter((id) => sortieAllies.some((unit) => unit.id === id));
|
|
const requiredUnits = requiredUnitIds.map((id) => sortieAllies.find((unit) => unit.id === id)).filter(Boolean) as UnitData[];
|
|
const requiredNames = requiredUnits.map((unit) => unit.name);
|
|
const requiredLabelName = requiredNames.length === 0
|
|
? hasBattle ? '필수 무장' : '필수 동행'
|
|
: requiredNames.length === 1
|
|
? requiredNames[0]
|
|
: requiredNames.length === 2
|
|
? `${requiredNames[0]}·${requiredNames[1]}`
|
|
: `${requiredNames[0]} 외 ${requiredNames.length - 1}명`;
|
|
const requiredLabel = `${requiredLabelName} ${hasBattle ? '생존' : '참석'}`;
|
|
const requiredComplete = requiredUnits.length > 0 ? requiredUnits.every((unit) => unit.hp > 0) : true;
|
|
const requiredDetail = requiredUnits.length > 0
|
|
? requiredUnits.map((unit) => `${unit.name} ${unit.hp}/${unit.maxHp}`).join(', ')
|
|
: '필수 무장 없음';
|
|
const injured = units.filter((unit) => unit.hp > 0 && unit.hp < unit.maxHp);
|
|
const recoveryTargets = this.sortieRecoveryTargets();
|
|
const recoveryDetail = recoveryTargets.length > 0
|
|
? `${recoveryTargets.map((unit) => `${unit.name} ${unit.hp}/${unit.maxHp}`).join(', ')} 회복 권장`
|
|
: injured.length > 0
|
|
? `${injured.length}명 경상 · ${hasBattle ? '출전' : '동행'} 가능`
|
|
: '전원 완전한 병력';
|
|
const supplyCount = campSupplies.reduce((total, supply) => total + this.inventoryAmount(supply.label), 0);
|
|
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
|
|
const availableDialogues = this.availableCampDialogues();
|
|
const completedDialogues = this.completedAvailableDialogues().length;
|
|
const availableVisits = this.availableCampVisits();
|
|
const completedVisits = this.completedAvailableVisits().length;
|
|
const selected = this.selectedSortieUnits();
|
|
const recommendedClassLine = this.sortieRecommendedClassLine();
|
|
const recommendedClassComplete = this.coversRecommendedClasses(selected);
|
|
const roleCounts = selected.reduce(
|
|
(counts, unit) => {
|
|
counts[this.sortieFormationRole(unit)] += 1;
|
|
return counts;
|
|
},
|
|
{ front: 0, flank: 0, support: 0, reserve: 0 } as Record<SortieFormationRole, number>
|
|
);
|
|
const rolesReady = selected.length <= 1 || (roleCounts.front > 0 && roleCounts.flank > 0 && roleCounts.support > 0);
|
|
const roleDetail = hasBattle
|
|
? `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support}`
|
|
: `대표 ${roleCounts.front} · 현장 ${roleCounts.flank} · 보좌 ${roleCounts.support}`;
|
|
return [
|
|
{
|
|
label: requiredLabel,
|
|
complete: requiredComplete,
|
|
detail: requiredDetail,
|
|
priority: 'required'
|
|
},
|
|
{
|
|
label: '부상 장수 확인',
|
|
complete: recoveryTargets.length === 0,
|
|
detail: recoveryDetail,
|
|
priority: 'recommended'
|
|
},
|
|
{
|
|
label: hasBattle ? '출전 구성' : '동행 구성',
|
|
complete: requiredUnitIds.every((id) => selected.some((unit) => unit.id === id)) && selected.length > 0,
|
|
detail: selected.length > 0 ? selected.map((unit) => unit.name).join(', ') : hasBattle ? '출전 무장 선택 필요' : '동행 무장 선택 필요',
|
|
priority: 'required'
|
|
},
|
|
{
|
|
label: hasBattle ? '추천 병종' : '추천 편성',
|
|
complete: recommendedClassComplete,
|
|
detail: recommendedClassLine,
|
|
priority: 'recommended'
|
|
},
|
|
{
|
|
label: hasBattle ? '전열 역할' : '의정 역할',
|
|
complete: rolesReady,
|
|
detail: roleDetail,
|
|
priority: 'recommended'
|
|
},
|
|
{
|
|
label: hasBattle ? '전투 보급' : '보급 배정',
|
|
complete: supplyCount === 0 || assignedSupplyCount > 0,
|
|
detail: supplyCount > 0 ? `배정 ${assignedSupplyCount}/${supplyCount}` : '보급 없음',
|
|
priority: 'recommended'
|
|
},
|
|
{
|
|
label: '공명 대화',
|
|
complete: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length,
|
|
detail: `${completedDialogues}/${availableDialogues.length} 완료`,
|
|
priority: 'recommended'
|
|
},
|
|
{
|
|
label: '현지 방문',
|
|
complete: availableVisits.length === 0 || completedVisits > 0,
|
|
detail: availableVisits.length === 0 ? '방문 없음' : `${completedVisits}/${availableVisits.length} 완료`,
|
|
priority: 'recommended'
|
|
},
|
|
{
|
|
label: '장비 상태',
|
|
complete: this.visitedTabs.has('status'),
|
|
detail: this.visitedTabs.has('status') ? '장수 탭 확인됨' : '장수 탭 확인 권장',
|
|
priority: 'recommended'
|
|
}
|
|
];
|
|
}
|
|
|
|
private addSortieButton(label: string, x: number, y: number, width: number, action: () => void, depth: number, primary = false) {
|
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, 36, 0x1a2630, 0.96));
|
|
bg.setDepth(depth);
|
|
bg.setStrokeStyle(1, primary ? palette.gold : palette.blue, 0.78);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
|
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
|
|
bg.on('pointerdown', action);
|
|
|
|
const text = this.trackSortie(this.add.text(x, y, label, this.textStyle(15, '#f2e3bf', true)));
|
|
text.setOrigin(0.5);
|
|
text.setDepth(depth + 1);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', action);
|
|
}
|
|
|
|
private startVictoryStory() {
|
|
const flow = this.currentSortieFlow();
|
|
if (!this.isFinalEpilogueFlow(flow) && !this.ensureSortieSelectionSaved()) {
|
|
const hasBattle = Boolean(flow.nextBattleId);
|
|
const availableIds = new Set(this.sortieAllies().map((unit) => unit.id));
|
|
const requiredNames = [...this.requiredSortieUnitIdsFor()]
|
|
.filter((unitId) => availableIds.has(unitId))
|
|
.map((unitId) => this.unitName(unitId));
|
|
this.showCampNotice(
|
|
requiredNames.length > 0
|
|
? `${hasBattle ? '출전' : '동행'}할 무장을 선택하세요. ${requiredNames.join(', ')}는 반드시 포함되어야 합니다.`
|
|
: `${hasBattle ? '출전' : '동행'}할 무장을 선택하세요.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep) {
|
|
this.hideSortiePrep();
|
|
void startLazyScene(this, 'EndingScene');
|
|
return;
|
|
}
|
|
|
|
if (!flow.nextBattleId || !flow.campaignStep || flow.pages.length === 0) {
|
|
this.hideSortiePrep();
|
|
if (!flow.nextBattleId && flow.campaignStep && flow.pages.length > 0) {
|
|
if (this.campaign?.step === flow.campaignStep) {
|
|
this.showCampNotice(flow.unavailableNotice ?? '현재 군영 정비를 먼저 마치세요. 대화, 보급, 편성을 점검할 수 있습니다.');
|
|
return;
|
|
}
|
|
markCampaignStep(flow.campaignStep);
|
|
this.campaign = getCampaignState();
|
|
void startLazyScene(this, 'StoryScene', {
|
|
pages: flow.pages,
|
|
nextScene: flow.campaignStep === 'ending-complete' ? 'EndingScene' : 'CampScene'
|
|
});
|
|
return;
|
|
}
|
|
if (flow.pages.length > 0) {
|
|
void startLazyScene(this, 'StoryScene', {
|
|
pages: flow.pages,
|
|
nextScene: 'CampScene'
|
|
});
|
|
return;
|
|
}
|
|
this.showCampNotice(flow.unavailableNotice ?? '현재 군영 정비를 먼저 마치세요. 대화, 보급, 편성을 점검할 수 있습니다.');
|
|
return;
|
|
}
|
|
|
|
if (flow.nextBattleId === campBattleIds.seventeenth && this.wolongClueCount() <= 0) {
|
|
this.hideSortiePrep();
|
|
this.activeTab = 'visit';
|
|
this.render();
|
|
this.showCampNotice('와룡의 단서를 먼저 모으십시오. 방문 탭에서 형주 선비들의 말을 들어 보세요.');
|
|
return;
|
|
}
|
|
|
|
markCampaignStep(flow.campaignStep);
|
|
const selectedSortieUnitIds = [...this.selectedSortieUnitIds];
|
|
const sortieFormationAssignments = { ...this.sortieFormationAssignments };
|
|
const sortieItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
|
if (this.skipIntroStoryForSortie) {
|
|
void startLazyScene(this, 'BattleScene', {
|
|
battleId: flow.nextBattleId,
|
|
selectedSortieUnitIds,
|
|
sortieFormationAssignments,
|
|
sortieItemAssignments
|
|
});
|
|
return;
|
|
}
|
|
|
|
void startLazyScene(this, 'StoryScene', {
|
|
pages: flow.pages,
|
|
nextScene: 'BattleScene',
|
|
nextSceneData: { battleId: flow.nextBattleId, selectedSortieUnitIds, sortieFormationAssignments, sortieItemAssignments }
|
|
});
|
|
}
|
|
|
|
private isFinalEpilogueFlow(flow = this.currentSortieFlow()) {
|
|
return flow.campaignStep === 'ending-complete' && !flow.nextBattleId;
|
|
}
|
|
|
|
private hideSortiePrep() {
|
|
this.sortieObjects.forEach((object) => object.destroy());
|
|
this.sortieObjects = [];
|
|
}
|
|
|
|
private trackSortie<T extends Phaser.GameObjects.GameObject>(object: T) {
|
|
this.sortieObjects.push(object);
|
|
return object;
|
|
}
|
|
|
|
private drawSortieBar(x: number, y: number, width: number, height: number, ratio: number, color: number, depth: number) {
|
|
const track = this.trackSortie(this.add.rectangle(x, y, width, height, 0x070b10, 0.86));
|
|
track.setOrigin(0);
|
|
track.setDepth(depth);
|
|
const fill = this.trackSortie(this.add.rectangle(x, y, Math.max(2, width * Phaser.Math.Clamp(ratio, 0, 1)), height, color, 0.96));
|
|
fill.setOrigin(0);
|
|
fill.setDepth(depth + 1);
|
|
}
|
|
|
|
private renderUnitColumn() {
|
|
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
|
const availableHeight = 462;
|
|
const rowGap = allies.length > 1 ? Math.min(132, Math.floor(availableHeight / allies.length)) : 132;
|
|
const cardHeight = Math.min(118, Math.max(48, rowGap - 8));
|
|
const compact = cardHeight < 88;
|
|
this.renderRosterColumnSummary(42, 86, 318, allies);
|
|
allies.forEach((unit, index) => {
|
|
const x = 42;
|
|
const y = 120 + index * rowGap;
|
|
const active = this.selectedUnitId === unit.id;
|
|
const rosterStatus = this.unitRosterStatus(unit);
|
|
const training = this.latestReserveTraining(unit.id);
|
|
const bg = this.track(this.add.rectangle(x, y, 318, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(2, active ? palette.gold : palette.blue, active ? 0.9 : 0.46);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.selectedUnitId = unit.id;
|
|
this.render();
|
|
});
|
|
|
|
const portraitKey = portraitByUnitId[unit.id];
|
|
const portraitTexture = portraitKey ? portraitTextureKey(portraitKey) : undefined;
|
|
const portraitCenterY = y + cardHeight / 2;
|
|
const portraitSize = compact ? 40 : 84;
|
|
if (portraitTexture && this.textures.exists(portraitTexture)) {
|
|
const portrait = this.track(this.add.image(x + 58, portraitCenterY, portraitTexture));
|
|
portrait.setDisplaySize(portraitSize, portraitSize);
|
|
} else {
|
|
this.renderUnitFallbackPortrait(x + 58, portraitCenterY, compact ? 20 : 40, unit);
|
|
}
|
|
|
|
const textX = compact ? x + 102 : x + 116;
|
|
this.track(this.add.text(textX, y + (compact ? 7 : 17), `${unit.name} Lv ${unit.level}`, this.textStyle(compact ? 14 : 20, '#f2e3bf', true)));
|
|
this.track(this.add.text(textX, y + (compact ? 27 : 47), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 10 : 13, '#d4dce6')));
|
|
const expLine = training
|
|
? `경험 ${unit.exp}/100 · ${training.focusLabel ?? '대기'} +${training.expGained}`
|
|
: `경험 ${unit.exp}/100`;
|
|
this.track(this.add.text(textX, y + (compact ? 40 : 72), expLine, this.textStyle(compact ? 10 : 13, training ? '#a8ffd0' : '#d8b15f', true)));
|
|
this.drawBar(textX, y + cardHeight - (compact ? 10 : 17), compact ? 142 : 176, compact ? 5 : 8, unit.exp / 100, training ? palette.green : palette.gold);
|
|
const badgeColor = rosterStatus.tone === 'selected' ? '#a8ffd0' : rosterStatus.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
|
const badge = this.track(this.add.text(x + 302, y + (compact ? 7 : 17), rosterStatus.label, this.textStyle(compact ? 10 : 11, badgeColor, true)));
|
|
badge.setOrigin(1, 0);
|
|
});
|
|
}
|
|
|
|
private renderRosterColumnSummary(x: number, y: number, width: number, allies: UnitData[]) {
|
|
const selected = this.selectedSortieUnits();
|
|
const reserveCount = Math.max(0, allies.length - selected.length);
|
|
const recentTrainingCount = this.latestReserveTrainingAwards().length;
|
|
const selectedIds = new Set(selected.map((unit) => unit.id));
|
|
const reserveTrainingAssignmentCount = allies.filter((unit) => !selectedIds.has(unit.id) && this.campaign?.reserveTrainingAssignments[unit.id]).length;
|
|
const bg = this.track(this.add.rectangle(x, y, width, 26, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.38);
|
|
this.track(this.add.text(x + 12, y + 6, `전군 ${allies.length} · 출전 ${selected.length} · 대기 ${reserveCount}`, this.textStyle(11, '#f2e3bf', true)));
|
|
const trainingText = this.track(
|
|
this.add.text(
|
|
x + width - 12,
|
|
y + 6,
|
|
recentTrainingCount > 0
|
|
? `최근 훈련 ${recentTrainingCount} · ${this.latestReserveTrainingFocusLabel()}`
|
|
: reserveTrainingAssignmentCount > 0
|
|
? `개별 훈련 ${reserveTrainingAssignmentCount}`
|
|
: '훈련 대기',
|
|
this.textStyle(11, recentTrainingCount > 0 || reserveTrainingAssignmentCount > 0 ? '#a8ffd0' : '#9fb0bf', true)
|
|
)
|
|
);
|
|
trainingText.setOrigin(1, 0);
|
|
}
|
|
|
|
private unitRosterStatus(unit: UnitData) {
|
|
const availability = this.sortieUnitAvailability(unit);
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
if (!availability.available) {
|
|
return { label: availability.label, tone: 'disabled' as const };
|
|
}
|
|
const selected = this.isSortieSelected(unit.id);
|
|
const recommendation = this.sortieRecommendation(unit.id);
|
|
if (this.isRequiredSortieUnit(unit.id)) {
|
|
return { label: '필수', tone: 'recommended' as const };
|
|
}
|
|
if (selected) {
|
|
return { label: recommendation ? (hasBattle ? '추천 출전' : '추천 동행') : hasBattle ? '출전' : '동행', tone: 'selected' as const };
|
|
}
|
|
if (recommendation) {
|
|
return { label: '추천 대기', tone: 'recommended' as const };
|
|
}
|
|
if (this.latestReserveTraining(unit.id)) {
|
|
return { label: '훈련', tone: 'selected' as const };
|
|
}
|
|
return { label: '대기', tone: 'reserve' as const };
|
|
}
|
|
|
|
private renderUnitFallbackPortrait(x: number, y: number, radius: number, unit: UnitData) {
|
|
const bg = this.track(this.add.circle(x, y, radius, 0x1f3140, 0.96));
|
|
bg.setStrokeStyle(2, palette.gold, 0.58);
|
|
const initial = this.track(this.add.text(x, y - radius * 0.34, unit.name.slice(0, 1), this.textStyle(Math.max(17, Math.floor(radius * 0.86)), '#f2e3bf', true)));
|
|
initial.setOrigin(0.5);
|
|
const label = this.track(this.add.text(x, y + radius * 0.34, unit.className, this.textStyle(Math.max(9, Math.floor(radius * 0.3)), '#d4dce6', true)));
|
|
label.setOrigin(0.5);
|
|
}
|
|
|
|
private renderReportPanel() {
|
|
const report = this.report;
|
|
if (!report) {
|
|
return;
|
|
}
|
|
|
|
const x = 42;
|
|
const y = 590;
|
|
const bg = this.track(this.add.rectangle(x, y, 1180, 84, 0x101820, 0.86));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.48);
|
|
this.track(this.add.text(x + 18, y + 14, '전투 보고', this.textStyle(18, '#f2e3bf', true)));
|
|
this.track(
|
|
this.add.text(
|
|
x + 18,
|
|
y + 39,
|
|
`MVP ${report.mvp?.name ?? '-'} · 전투 보상 ${report.rewardGold} · 전리품 ${report.itemRewards.join(', ') || '없음'}`,
|
|
this.textStyle(14, '#d4dce6')
|
|
)
|
|
);
|
|
this.track(
|
|
this.add.text(
|
|
x + 18,
|
|
y + 62,
|
|
this.reportCampaignRewardLine(report),
|
|
this.textStyle(12, '#d8b15f', true)
|
|
)
|
|
);
|
|
}
|
|
|
|
private reportCampaignRewardLine(report: FirstBattleReport) {
|
|
const rewards = report.campaignRewards;
|
|
if (!rewards) {
|
|
return '캠페인 보상: 기존 전리품이 군영 장부에 반영되었습니다.';
|
|
}
|
|
|
|
const recruits = rewards.recruits ?? [];
|
|
const equipment = rewards.equipment ?? [];
|
|
const reputation = rewards.reputation ?? [];
|
|
const unlocks = rewards.unlocks ?? [];
|
|
const parts = [
|
|
recruits.length > 0 ? `합류 ${recruits.map((recruit) => recruit.name).join(', ')}` : '',
|
|
equipment.length > 0 ? `장비 ${equipment.join(', ')}` : '',
|
|
reputation.length > 0 ? `명성 ${reputation.join(', ')}` : '',
|
|
unlocks.length > 0 ? `다음 ${unlocks.map((unlock) => unlock.title).join(', ')}` : ''
|
|
].filter(Boolean);
|
|
return parts.length > 0 ? `캠페인 반영: ${parts.join(' · ')}` : '캠페인 보상: 추가 해금 없음';
|
|
}
|
|
|
|
private renderProgressPanel() {
|
|
const x = 394;
|
|
const y = 120;
|
|
const width = 828;
|
|
const height = 444;
|
|
const progress = this.campaignTimelineProgress();
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x101820, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.56);
|
|
|
|
this.track(this.add.text(x + 24, y + 22, '군영 연표', this.textStyle(24, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 24, y + 56, '유비군의 장기 서사와 현재 위치를 확인합니다.', this.textStyle(14, '#d4dce6')));
|
|
|
|
const statY = y + 82;
|
|
this.renderProgressStatCard('완료 전장', `${progress.completedKnown}/${progress.totalKnown}`, x + 24, statY, 176);
|
|
this.renderProgressStatCard('현재 장', progress.activeChapter.title, x + 212, statY, 198);
|
|
this.renderProgressStatCard('최근 전장', progress.latestBattleTitle, x + 422, statY, 176);
|
|
this.renderProgressStatCard('다음 전장', progress.nextBattleTitle, x + 610, statY, 190);
|
|
|
|
const completedIds = this.completedBattleIds();
|
|
const listX = x + 24;
|
|
const listY = y + 142;
|
|
this.track(this.add.text(listX, listY, '큰 흐름', this.textStyle(17, '#f2e3bf', true)));
|
|
campaignTimelineChapters.forEach((chapter, index) => {
|
|
const status = this.timelineChapterStatus(chapter, index, progress.activeChapterIndex, completedIds);
|
|
const rowY = listY + 30 + index * 31;
|
|
const active = index === progress.activeChapterIndex;
|
|
const complete = status === '완료';
|
|
const row = this.track(this.add.rectangle(listX, rowY, 360, 27, active ? 0x25384a : complete ? 0x17231d : 0x151f2a, active ? 0.96 : 0.84));
|
|
row.setOrigin(0);
|
|
row.setStrokeStyle(1, active ? palette.gold : complete ? palette.green : palette.blue, active ? 0.7 : 0.34);
|
|
const markerColor = active ? '#fff2b8' : complete ? '#a8ffd0' : '#9fb0bf';
|
|
this.track(this.add.text(listX + 12, rowY + 5, status, this.textStyle(11, markerColor, true)));
|
|
this.track(this.add.text(listX + 64, rowY + 4, chapter.title, this.textStyle(13, active ? '#f2e3bf' : '#d4dce6', active)));
|
|
const countText = chapter.battleIds.length > 0
|
|
? `${chapter.battleIds.filter((id) => completedIds.has(id)).length}/${chapter.battleIds.length}`
|
|
: '예정';
|
|
const count = this.track(this.add.text(listX + 342, rowY + 5, countText, this.textStyle(11, markerColor, true)));
|
|
count.setOrigin(1, 0);
|
|
});
|
|
|
|
this.renderProgressChapterDetail(progress.activeChapter, progress.activeChapterIndex, x + 410, listY, 390, 310, completedIds);
|
|
}
|
|
|
|
private renderProgressStatCard(label: string, value: string, x: number, y: number, width: number) {
|
|
const bg = this.track(this.add.rectangle(x, y, width, 44, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.blue, 0.4);
|
|
this.track(this.add.text(x + 12, y + 8, label, this.textStyle(11, '#9fb0bf', true)));
|
|
const valueText = this.track(this.add.text(x + width - 12, y + 21, this.compactText(value, width > 190 ? 14 : 11), this.textStyle(14, '#f2e3bf', true)));
|
|
valueText.setOrigin(1, 0);
|
|
}
|
|
|
|
private renderProgressChapterDetail(
|
|
chapter: CampaignTimelineChapter,
|
|
chapterIndex: number,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
completedIds: Set<string>
|
|
) {
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.48);
|
|
const status = this.timelineChapterStatus(chapter, chapterIndex, this.campaignTimelineProgress().activeChapterIndex, completedIds);
|
|
this.track(this.add.text(x + 18, y + 16, chapter.title, this.textStyle(21, '#f2e3bf', true)));
|
|
const statusText = this.track(this.add.text(x + width - 18, y + 19, status, this.textStyle(13, this.timelineStatusColor(status), true)));
|
|
statusText.setOrigin(1, 0);
|
|
this.track(this.add.text(x + 18, y + 48, chapter.period, this.textStyle(13, '#d8b15f', true)));
|
|
this.track(
|
|
this.add.text(x + 18, y + 74, chapter.description, {
|
|
...this.textStyle(13, '#d4dce6'),
|
|
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
|
lineSpacing: 2
|
|
})
|
|
);
|
|
|
|
const completedCount = chapter.battleIds.filter((id) => completedIds.has(id)).length;
|
|
const total = chapter.battleIds.length;
|
|
const ratio = total > 0 ? completedCount / total : status === '다음' ? 0.08 : 0;
|
|
this.track(this.add.text(x + 18, y + 136, total > 0 ? `전장 진행 ${completedCount}/${total}` : '아직 전장이 편성되지 않은 장입니다.', this.textStyle(13, '#f2e3bf', true)));
|
|
this.drawBar(x + 18, y + 162, width - 36, 8, ratio, status === '완료' ? palette.green : palette.gold);
|
|
|
|
if (chapter.battleIds.length > 0) {
|
|
chapter.battleIds.forEach((battleId, index) => {
|
|
const battle = getBattleScenario(battleId);
|
|
const complete = completedIds.has(battleId);
|
|
const rowY = y + 190 + index * 28;
|
|
const row = this.track(this.add.rectangle(x + 18, rowY, width - 36, 24, complete ? 0x17231d : 0x151f2a, 0.86));
|
|
row.setOrigin(0);
|
|
row.setStrokeStyle(1, complete ? palette.green : palette.blue, complete ? 0.48 : 0.32);
|
|
this.track(this.add.text(x + 30, rowY + 4, complete ? '완료' : '대기', this.textStyle(10, complete ? '#a8ffd0' : '#9fb0bf', true)));
|
|
this.track(this.add.text(x + 78, rowY + 3, battle.title, this.textStyle(12, complete ? '#d4dce6' : '#f2e3bf', !complete)));
|
|
const objective = this.track(this.add.text(x + width - 30, rowY + 4, battle.victoryConditionLabel, this.textStyle(10, '#9fb0bf', true)));
|
|
objective.setOrigin(1, 0);
|
|
});
|
|
return;
|
|
}
|
|
|
|
chapter.nextHints.forEach((hint, index) => {
|
|
const rowY = y + 190 + index * 30;
|
|
const dot = this.track(this.add.circle(x + 28, rowY + 11, 4, status === '다음' ? palette.gold : palette.blue, status === '다음' ? 0.92 : 0.48));
|
|
dot.setStrokeStyle(1, 0x05070a, 0.6);
|
|
this.track(this.add.text(x + 44, rowY + 3, hint, this.textStyle(13, status === '다음' ? '#f2e3bf' : '#9fb0bf', status === '다음')));
|
|
});
|
|
}
|
|
|
|
private campaignTimelineProgress() {
|
|
const completedIds = this.completedBattleIds();
|
|
const totalKnown = campaignTimelineChapters.reduce((sum, chapter) => sum + chapter.battleIds.length, 0);
|
|
const completedKnown = campaignTimelineChapters.reduce(
|
|
(sum, chapter) => sum + chapter.battleIds.filter((id) => completedIds.has(id)).length,
|
|
0
|
|
);
|
|
const activeChapterIndex = this.activeTimelineChapterIndex(completedIds);
|
|
const activeChapter = campaignTimelineChapters[activeChapterIndex];
|
|
const latestBattleId = this.campaign?.latestBattleId ?? this.report?.battleId;
|
|
const flow = this.currentSortieFlow();
|
|
const nextBattleId = flow.nextBattleId;
|
|
return {
|
|
completedKnown,
|
|
totalKnown,
|
|
activeChapterIndex,
|
|
activeChapter,
|
|
latestBattleTitle: latestBattleId ? getBattleScenario(latestBattleId).title : '없음',
|
|
nextBattleTitle: nextBattleId
|
|
? getBattleScenario(nextBattleId).title
|
|
: flow.campaignStep
|
|
? flow.title
|
|
: '군영 정비'
|
|
};
|
|
}
|
|
|
|
private activeTimelineChapterIndex(completedIds = this.completedBattleIds()) {
|
|
const latestBattleId = this.campaign?.latestBattleId ?? this.report?.battleId;
|
|
const flow = this.currentSortieFlow();
|
|
const nextBattleId = flow.nextBattleId;
|
|
if (!nextBattleId && flow.campaignStep === 'northern-campaign-prep-camp') {
|
|
const northernIndex = campaignTimelineChapters.findIndex((chapter) => chapter.id === 'northern-campaign');
|
|
if (northernIndex >= 0) {
|
|
return northernIndex;
|
|
}
|
|
}
|
|
if (!nextBattleId && latestBattleId) {
|
|
const latestChapterIndex = campaignTimelineChapters.findIndex((chapter) =>
|
|
chapter.battleIds.includes(latestBattleId as BattleScenarioId)
|
|
);
|
|
if (latestChapterIndex >= 0) {
|
|
return latestChapterIndex;
|
|
}
|
|
}
|
|
|
|
const index = campaignTimelineChapters.findIndex((chapter) => {
|
|
if (chapter.battleIds.length === 0) {
|
|
return true;
|
|
}
|
|
return chapter.battleIds.some((battleId) => !completedIds.has(battleId));
|
|
});
|
|
return index >= 0 ? index : campaignTimelineChapters.length - 1;
|
|
}
|
|
|
|
private timelineChapterStatus(chapter: CampaignTimelineChapter, index: number, activeIndex: number, completedIds = this.completedBattleIds()) {
|
|
if (index < activeIndex) {
|
|
return '완료';
|
|
}
|
|
if (index > activeIndex) {
|
|
return '예정';
|
|
}
|
|
if (chapter.battleIds.length === 0) {
|
|
return '다음';
|
|
}
|
|
return '진행';
|
|
}
|
|
|
|
private timelineStatusColor(status: string) {
|
|
if (status === '완료') {
|
|
return '#a8ffd0';
|
|
}
|
|
if (status === '다음') {
|
|
return '#fff2b8';
|
|
}
|
|
if (status === '진행') {
|
|
return '#d8b15f';
|
|
}
|
|
return '#9fb0bf';
|
|
}
|
|
|
|
private completedBattleIds() {
|
|
return new Set(Object.keys(this.campaign?.battleHistory ?? {}));
|
|
}
|
|
|
|
private latestReserveTrainingAwards() {
|
|
const latestBattleId = this.campaign?.latestBattleId;
|
|
if (!latestBattleId) {
|
|
return [];
|
|
}
|
|
return this.campaign?.battleHistory[latestBattleId]?.reserveTraining ?? [];
|
|
}
|
|
|
|
private latestReserveTrainingFocusLabel() {
|
|
return this.latestReserveTrainingAwards()[0]?.focusLabel ?? '대기';
|
|
}
|
|
|
|
private latestReserveTraining(unitId: string) {
|
|
return this.latestReserveTrainingAwards().find((entry) => entry.unitId === unitId);
|
|
}
|
|
|
|
private rosterCollectionSummary() {
|
|
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
|
const selectedIds = new Set(this.selectedSortieUnitIds);
|
|
const selectedCount = allies.filter((unit) => selectedIds.has(unit.id)).length;
|
|
const recruitedCount = allies.filter((unit) => !foundingSortieUnitIds.has(unit.id)).length;
|
|
const reserveUnits = allies.filter((unit) => !selectedIds.has(unit.id));
|
|
const reserveTrainingAwards = this.latestReserveTrainingAwards();
|
|
const averageLevel = allies.length > 0 ? Math.round(allies.reduce((sum, unit) => sum + unit.level, 0) / allies.length) : 0;
|
|
return {
|
|
total: allies.length,
|
|
selectedCount,
|
|
reserveCount: reserveUnits.length,
|
|
recruitedCount,
|
|
averageLevel,
|
|
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
|
|
reserveTrainingAssignmentCount: reserveUnits.filter((unit) => this.campaign?.reserveTrainingAssignments[unit.id]).length,
|
|
reserveTrainingCount: reserveTrainingAwards.length,
|
|
reserveTrainingExp: reserveTrainingAwards.reduce((sum, entry) => sum + entry.expGained, 0)
|
|
};
|
|
}
|
|
|
|
private renderStatusPanel() {
|
|
const unit = this.selectedUnit();
|
|
if (!unit) {
|
|
return;
|
|
}
|
|
|
|
const x = 394;
|
|
const y = 120;
|
|
const bg = this.track(this.add.rectangle(x, y, 828, 444, 0x101820, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.blue, 0.56);
|
|
|
|
const unitClass = getUnitClass(unit.classKey);
|
|
const portraitKey = portraitByUnitId[unit.id];
|
|
const portraitTexture = portraitKey ? portraitTextureKey(portraitKey) : undefined;
|
|
const portraitFrame = this.track(this.add.rectangle(x + 84, y + 90, 122, 122, 0x0b1219, 0.92));
|
|
portraitFrame.setStrokeStyle(2, palette.gold, 0.62);
|
|
if (portraitTexture && this.textures.exists(portraitTexture)) {
|
|
const portrait = this.track(this.add.image(x + 84, y + 90, portraitTexture));
|
|
portrait.setDisplaySize(112, 112);
|
|
} else {
|
|
this.renderUnitFallbackPortrait(x + 84, y + 90, 48, unit);
|
|
}
|
|
|
|
this.track(this.add.text(x + 166, y + 22, `${unit.name} Lv ${unit.level}`, this.textStyle(27, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 166, y + 60, `${unit.className} · ${unitClass.family} · ${unitClass.role}`, this.textStyle(15, '#d8b15f', true)));
|
|
this.track(
|
|
this.add.text(x + 166, y + 91, unitClass.description, {
|
|
...this.textStyle(14, '#d4dce6'),
|
|
wordWrap: { width: 324, useAdvancedWrap: true },
|
|
lineSpacing: 2
|
|
})
|
|
);
|
|
this.renderDetailGauge('병력', `${unit.hp}/${unit.maxHp}`, unit.hp / unit.maxHp, x + 166, y + 142, 300, palette.green);
|
|
this.renderDetailGauge('경험', `${unit.exp}/100`, unit.exp / 100, x + 166, y + 168, 300, palette.gold);
|
|
|
|
const statX = x + 524;
|
|
const stats = [
|
|
['공격', this.statWithBonus(unit.attack, this.equipmentAttackBonus(unit))],
|
|
['이동', unit.move],
|
|
['무력', unit.stats.might],
|
|
['지력', this.statWithBonus(unit.stats.intelligence, this.equipmentStrategyBonus(unit))],
|
|
['통솔', this.statWithBonus(unit.stats.leadership, this.equipmentDefenseBonus(unit))],
|
|
['민첩', unit.stats.agility],
|
|
['운', unit.stats.luck]
|
|
] as const;
|
|
stats.forEach(([label, value], index) => {
|
|
this.renderStatCard(label, `${value}`, statX + (index % 2) * 128, y + 22 + Math.floor(index / 2) * 42, 116);
|
|
});
|
|
|
|
this.renderSelectedUnitRosterPlan(unit, x + 24, y + 196, 780);
|
|
|
|
this.track(this.add.text(x + 24, y + 224, '장비', this.textStyle(20, '#f2e3bf', true)));
|
|
equipmentSlots.forEach((slot, index) => {
|
|
this.renderEquipmentCard(unit, slot, x + 24 + index * 260, y + 256, 244, 98);
|
|
});
|
|
|
|
this.renderSelectedUnitBondPanel(unit, x + 24, y + 366, 780);
|
|
}
|
|
|
|
private renderDialoguePanel() {
|
|
const x = 394;
|
|
const y = 120;
|
|
const bg = this.track(this.add.rectangle(x, y, 828, 444, 0x101820, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.58);
|
|
this.track(this.add.text(x + 24, y + 22, '장수 대화', this.textStyle(24, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 24, y + 56, '출진 전 대화에서 선택지를 고르면 해당 장수들의 공명 경험치가 오릅니다.', this.textStyle(14, '#d4dce6')));
|
|
|
|
const dialogues = this.availableCampDialogues();
|
|
const compactDialogueList = dialogues.length > 3;
|
|
const dialogueRowGap = compactDialogueList ? 44 : 64;
|
|
const dialogueRowHeight = compactDialogueList ? 36 : 48;
|
|
if (!dialogues.some((dialogue) => dialogue.id === this.selectedDialogueId)) {
|
|
this.selectedDialogueId = dialogues[0]?.id ?? campDialogues[0].id;
|
|
}
|
|
|
|
dialogues.forEach((dialogue, index) => {
|
|
const completed = this.completedCampDialogues().includes(dialogue.id);
|
|
const rowY = y + 96 + index * dialogueRowGap;
|
|
const selected = this.selectedDialogueId === dialogue.id;
|
|
const row = this.track(this.add.rectangle(x + 24, rowY, 320, dialogueRowHeight, selected ? 0x25384a : 0x151f2a, selected ? 0.96 : 0.86));
|
|
row.setOrigin(0);
|
|
row.setStrokeStyle(1, completed ? palette.green : selected ? palette.gold : palette.blue, selected ? 0.72 : 0.46);
|
|
row.setInteractive({ useHandCursor: true });
|
|
row.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.selectedDialogueId = dialogue.id;
|
|
this.render();
|
|
});
|
|
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 5 : 8), dialogue.title, this.textStyle(compactDialogueList ? 13 : 15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
|
const maxReward = dialogue.rewardExp + Math.max(...dialogue.choices.map((choice) => choice.rewardExp));
|
|
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 22 : 29), `${this.unitName(dialogue.unitIds[0])} · ${this.unitName(dialogue.unitIds[1])} 공명 +${dialogue.rewardExp}~${maxReward}`, this.textStyle(compactDialogueList ? 10 : 12, '#9fb0bf')));
|
|
});
|
|
|
|
this.renderSelectedDialogue(x + 372, y + 96, 416, 300);
|
|
this.renderBondList(x + 24, y + 308, 320);
|
|
}
|
|
|
|
private renderSelectedDialogue(x: number, y: number, width: number, height: number) {
|
|
const dialogues = this.availableCampDialogues();
|
|
const dialogue = dialogues.find((candidate) => candidate.id === this.selectedDialogueId) ?? dialogues[0];
|
|
if (!dialogue) {
|
|
const empty = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
empty.setOrigin(0);
|
|
empty.setStrokeStyle(1, palette.blue, 0.42);
|
|
this.track(this.add.text(x + 18, y + 16, '대화 없음', this.textStyle(20, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 18, y + 54, '현재 군영에서 가능한 장수 대화가 없습니다.', this.textStyle(14, '#9fb0bf')));
|
|
return;
|
|
}
|
|
const completed = this.completedCampDialogues().includes(dialogue.id);
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.5);
|
|
this.track(this.add.text(x + 18, y + 16, dialogue.title, this.textStyle(20, '#f2e3bf', true)));
|
|
this.track(
|
|
this.add.text(x + 18, y + 52, dialogue.lines.join('\n\n'), {
|
|
...this.textStyle(15, '#d4dce6'),
|
|
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
|
lineSpacing: 5
|
|
})
|
|
);
|
|
|
|
if (completed) {
|
|
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 220, 34, 0x17231d, 0.96));
|
|
done.setStrokeStyle(1, palette.green, 0.76);
|
|
const text = this.track(this.add.text(x + width / 2, y + height - 34, '대화 완료', this.textStyle(15, '#a8ffd0', true)));
|
|
text.setOrigin(0.5);
|
|
return;
|
|
}
|
|
|
|
dialogue.choices.forEach((choice, index) => {
|
|
const choiceY = y + height - 80 + index * 42;
|
|
const button = this.track(this.add.rectangle(x + width / 2, choiceY, width - 36, 34, 0x1a2630, 0.96));
|
|
button.setStrokeStyle(1, palette.gold, 0.76);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
|
button.on('pointerdown', () => this.completeDialogue(dialogue, choice));
|
|
|
|
const label = `${choice.label} 공명 +${dialogue.rewardExp + choice.rewardExp}`;
|
|
const text = this.track(this.add.text(x + width / 2, choiceY, label, this.textStyle(14, '#f2e3bf', true)));
|
|
text.setOrigin(0.5);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', () => this.completeDialogue(dialogue, choice));
|
|
});
|
|
}
|
|
|
|
private renderBondList(x: number, y: number, width: number) {
|
|
const bonds = this.currentBonds();
|
|
const compact = bonds.length > 3;
|
|
const rowGap = compact ? 25 : 36;
|
|
this.track(this.add.text(x, y, '공명', this.textStyle(18, '#f2e3bf', true)));
|
|
bonds.forEach((bond, index) => {
|
|
const rowY = y + (compact ? 28 : 32) + index * rowGap;
|
|
const first = this.unitName(bond.unitIds[0]);
|
|
const second = this.unitName(bond.unitIds[1]);
|
|
this.track(this.add.text(x, rowY, `${first}·${second} Lv ${bond.level}`, this.textStyle(compact ? 11 : 13, '#d4dce6')));
|
|
this.drawBar(x + 136, rowY + 6, width - 136, 7, bond.exp / 100, bond.battleExp > 0 ? palette.gold : palette.blue);
|
|
});
|
|
}
|
|
|
|
private renderVisitPanel() {
|
|
const x = 394;
|
|
const y = 120;
|
|
const bg = this.track(this.add.rectangle(x, y, 828, 444, 0x101820, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.58);
|
|
this.track(this.add.text(x + 24, y + 22, '형주 방문', this.textStyle(24, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 24, y + 56, '군영 밖 사람들을 만나 정보, 보급, 공명 보상을 얻습니다. 각 방문은 한 번만 완료할 수 있습니다.', this.textStyle(14, '#d4dce6')));
|
|
|
|
const visits = this.availableCampVisits();
|
|
if (!visits.some((visit) => visit.id === this.selectedVisitId)) {
|
|
this.selectedVisitId = visits[0]?.id ?? campVisits[0].id;
|
|
}
|
|
|
|
if (visits.length === 0) {
|
|
const empty = this.track(this.add.rectangle(x + 24, y + 104, 780, 250, 0x0d141c, 0.92));
|
|
empty.setOrigin(0);
|
|
empty.setStrokeStyle(1, palette.blue, 0.42);
|
|
this.track(this.add.text(x + 48, y + 130, '방문 가능한 장소 없음', this.textStyle(20, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 48, y + 170, '현재 군영에서는 따로 방문할 장소가 없습니다. 장수 대화와 정비를 진행하십시오.', this.textStyle(14, '#9fb0bf')));
|
|
return;
|
|
}
|
|
|
|
visits.forEach((visit, index) => {
|
|
const completed = this.completedCampVisits().includes(visit.id);
|
|
const rowY = y + 96 + index * 66;
|
|
const selected = this.selectedVisitId === visit.id;
|
|
const row = this.track(this.add.rectangle(x + 24, rowY, 320, 50, selected ? 0x25384a : 0x151f2a, selected ? 0.96 : 0.86));
|
|
row.setOrigin(0);
|
|
row.setStrokeStyle(1, completed ? palette.green : selected ? palette.gold : palette.blue, selected ? 0.72 : 0.46);
|
|
row.setInteractive({ useHandCursor: true });
|
|
row.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.selectedVisitId = visit.id;
|
|
this.render();
|
|
});
|
|
this.track(this.add.text(x + 38, rowY + 8, visit.title, this.textStyle(15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 38, rowY + 29, visit.location, this.textStyle(12, '#9fb0bf')));
|
|
});
|
|
|
|
this.renderSelectedVisit(x + 372, y + 96, 416, 300);
|
|
this.renderVisitPreparationSummary(x + 24, y + 322, 320);
|
|
}
|
|
|
|
private renderSelectedVisit(x: number, y: number, width: number, height: number) {
|
|
const visits = this.availableCampVisits();
|
|
const visit = visits.find((candidate) => candidate.id === this.selectedVisitId) ?? visits[0];
|
|
if (!visit) {
|
|
return;
|
|
}
|
|
|
|
const completed = this.completedCampVisits().includes(visit.id);
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.5);
|
|
this.track(this.add.text(x + 18, y + 14, visit.title, this.textStyle(20, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 18, y + 42, visit.description, {
|
|
...this.textStyle(13, '#d8b15f', true),
|
|
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
|
lineSpacing: 2
|
|
}));
|
|
this.track(
|
|
this.add.text(x + 18, y + 88, visit.lines.join('\n\n'), {
|
|
...this.textStyle(14, '#d4dce6'),
|
|
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
|
lineSpacing: 4
|
|
})
|
|
);
|
|
|
|
if (completed) {
|
|
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 220, 34, 0x17231d, 0.96));
|
|
done.setStrokeStyle(1, palette.green, 0.76);
|
|
const text = this.track(this.add.text(x + width / 2, y + height - 34, '방문 완료', this.textStyle(15, '#a8ffd0', true)));
|
|
text.setOrigin(0.5);
|
|
return;
|
|
}
|
|
|
|
visit.choices.forEach((choice, index) => {
|
|
const choiceY = y + height - 80 + index * 42;
|
|
const button = this.track(this.add.rectangle(x + width / 2, choiceY, width - 36, 34, 0x1a2630, 0.96));
|
|
button.setStrokeStyle(1, palette.gold, 0.76);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
|
button.on('pointerdown', () => this.completeVisit(visit, choice));
|
|
|
|
const label = `${choice.label} ${this.visitRewardText(choice)}`;
|
|
const text = this.track(this.add.text(x + width / 2, choiceY, label, this.textStyle(13, '#f2e3bf', true)));
|
|
text.setOrigin(0.5);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', () => this.completeVisit(visit, choice));
|
|
});
|
|
}
|
|
|
|
private renderVisitPreparationSummary(x: number, y: number, width: number) {
|
|
const visits = this.availableCampVisits();
|
|
const completed = this.completedAvailableVisits();
|
|
const bg = this.track(this.add.rectangle(x, y, width, 104, 0x0d141c, 0.9));
|
|
bg.setOrigin(0);
|
|
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.wolongClueCount();
|
|
this.track(this.add.text(x + 14, y + 66, `제갈량 단서 ${insightCount} · 군자금 ${this.campaign?.gold ?? 0}`, this.textStyle(13, '#9fb0bf', true)));
|
|
}
|
|
|
|
private renderEquipmentPanel() {
|
|
const unit = this.selectedUnit();
|
|
if (!unit) {
|
|
return;
|
|
}
|
|
|
|
const x = 394;
|
|
const y = 120;
|
|
const width = 828;
|
|
const height = 464;
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x101820, 0.91));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.58);
|
|
|
|
this.track(this.add.text(x + 24, y + 22, '장비 관리', this.textStyle(24, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 24, y + 58, '선택 장수의 무기·방어구·보조구 효과와 성장도를 비교하고 바로 교체합니다.', this.textStyle(14, '#d4dce6')));
|
|
|
|
this.renderEquipmentUnitSummary(unit, x + 24, y + 86, width - 48, 48);
|
|
|
|
equipmentSlots.forEach((slot, index) => {
|
|
this.renderEquipmentCard(unit, slot, x + 24 + index * 260, y + 150, 252, 96);
|
|
});
|
|
|
|
const inventoryEntries = this.equipmentInventoryEntries();
|
|
this.track(this.add.text(x + 24, y + 266, `보유 장비 ${inventoryEntries.length}`, this.textStyle(18, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 516, y + 268, '장비 효과 요약', this.textStyle(17, '#f2e3bf', true)));
|
|
|
|
if (inventoryEntries.length === 0) {
|
|
const empty = this.track(this.add.rectangle(x + 24, y + 298, 468, 86, 0x0d141c, 0.9));
|
|
empty.setOrigin(0);
|
|
empty.setStrokeStyle(1, palette.blue, 0.38);
|
|
this.track(this.add.text(x + 44, y + 324, '교체 가능한 보유 장비가 없습니다.', this.textStyle(15, '#9fb0bf', true)));
|
|
this.track(this.add.text(x + 44, y + 352, '전투 보상이나 군영 상인을 통해 장비를 확보하면 이곳에서 비교할 수 있습니다.', this.textStyle(12, '#77818c')));
|
|
} else {
|
|
inventoryEntries.slice(0, 4).forEach((entry, index) => {
|
|
this.renderEquipmentCompareRow(entry, unit, x + 24, y + 298 + index * 40, 468, 36);
|
|
});
|
|
}
|
|
|
|
this.renderEquipmentEffectSummary(unit, x + 516, y + 298, 280, 128);
|
|
this.renderEquipmentInventorySummary(inventoryEntries, x + 516, y + 432, 280);
|
|
}
|
|
|
|
private renderEquipmentUnitSummary(unit: UnitData, x: number, y: number, width: number, height: number) {
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.blue, 0.42);
|
|
const classInfo = getUnitClass(unit.classKey);
|
|
this.track(this.add.text(x + 14, y + 9, `${unit.name} Lv ${unit.level}`, this.textStyle(17, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 14, y + 29, `${unit.className} · ${classInfo.role}`, this.textStyle(11, '#9fb0bf', true)));
|
|
|
|
const stats = [
|
|
`공격 ${this.statWithBonus(unit.attack, this.equipmentAttackBonus(unit))}`,
|
|
`지력 ${this.statWithBonus(unit.stats.intelligence, this.equipmentStrategyBonus(unit))}`,
|
|
`통솔 ${this.statWithBonus(unit.stats.leadership, this.equipmentDefenseBonus(unit))}`,
|
|
`HP ${unit.hp}/${unit.maxHp}`
|
|
];
|
|
const statText = this.track(this.add.text(x + width - 14, y + 15, stats.join(' '), this.textStyle(13, '#d4dce6', true)));
|
|
statText.setOrigin(1, 0);
|
|
}
|
|
|
|
private renderEquipmentCompareRow(
|
|
entry: { label: string; amount: number; item: ItemDefinition },
|
|
unit: UnitData,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number
|
|
) {
|
|
const currentItem = getItem(unit.equipment[entry.item.slot].itemId);
|
|
const sameItem = currentItem.id === entry.item.id;
|
|
const deltaScore = this.itemTotalBonus(entry.item) - this.itemTotalBonus(currentItem);
|
|
const canEquip = !sameItem;
|
|
const rowColor = sameItem ? 0x111922 : deltaScore > 0 ? 0x172a22 : deltaScore < 0 ? 0x241b1b : 0x151f2a;
|
|
const strokeColor = sameItem ? 0x53606c : entry.item.rank === 'treasure' ? palette.gold : deltaScore > 0 ? palette.green : palette.blue;
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, rowColor, canEquip ? 0.94 : 0.72));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, strokeColor, canEquip ? 0.58 : 0.3);
|
|
|
|
const iconFrame = this.track(this.add.rectangle(x + 20, y + height / 2, 28, 28, 0x0a1017, 0.92));
|
|
iconFrame.setStrokeStyle(1, entry.item.rank === 'treasure' ? palette.gold : 0x53606c, 0.54);
|
|
const icon = this.track(this.add.image(x + 20, y + height / 2, `item-${entry.item.id}`));
|
|
icon.setDisplaySize(22, 22);
|
|
icon.setAlpha(canEquip ? 1 : 0.52);
|
|
|
|
const nameColor = sameItem ? '#87919c' : entry.item.rank === 'treasure' ? '#f4dfad' : '#f2e3bf';
|
|
this.track(this.add.text(x + 42, y + 5, `${entry.item.name} x${entry.amount}`, this.textStyle(13, nameColor, true)));
|
|
this.track(this.add.text(x + 42, y + 22, `${equipmentSlotLabels[entry.item.slot]} · ${this.equipmentDeltaText(entry.item, currentItem)}`, this.textStyle(10, deltaScore > 0 ? '#a8ffd0' : deltaScore < 0 ? '#ff9d7d' : '#9fb0bf', true)));
|
|
this.track(this.add.text(x + 244, y + 7, this.compactText(entry.item.effects[0] ?? entry.item.description, 13), this.textStyle(10, '#c8d2dd')));
|
|
|
|
const button = this.track(this.add.rectangle(x + width - 34, y + height / 2, 52, 24, canEquip ? 0x1a2630 : 0x121922, canEquip ? 0.96 : 0.62));
|
|
button.setStrokeStyle(1, canEquip ? palette.gold : 0x53606c, canEquip ? 0.68 : 0.3);
|
|
const buttonText = this.track(this.add.text(x + width - 34, y + height / 2 - 7, canEquip ? '교체' : '착용', this.textStyle(11, canEquip ? '#f2e3bf' : '#87919c', true)));
|
|
buttonText.setOrigin(0.5, 0);
|
|
|
|
if (canEquip) {
|
|
const action = () => this.swapUnitEquipment(unit.id, entry.item.slot, entry.item.id);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerover', () => bg.setFillStyle(deltaScore > 0 ? 0x22342b : 0x283947, 0.98));
|
|
bg.on('pointerout', () => bg.setFillStyle(rowColor, 0.94));
|
|
bg.on('pointerdown', action);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerdown', action);
|
|
buttonText.setInteractive({ useHandCursor: true });
|
|
buttonText.on('pointerdown', action);
|
|
}
|
|
}
|
|
|
|
private renderEquipmentEffectSummary(unit: UnitData, x: number, y: number, width: number, height: number) {
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.46);
|
|
this.track(this.add.text(x + 14, y + 12, '착용 효과', this.textStyle(15, '#f2e3bf', true)));
|
|
|
|
const equipped = equipmentSlots.map((slot) => ({ slot, state: unit.equipment[slot], item: getItem(unit.equipment[slot].itemId) }));
|
|
equipped.forEach((entry, index) => {
|
|
const rowY = y + 42 + index * 30;
|
|
const icon = this.track(this.add.image(x + 20, rowY + 8, `item-${entry.item.id}`));
|
|
icon.setDisplaySize(20, 20);
|
|
this.track(this.add.text(x + 36, rowY, `${equipmentSlotLabels[entry.slot]} Lv${entry.state.level}`, this.textStyle(11, '#d8b15f', true)));
|
|
this.track(this.add.text(x + 108, rowY, this.compactText(this.itemBonusText(entry.item), 18), this.textStyle(11, '#d4dce6', true)));
|
|
this.drawBar(x + 36, rowY + 17, width - 56, 5, entry.state.exp / equipmentExpToNext(entry.state.level), entry.item.rank === 'treasure' ? palette.gold : palette.blue);
|
|
});
|
|
|
|
}
|
|
|
|
private renderEquipmentInventorySummary(entries: { label: string; amount: number; item: ItemDefinition }[], x: number, y: number, width: number) {
|
|
const total = entries.reduce((sum, entry) => sum + entry.amount, 0);
|
|
const treasure = entries.filter((entry) => entry.item.rank === 'treasure').reduce((sum, entry) => sum + entry.amount, 0);
|
|
const bySlot = equipmentSlots
|
|
.map((slot) => `${equipmentSlotLabels[slot]} ${entries.filter((entry) => entry.item.slot === slot).reduce((sum, entry) => sum + entry.amount, 0)}`)
|
|
.join(' · ');
|
|
const bg = this.track(this.add.rectangle(x, y, width, 32, 0x151f2a, 0.82));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.blue, 0.34);
|
|
this.track(this.add.text(x + 14, y + 6, `창고 ${total}개 · 보물 ${treasure}개`, this.textStyle(11, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 136, y + 6, this.compactText(bySlot, 24), this.textStyle(11, '#9fb0bf', true)));
|
|
}
|
|
|
|
private equipmentDeltaText(nextItem: ItemDefinition, currentItem: ItemDefinition) {
|
|
const rawDeltas: Array<[string, number]> = [
|
|
['공격', (nextItem.attackBonus ?? 0) - (currentItem.attackBonus ?? 0)],
|
|
['방어', (nextItem.defenseBonus ?? 0) - (currentItem.defenseBonus ?? 0)],
|
|
['책략', (nextItem.strategyBonus ?? 0) - (currentItem.strategyBonus ?? 0)]
|
|
];
|
|
const deltas = rawDeltas.filter(([, delta]) => delta !== 0);
|
|
return deltas.map(([label, delta]) => `${label}${delta > 0 ? '+' : ''}${delta}`).join(' / ') || '수치 변화 없음';
|
|
}
|
|
|
|
private renderSuppliesPanel() {
|
|
const x = 394;
|
|
const y = 120;
|
|
const bg = this.track(this.add.rectangle(x, y, 828, 444, 0x101820, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.blue, 0.56);
|
|
this.track(this.add.text(x + 24, y + 22, '정비와 보급', this.textStyle(24, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 24, y + 58, '소모품 사용, 출전 보급, 보유 장비를 한 번에 정리합니다.', this.textStyle(14, '#d4dce6')));
|
|
|
|
const unit = this.selectedUnit();
|
|
if (unit) {
|
|
this.renderSupplyTargetCard(unit, x + 24, y + 94, 342, 86);
|
|
}
|
|
|
|
campSupplies.forEach((supply, index) => {
|
|
this.renderSupplyUseRow(supply, x + 390, y + 94 + index * 74, 390);
|
|
});
|
|
|
|
this.renderMerchantPanel(x + 24, y + 202, 342);
|
|
|
|
this.track(this.add.text(x + 390, y + 318, '보유 장비', this.textStyle(18, '#f2e3bf', true)));
|
|
const equipmentEntries = this.equipmentInventoryEntries();
|
|
if (equipmentEntries.length === 0) {
|
|
this.track(this.add.text(x + 390, y + 346, '교체 가능한 장비 없음', this.textStyle(12, '#9fb0bf')));
|
|
} else {
|
|
equipmentEntries.slice(0, 3).forEach((entry, index) => {
|
|
this.renderEquipmentInventoryBox(entry, x + 390 + index * 128, y + 344);
|
|
});
|
|
}
|
|
|
|
this.track(this.add.text(x + 390, y + 410, '전리품과 명성', this.textStyle(17, '#f2e3bf', true)));
|
|
const trophies = this.nonEquipmentInventoryLabels();
|
|
if (trophies.length === 0) {
|
|
this.track(this.add.text(x + 390, y + 438, '장부/명성 전리품 없음', this.textStyle(12, '#9fb0bf')));
|
|
} else {
|
|
trophies.slice(0, 3).forEach((reward, index) => {
|
|
this.renderSupplyBox(reward, x + 390 + index * 128, y + 436);
|
|
});
|
|
}
|
|
}
|
|
|
|
private renderMerchantPanel(x: number, y: number, width: number) {
|
|
const campaign = this.campaign ?? getCampaignState();
|
|
const bg = this.track(this.add.rectangle(x, y, width, 210, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.48);
|
|
this.track(this.add.text(x + 16, y + 10, `군영 상인 · 군자금 ${campaign.gold}`, this.textStyle(17, '#f2e3bf', true)));
|
|
|
|
merchantItems.forEach((item, index) => {
|
|
this.renderMerchantItem(item, x + 16, y + 44 + index * 52, width - 32);
|
|
});
|
|
}
|
|
|
|
private renderMerchantItem(item: MerchantItemDefinition, x: number, y: number, width: number) {
|
|
const campaign = this.campaign ?? getCampaignState();
|
|
const canBuy = campaign.gold >= item.price;
|
|
const bg = this.track(this.add.rectangle(x, y, width, 40, canBuy ? 0x151f2a : 0x111820, canBuy ? 0.94 : 0.76));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, canBuy ? palette.gold : 0x53606c, canBuy ? 0.58 : 0.36);
|
|
this.track(this.add.text(x + 10, y + 6, item.title, this.textStyle(13, canBuy ? '#f2e3bf' : '#7f8994', true)));
|
|
this.track(this.add.text(x + 10, y + 23, `${item.price}금`, this.textStyle(11, canBuy ? '#d8b15f' : '#7f8994', true)));
|
|
|
|
const button = this.track(this.add.rectangle(x + width - 42, y + 20, 60, 26, canBuy ? 0x1a2630 : 0x121922, canBuy ? 0.96 : 0.72));
|
|
button.setStrokeStyle(1, canBuy ? palette.gold : 0x53606c, canBuy ? 0.72 : 0.4);
|
|
const text = this.track(this.add.text(x + width - 42, y + 20, '구매', this.textStyle(12, canBuy ? '#f2e3bf' : '#7f8994', true)));
|
|
text.setOrigin(0.5);
|
|
|
|
if (canBuy) {
|
|
const action = () => this.buyMerchantItem(item);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
|
button.on('pointerdown', action);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerdown', action);
|
|
}
|
|
}
|
|
|
|
private buyMerchantItem(item: MerchantItemDefinition) {
|
|
const campaign = this.campaign ?? getCampaignState();
|
|
if (campaign.gold < item.price) {
|
|
this.showCampNotice('군자금이 부족합니다.');
|
|
return;
|
|
}
|
|
|
|
campaign.gold -= item.price;
|
|
campaign.inventory[item.label] = (campaign.inventory[item.label] ?? 0) + 1;
|
|
this.campaign = saveCampaignState(campaign);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
soundDirector.playSelect();
|
|
this.showCampNotice(`${item.title} 구입 · 군자금 -${item.price}`);
|
|
this.render();
|
|
}
|
|
|
|
private renderSupplyBox(label: string, x: number, y: number) {
|
|
const bg = this.track(this.add.rectangle(x, y, 118, 46, 0x151f2a, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.52);
|
|
this.track(this.add.text(x + 10, y + 14, this.compactText(label, 9), this.textStyle(13, '#f2e3bf', true)));
|
|
}
|
|
|
|
private renderEquipmentInventoryBox(entry: { label: string; amount: number; item: ItemDefinition }, x: number, y: number) {
|
|
const unit = this.selectedUnit();
|
|
const canEquip = Boolean(unit);
|
|
const bg = this.track(this.add.rectangle(x, y, 118, 54, canEquip ? 0x151f2a : 0x111820, canEquip ? 0.92 : 0.72));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, entry.item.rank === 'treasure' ? palette.gold : palette.blue, canEquip ? 0.58 : 0.28);
|
|
const iconFrame = this.track(this.add.rectangle(x + 18, y + 18, 28, 28, 0x0a1017, 0.92));
|
|
iconFrame.setStrokeStyle(1, entry.item.rank === 'treasure' ? palette.gold : 0x53606c, 0.54);
|
|
const icon = this.track(this.add.image(x + 18, y + 18, `item-${entry.item.id}`));
|
|
icon.setDisplaySize(21, 21);
|
|
this.track(this.add.text(x + 38, y + 7, this.compactText(`${entry.item.name} x${entry.amount}`, 8), this.textStyle(11, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 38, y + 23, this.compactText(`${equipmentSlotLabels[entry.item.slot]} · ${this.itemBonusText(entry.item)}`, 11), this.textStyle(9, '#9fb0bf', true)));
|
|
const actionText = this.track(this.add.text(x + 58, y + 39, canEquip ? '장착' : '장수 선택', this.textStyle(9, canEquip ? '#d8b15f' : '#7f8994', true)));
|
|
actionText.setOrigin(0.5, 0);
|
|
|
|
if (canEquip) {
|
|
const action = () => this.swapUnitEquipment(unit!.id, entry.item.slot, entry.item.id);
|
|
bg.setInteractive({ useHandCursor: true });
|
|
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
|
bg.on('pointerout', () => bg.setFillStyle(0x151f2a, 0.92));
|
|
bg.on('pointerdown', action);
|
|
actionText.setInteractive({ useHandCursor: true });
|
|
actionText.on('pointerdown', action);
|
|
}
|
|
}
|
|
|
|
private renderSupplyTargetCard(unit: UnitData, x: number, y: number, width: number, height: number) {
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.48);
|
|
this.track(this.add.text(x + 16, y + 12, '사용 대상', this.textStyle(15, '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 16, y + 38, `${unit.name} ${unit.className} Lv ${unit.level}`, this.textStyle(16, '#d4dce6', true)));
|
|
this.track(this.add.text(x + 16, y + 62, `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(13, '#9fb0bf', true)));
|
|
this.drawBar(x + 118, y + 68, width - 138, 8, unit.hp / unit.maxHp, palette.green);
|
|
}
|
|
|
|
private renderSupplyUseRow(supply: CampSupplyDefinition, x: number, y: number, width: number) {
|
|
const unit = this.selectedUnit();
|
|
const amount = this.inventoryAmount(supply.label);
|
|
const enabled = Boolean(unit && this.canUseSupply(supply, unit));
|
|
const bg = this.track(this.add.rectangle(x, y, width, 62, enabled ? 0x151f2a : 0x111820, enabled ? 0.92 : 0.76));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.62 : 0.38);
|
|
|
|
this.track(this.add.text(x + 14, y + 10, `${supply.title} x${amount}`, this.textStyle(16, amount > 0 ? '#f2e3bf' : '#7f8994', true)));
|
|
this.track(
|
|
this.add.text(x + 14, y + 34, supply.description, {
|
|
...this.textStyle(12, enabled ? '#c8d2dd' : '#77818c'),
|
|
wordWrap: { width: width - 128, useAdvancedWrap: true }
|
|
})
|
|
);
|
|
|
|
const button = this.track(this.add.rectangle(x + width - 54, y + 31, 82, 30, enabled ? 0x1a2630 : 0x121922, enabled ? 0.96 : 0.72));
|
|
button.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.76 : 0.42);
|
|
const buttonText = this.track(this.add.text(x + width - 54, y + 31, enabled ? '사용' : '불가', this.textStyle(13, enabled ? '#f2e3bf' : '#7f8994', true)));
|
|
buttonText.setOrigin(0.5);
|
|
|
|
if (enabled) {
|
|
const action = () => this.useSupply(supply);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
|
button.on('pointerdown', action);
|
|
buttonText.setInteractive({ useHandCursor: true });
|
|
buttonText.on('pointerdown', action);
|
|
}
|
|
}
|
|
|
|
private canUseSupply(supply: CampSupplyDefinition, unit: UnitData) {
|
|
if (this.inventoryAmount(supply.label) <= 0) {
|
|
return false;
|
|
}
|
|
if (supply.healHp > 0 && unit.hp < unit.maxHp) {
|
|
return true;
|
|
}
|
|
return supply.bondExp > 0 && this.currentBonds().some((bond) => bond.unitIds.includes(unit.id));
|
|
}
|
|
|
|
private useSupply(supply: CampSupplyDefinition) {
|
|
const campaign = this.campaign ?? getCampaignState();
|
|
const target = campaign.roster.find((unit) => unit.id === this.selectedUnitId);
|
|
if (!target) {
|
|
this.showCampNotice('사용할 장수를 선택하세요.');
|
|
return;
|
|
}
|
|
|
|
if ((campaign.inventory[supply.label] ?? 0) <= 0) {
|
|
this.showCampNotice(`${supply.title}이 없습니다.`);
|
|
return;
|
|
}
|
|
|
|
const previousHp = target.hp;
|
|
target.hp = Math.min(target.maxHp, target.hp + supply.healHp);
|
|
const hpGain = target.hp - previousHp;
|
|
const bondGain = this.applySupplyBondExp(campaign, target.id, supply.bondExp);
|
|
|
|
if (hpGain <= 0 && bondGain <= 0) {
|
|
this.showCampNotice(`${target.name}에게 지금은 사용할 필요가 없습니다.`);
|
|
return;
|
|
}
|
|
|
|
campaign.inventory[supply.label] -= 1;
|
|
if (campaign.inventory[supply.label] <= 0) {
|
|
delete campaign.inventory[supply.label];
|
|
}
|
|
this.syncReportUnitHp(campaign, target.id, target.hp);
|
|
|
|
this.campaign = saveCampaignState(campaign);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
soundDirector.playEffect('exp-gain', { volume: 0.24, stopAfterMs: 620 });
|
|
|
|
const hpText = hpGain > 0 ? `병력 +${hpGain}` : '';
|
|
const bondText = bondGain > 0 ? `공명 +${supply.bondExp}` : '';
|
|
this.showCampNotice(`${target.name} ${supply.title} 사용 · ${[hpText, bondText].filter(Boolean).join(' / ')}`);
|
|
this.render();
|
|
}
|
|
|
|
private applySupplyBondExp(campaign: CampaignState, unitId: string, amount: number) {
|
|
if (amount <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
let affected = 0;
|
|
campaign.bonds
|
|
.filter((bond) => bond.unitIds.includes(unitId))
|
|
.forEach((bond) => {
|
|
this.advanceCampBond(bond, amount);
|
|
affected += 1;
|
|
const reportBond = campaign.firstBattleReport?.bonds.find((candidate) => candidate.id === bond.id);
|
|
if (reportBond) {
|
|
reportBond.level = bond.level;
|
|
reportBond.exp = bond.exp;
|
|
reportBond.battleExp = bond.battleExp;
|
|
}
|
|
});
|
|
|
|
return affected * amount;
|
|
}
|
|
|
|
private advanceCampBond(bond: CampaignState['bonds'][number], amount: number) {
|
|
const gained = Math.max(0, Math.floor(amount));
|
|
if (gained <= 0) {
|
|
return;
|
|
}
|
|
|
|
bond.battleExp += gained;
|
|
let exp = bond.exp + gained;
|
|
while (bond.level < 100 && exp >= 100) {
|
|
exp -= 100;
|
|
bond.level += 1;
|
|
}
|
|
bond.exp = bond.level >= 100 ? Math.min(exp, 100) : exp;
|
|
}
|
|
|
|
private syncReportUnitHp(campaign: CampaignState, unitId: string, hp: number) {
|
|
const reportUnit = campaign.firstBattleReport?.units.find((unit) => unit.id === unitId);
|
|
if (reportUnit) {
|
|
reportUnit.hp = hp;
|
|
}
|
|
}
|
|
|
|
private renderDetailGauge(label: string, value: string, ratio: number, x: number, y: number, width: number, color: number) {
|
|
this.track(this.add.text(x, y - 2, label, this.textStyle(13, '#9fb0bf', true)));
|
|
this.drawBar(x + 54, y + 6, width - 134, 8, ratio, color);
|
|
const valueText = this.track(this.add.text(x + width, y - 3, value, this.textStyle(13, '#f2e3bf', true)));
|
|
valueText.setOrigin(1, 0);
|
|
}
|
|
|
|
private renderStatCard(label: string, value: string, x: number, y: number, width: number) {
|
|
const bg = this.track(this.add.rectangle(x, y, width, 34, 0x151f2a, 0.82));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, 0x53606c, 0.38);
|
|
this.track(this.add.text(x + 10, y + 8, label, this.textStyle(12, '#9fb0bf', true)));
|
|
const valueText = this.track(this.add.text(x + width - 10, y + 5, value, this.textStyle(17, '#f2e3bf', true)));
|
|
valueText.setOrigin(1, 0);
|
|
}
|
|
|
|
private renderEquipmentCard(unit: UnitData, slot: EquipmentSlot, x: number, y: number, width: number, height: number) {
|
|
const state = unit.equipment[slot];
|
|
const item = getItem(state.itemId);
|
|
const next = equipmentExpToNext(state.level);
|
|
const isTreasure = item.rank === 'treasure';
|
|
const alternatives = this.equipmentInventoryEntries(slot).filter((entry) => entry.item.id !== item.id);
|
|
const bg = this.track(this.add.rectangle(x, y, width, height, isTreasure ? 0x1f2430 : 0x151f2a, 0.92));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, isTreasure ? palette.gold : palette.blue, isTreasure ? 0.68 : 0.42);
|
|
|
|
const iconFrame = this.track(this.add.rectangle(x + 28, y + 29, 42, 42, 0x0a1017, 0.92));
|
|
iconFrame.setStrokeStyle(1, isTreasure ? palette.gold : 0x53606c, isTreasure ? 0.72 : 0.48);
|
|
const icon = this.track(this.add.image(x + 28, y + 29, `item-${item.id}`));
|
|
icon.setDisplaySize(30, 30);
|
|
|
|
this.track(this.add.text(x + 58, y + 11, this.compactText(item.name, 8), this.textStyle(14, isTreasure ? '#f4dfad' : '#d4dce6', true)));
|
|
this.track(this.add.text(x + 58, y + 34, `${equipmentSlotLabels[slot]} · ${isTreasure ? '보물' : '일반'} Lv ${state.level}`, this.textStyle(11, isTreasure ? '#d8b15f' : '#9fb0bf', true)));
|
|
const valueText = this.track(this.add.text(x + width - 12, y + 34, `${state.exp}/${next}`, this.textStyle(12, '#f0e4c8', true)));
|
|
valueText.setOrigin(1, 0);
|
|
this.drawBar(x + 58, y + 57, width - 74, 7, state.exp / next, isTreasure ? palette.gold : palette.blue);
|
|
|
|
const canSwap = alternatives.length > 0;
|
|
const button = this.track(this.add.rectangle(x + width - 36, y + 16, 54, 22, canSwap ? 0x1a2630 : 0x121922, canSwap ? 0.96 : 0.66));
|
|
button.setStrokeStyle(1, canSwap ? palette.gold : 0x53606c, canSwap ? 0.7 : 0.3);
|
|
const buttonText = this.track(this.add.text(x + width - 36, y + 16, canSwap ? '교체' : '없음', this.textStyle(10, canSwap ? '#f2e3bf' : '#7f8994', true)));
|
|
buttonText.setOrigin(0.5);
|
|
if (canSwap) {
|
|
const action = () => this.swapUnitEquipment(unit.id, slot);
|
|
button.setInteractive({ useHandCursor: true });
|
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
|
button.on('pointerdown', action);
|
|
buttonText.setInteractive({ useHandCursor: true });
|
|
buttonText.on('pointerdown', action);
|
|
}
|
|
|
|
this.track(
|
|
this.add.text(x + 14, y + 74, this.compactText(`${this.itemBonusText(item)} · ${item.effects[0] ?? item.description}`, 27), {
|
|
...this.textStyle(11, '#c8d2dd'),
|
|
wordWrap: { width: width - 28, useAdvancedWrap: true }
|
|
})
|
|
);
|
|
}
|
|
|
|
private renderSelectedUnitBondPanel(unit: UnitData, x: number, y: number, width: number) {
|
|
const bonds = this.currentBonds().filter((bond) => bond.unitIds.includes(unit.id));
|
|
const bg = this.track(this.add.rectangle(x, y, width, 62, 0x0d141c, 0.9));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, palette.gold, 0.42);
|
|
this.track(this.add.text(x + 14, y + 9, '공명 관계', this.textStyle(16, '#f2e3bf', true)));
|
|
|
|
if (bonds.length === 0) {
|
|
this.track(this.add.text(x + 120, y + 22, '아직 연결된 공명 관계가 없습니다.', this.textStyle(13, '#9fb0bf')));
|
|
return;
|
|
}
|
|
|
|
bonds.forEach((bond, index) => {
|
|
const cardX = x + 116 + index * 322;
|
|
const partnerId = bond.unitIds.find((unitId) => unitId !== unit.id) ?? bond.unitIds[0];
|
|
const partnerName = this.unitName(partnerId);
|
|
this.track(this.add.text(cardX, y + 10, `${partnerName} · ${bond.title}`, this.textStyle(13, '#d4dce6', true)));
|
|
this.track(this.add.text(cardX, y + 32, `Lv ${bond.level} ${bond.exp}/100 전투 +${bond.battleExp}`, this.textStyle(12, bond.battleExp > 0 ? '#d8b15f' : '#9fb0bf', true)));
|
|
this.drawBar(cardX + 176, y + 38, 120, 7, bond.exp / 100, bond.battleExp > 0 ? palette.gold : palette.blue);
|
|
});
|
|
}
|
|
|
|
private renderSelectedUnitRosterPlan(unit: UnitData, x: number, y: number, width: number) {
|
|
const status = this.unitRosterStatus(unit);
|
|
const training = this.latestReserveTraining(unit.id);
|
|
const role = this.sortieFormationRole(unit);
|
|
const roleLabel = this.sortieFormationRoleLabel(role);
|
|
const recommendation = this.sortieRecommendation(unit.id);
|
|
const plannedReserveFocus = !this.isSortieSelected(unit.id) ? this.reserveTrainingFocusDefinitionForUnit(unit.id) : undefined;
|
|
const hasIndividualReserveFocus = Boolean(this.campaign?.reserveTrainingAssignments[unit.id]);
|
|
const bg = this.track(this.add.rectangle(x, y, width, 22, 0x151f2a, 0.82));
|
|
bg.setOrigin(0);
|
|
bg.setStrokeStyle(1, training ? palette.green : palette.blue, training ? 0.5 : 0.34);
|
|
const planText = recommendation ? this.sortieRecommendationHint(recommendation, unit, 28) : `${roleLabel} 역할로 편성 가능`;
|
|
const trainingText = training
|
|
? `최근 ${training.focusLabel ?? '대기 훈련'} 경험 +${training.expGained}, 장비 +${training.equipmentExpGained}${training.bondExpGained ? `, 공명 +${training.bondExpGained}` : ''}`
|
|
: plannedReserveFocus
|
|
? `${hasIndividualReserveFocus ? '개별' : '기본'} ${plannedReserveFocus.label} 예정`
|
|
: '대기 중에도 전투 후 선택한 훈련 방침의 성장이 반영됩니다.';
|
|
this.track(this.add.text(x + 12, y + 5, `${status.label} · ${roleLabel}`, this.textStyle(11, status.tone === 'reserve' ? '#9fb0bf' : '#f2e3bf', true)));
|
|
this.track(this.add.text(x + 112, y + 5, this.compactText(planText, 48), this.textStyle(11, '#d4dce6')));
|
|
const trainingLabel = this.track(this.add.text(x + width - 12, y + 5, this.compactText(trainingText, 28), this.textStyle(11, training ? '#a8ffd0' : '#9fb0bf', true)));
|
|
trainingLabel.setOrigin(1, 0);
|
|
}
|
|
|
|
private itemBonusText(item: ReturnType<typeof getItem>) {
|
|
const bonuses = [
|
|
item.attackBonus ? `공격 +${item.attackBonus}` : '',
|
|
item.defenseBonus ? `방어 +${item.defenseBonus}` : '',
|
|
item.strategyBonus ? `책략 +${item.strategyBonus}` : ''
|
|
].filter(Boolean);
|
|
return bonuses.join(' / ') || '특수 보정 없음';
|
|
}
|
|
|
|
private completeVisit(visit: CampVisitDefinition, choice: CampVisitChoice) {
|
|
if (this.completedCampVisits().includes(visit.id)) {
|
|
this.showCampNotice('이미 완료한 방문입니다.');
|
|
return;
|
|
}
|
|
|
|
const updated = applyCampVisitReward(visit.id, {
|
|
bondId: visit.bondId,
|
|
bondExp: choice.bondExp,
|
|
gold: choice.gold,
|
|
itemRewards: choice.itemRewards
|
|
});
|
|
|
|
if (updated) {
|
|
this.campaign = updated;
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
}
|
|
|
|
soundDirector.playEffect('exp-gain', { volume: 0.26, stopAfterMs: 700 });
|
|
this.showVisitReward(choice);
|
|
this.render();
|
|
}
|
|
|
|
private showVisitReward(choice: CampVisitChoice) {
|
|
this.showCampNotice(`${choice.label} · ${this.visitRewardText(choice)}`);
|
|
this.time.delayedCall(120, () => this.showCampNotice(choice.response));
|
|
}
|
|
|
|
private visitRewardText(choice: CampVisitChoice) {
|
|
const rewards = [
|
|
choice.bondExp ? `공명 +${choice.bondExp}` : '',
|
|
choice.gold ? `군자금 +${choice.gold}` : '',
|
|
...(choice.itemRewards ?? []).map((reward) => `${reward}`)
|
|
].filter(Boolean);
|
|
return rewards.join(' / ') || '정보 획득';
|
|
}
|
|
|
|
private completeDialogue(dialogue: CampDialogue, choice: CampDialogueChoice) {
|
|
if (this.completedCampDialogues().includes(dialogue.id)) {
|
|
this.showCampNotice('이미 완료된 대화입니다.');
|
|
return;
|
|
}
|
|
|
|
const rewardExp = dialogue.rewardExp + choice.rewardExp;
|
|
const updated = applyCampBondExp(dialogue.id, dialogue.bondId, rewardExp);
|
|
if (updated) {
|
|
this.report = updated;
|
|
this.campaign = getCampaignState();
|
|
} else if (this.report && !this.report.completedCampDialogues.includes(dialogue.id)) {
|
|
this.report.completedCampDialogues.push(dialogue.id);
|
|
const bond = this.report.bonds.find((candidate) => candidate.id === dialogue.bondId);
|
|
if (bond) {
|
|
this.advanceCampBond(bond, rewardExp);
|
|
}
|
|
}
|
|
|
|
soundDirector.playEffect('exp-gain', { volume: 0.28, stopAfterMs: 700 });
|
|
this.showDialogueReward(dialogue, choice, rewardExp);
|
|
this.render();
|
|
}
|
|
|
|
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
|
this.showCampNotice(`${choice.label} · 공명 +${rewardExp}`);
|
|
this.time.delayedCall(120, () => this.showCampNotice(choice.response));
|
|
}
|
|
|
|
private showCampNotice(message: string) {
|
|
this.dialogueObjects.forEach((object) => object.destroy());
|
|
this.dialogueObjects = [];
|
|
const depth = this.sortieObjects.length > 0 ? 72 : 30;
|
|
const noticeWidth = Math.min(640, Math.max(360, message.length * 13));
|
|
const box = this.add.rectangle(this.scale.width / 2, 104, noticeWidth, 50, 0x101820, 0.96);
|
|
box.setStrokeStyle(2, palette.gold, 0.84);
|
|
box.setDepth(depth);
|
|
const text = this.add.text(this.scale.width / 2, 104, message, {
|
|
...this.textStyle(14, '#f2e3bf', true),
|
|
align: 'center',
|
|
wordWrap: { width: noticeWidth - 44, useAdvancedWrap: true }
|
|
});
|
|
text.setOrigin(0.5);
|
|
text.setDepth(depth + 1);
|
|
this.dialogueObjects.push(box, text);
|
|
this.tweens.add({
|
|
targets: this.dialogueObjects,
|
|
alpha: 0,
|
|
delay: 1300,
|
|
duration: 320,
|
|
onComplete: () => {
|
|
this.dialogueObjects.forEach((object) => object.destroy());
|
|
this.dialogueObjects = [];
|
|
}
|
|
});
|
|
}
|
|
|
|
private selectedUnit() {
|
|
return this.currentUnits().find((unit) => unit.id === this.selectedUnitId);
|
|
}
|
|
|
|
private unitName(unitId: string) {
|
|
return this.currentUnits().find((unit) => unit.id === unitId)?.name ?? unitId;
|
|
}
|
|
|
|
private currentUnits() {
|
|
if (this.campaign?.roster.length) {
|
|
return this.campaign.roster;
|
|
}
|
|
return this.report?.units ?? [];
|
|
}
|
|
|
|
private currentBonds() {
|
|
if (this.campaign?.bonds.length) {
|
|
return this.campaign.bonds;
|
|
}
|
|
return this.report?.bonds ?? [];
|
|
}
|
|
|
|
private sortieAllies() {
|
|
const scenario = this.nextSortieScenario();
|
|
const rule = this.nextSortieRule(scenario);
|
|
return this.currentUnits().filter((unit) => this.sortieUnitAvailability(unit, scenario, rule).available);
|
|
}
|
|
|
|
private sortieRosterUnits() {
|
|
const campaignOrder = new Map(this.currentUnits().map((unit, index) => [unit.id, index]));
|
|
return this.currentUnits()
|
|
.filter((unit) => unit.faction === 'ally')
|
|
.sort((a, b) => (campaignOrder.get(a.id) ?? 0) - (campaignOrder.get(b.id) ?? 0));
|
|
}
|
|
|
|
private sortieUnitAvailability(
|
|
unit: UnitData,
|
|
scenario = this.nextSortieScenario(),
|
|
rule = this.nextSortieRule(scenario)
|
|
): SortieUnitAvailability {
|
|
const hasBattle = Boolean(scenario);
|
|
if (unit.faction !== 'ally') {
|
|
return { available: false, label: '적', reason: '아군 명단에 포함되지 않습니다.', tone: 'disabled' };
|
|
}
|
|
if (unit.hp <= 0) {
|
|
return { available: false, label: '부상', reason: `병력이 0이라 ${hasBattle ? '출전' : '동행'}할 수 없습니다. 보급 화면에서 회복이 필요합니다.`, tone: 'disabled' };
|
|
}
|
|
const excludedUnitIds = new Set(rule.excludedUnitIds ?? []);
|
|
if (excludedUnitIds.has(unit.id)) {
|
|
return { available: false, label: '제외', reason: `이번 장면의 지휘 조건상 ${hasBattle ? '출전' : '동행'} 후보에서 제외됩니다.`, tone: 'disabled' };
|
|
}
|
|
const scenarioAllyIds = scenario
|
|
? new Set(scenario.units.filter((candidate) => candidate.faction === 'ally').map((candidate) => candidate.id))
|
|
: undefined;
|
|
if (scenarioAllyIds && !scenarioAllyIds.has(unit.id)) {
|
|
return { available: false, label: '후방', reason: '이번 전투의 시작 배치 후보가 아닙니다.', tone: 'disabled' };
|
|
}
|
|
if (this.isSortieSelected(unit.id)) {
|
|
return {
|
|
available: true,
|
|
label: hasBattle ? '출전' : '동행',
|
|
reason: hasBattle ? '출전 슬롯에 배치되었습니다.' : '다음 장면 동행 명단에 포함되었습니다.',
|
|
tone: 'selected'
|
|
};
|
|
}
|
|
if (this.sortieRecommendation(unit.id, scenario)) {
|
|
return { available: true, label: '추천', reason: this.sortieRecommendation(unit.id, scenario)?.reason ?? (hasBattle ? '이번 전투 추천 무장입니다.' : '이번 장면 추천 동행입니다.'), tone: 'recommended' };
|
|
}
|
|
return { available: true, label: '대기', reason: hasBattle ? '출전 가능한 후보입니다.' : '동행 가능한 후보입니다.', tone: 'reserve' };
|
|
}
|
|
|
|
private selectedSortieUnits() {
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
return this.sortieAllies().filter((unit) => selected.has(unit.id));
|
|
}
|
|
|
|
private normalizedSortieItemAssignments(assignments?: CampaignSortieItemAssignments) {
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
const validSupplyIds = new Set(campSupplies.map((supply) => supply.usableId));
|
|
const remainingBySupply = new Map(campSupplies.map((supply) => [supply.usableId, this.inventoryAmount(supply.label)]));
|
|
|
|
return Object.entries(assignments ?? {}).reduce<CampaignSortieItemAssignments>((next, [unitId, stocks]) => {
|
|
if (!selected.has(unitId)) {
|
|
return next;
|
|
}
|
|
|
|
const unitStocks = Object.entries(stocks ?? {}).reduce<Record<string, number>>((nextStocks, [usableId, amount]) => {
|
|
if (!validSupplyIds.has(usableId)) {
|
|
return nextStocks;
|
|
}
|
|
const remaining = remainingBySupply.get(usableId) ?? 0;
|
|
const maxPerUnit = this.sortieSupplyMaxPerUnit(usableId);
|
|
const normalizedAmount = Math.min(maxPerUnit, remaining, Math.max(0, Math.floor(Number(amount))));
|
|
if (normalizedAmount > 0) {
|
|
nextStocks[usableId] = normalizedAmount;
|
|
remainingBySupply.set(usableId, remaining - normalizedAmount);
|
|
}
|
|
return nextStocks;
|
|
}, {});
|
|
|
|
if (Object.keys(unitStocks).length > 0) {
|
|
next[unitId] = unitStocks;
|
|
}
|
|
return next;
|
|
}, {});
|
|
}
|
|
|
|
private normalizedSortieUnitIds(candidateIds?: string[]) {
|
|
const allies = this.sortieAllies();
|
|
const alliesById = new Map(allies.map((unit) => [unit.id, unit]));
|
|
const allyIds = new Set(allies.map((unit) => unit.id));
|
|
const maxUnits = this.sortieMaxUnits();
|
|
const useDefaultSelection = !candidateIds || candidateIds.length === 0;
|
|
const candidateSet = new Set((candidateIds ?? []).filter((id) => allyIds.has(id)));
|
|
const requiredSortieUnitIds = this.requiredSortieUnitIdsFor();
|
|
requiredSortieUnitIds.forEach((id) => {
|
|
if (allyIds.has(id)) {
|
|
candidateSet.add(id);
|
|
}
|
|
});
|
|
|
|
const addDefaultCandidate = (unitId: string) => {
|
|
if (allyIds.has(unitId)) {
|
|
candidateSet.add(unitId);
|
|
}
|
|
};
|
|
if (useDefaultSelection || candidateSet.size < maxUnits) {
|
|
const rule = this.nextSortieRule();
|
|
rule.recommended.forEach((entry) => addDefaultCandidate(entry.unitId));
|
|
(rule.recommendedClasses ?? []).forEach((entry) => {
|
|
allies.filter((unit) => entry.classKeys.includes(unit.classKey)).forEach((unit) => addDefaultCandidate(unit.id));
|
|
});
|
|
allies.forEach((unit) => addDefaultCandidate(unit.id));
|
|
}
|
|
|
|
const required = allies.filter((unit) => requiredSortieUnitIds.has(unit.id)).map((unit) => unit.id);
|
|
const optionalSource = useDefaultSelection
|
|
? [...candidateSet].map((unitId) => alliesById.get(unitId)).filter((unit): unit is UnitData => Boolean(unit))
|
|
: allies;
|
|
const optional = optionalSource
|
|
.filter((unit) => !requiredSortieUnitIds.has(unit.id) && candidateSet.has(unit.id))
|
|
.slice(0, Math.max(0, maxUnits - required.length))
|
|
.map((unit) => unit.id);
|
|
|
|
return [...new Set([...required, ...optional])].slice(0, maxUnits);
|
|
}
|
|
|
|
private normalizedSortieFormationAssignments(assignments?: Record<string, unknown>) {
|
|
return normalizeSortieFormationAssignments(assignments, new Set(this.sortieAllies().map((unit) => unit.id)));
|
|
}
|
|
|
|
private cloneSortieItemAssignments(assignments: CampaignSortieItemAssignments) {
|
|
return Object.fromEntries(
|
|
Object.entries(assignments).map(([unitId, stocks]) => [unitId, { ...stocks }])
|
|
) as CampaignSortieItemAssignments;
|
|
}
|
|
|
|
private isSortieSelected(unitId: string) {
|
|
return this.selectedSortieUnitIds.includes(unitId);
|
|
}
|
|
|
|
private toggleSortieUnit(unitId: string) {
|
|
const unit = this.sortieRosterUnits().find((candidate) => candidate.id === unitId);
|
|
if (!unit) {
|
|
return;
|
|
}
|
|
this.sortieFocusedUnitId = unitId;
|
|
const availability = this.sortieUnitAvailability(unit);
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
if (!availability.available) {
|
|
this.showCampNotice(`${unit.name}: ${availability.reason}`);
|
|
return;
|
|
}
|
|
if (this.isRequiredSortieUnit(unitId)) {
|
|
this.showCampNotice(`${unit.name}는 반드시 ${hasBattle ? '출전' : '동행'}해야 합니다.`);
|
|
return;
|
|
}
|
|
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
if (selected.has(unitId)) {
|
|
selected.delete(unitId);
|
|
const nextAssignments = { ...this.sortieFormationAssignments };
|
|
delete nextAssignments[unitId];
|
|
this.sortieFormationAssignments = nextAssignments;
|
|
const nextItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
|
delete nextItemAssignments[unitId];
|
|
this.sortieItemAssignments = nextItemAssignments;
|
|
} else if (selected.size >= this.sortieMaxUnits()) {
|
|
this.showCampNotice(`${hasBattle ? '이번 전투' : '이번 장면'}는 최대 ${this.sortieMaxUnits()}명까지 ${hasBattle ? '출전' : '동행'}할 수 있습니다.`);
|
|
return;
|
|
} else {
|
|
selected.add(unitId);
|
|
}
|
|
|
|
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(Array.from(selected));
|
|
const selectionLabel = hasBattle ? '출전' : '동행';
|
|
this.sortiePlanFeedback = selected.has(unitId)
|
|
? `${unit.name} ${selectionLabel} 추가 · 직접 편성 중`
|
|
: `${unit.name} ${selectionLabel} 해제 · 직접 편성 중`;
|
|
this.persistSortieSelection();
|
|
soundDirector.playSelect();
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private ensureSortieSelectionSaved() {
|
|
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.selectedSortieUnitIds);
|
|
const selected = new Set(this.selectedSortieUnitIds);
|
|
const required = [...this.requiredSortieUnitIdsFor()].filter((id) => this.sortieAllies().some((unit) => unit.id === id));
|
|
if (!required.every((id) => selected.has(id)) || this.selectedSortieUnitIds.length === 0) {
|
|
return false;
|
|
}
|
|
this.persistSortieSelection();
|
|
return true;
|
|
}
|
|
|
|
private persistSortieSelection() {
|
|
const campaign = this.campaign ?? getCampaignState();
|
|
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.sortieFormationAssignments);
|
|
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.sortieItemAssignments);
|
|
campaign.selectedSortieUnitIds = [...this.selectedSortieUnitIds];
|
|
campaign.sortieFormationAssignments = { ...this.sortieFormationAssignments };
|
|
campaign.sortieItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
|
this.campaign = saveCampaignState(campaign);
|
|
}
|
|
|
|
private sortieSupplyMaxPerUnit(usableId: string) {
|
|
return usableId === 'bean' ? 2 : 1;
|
|
}
|
|
|
|
private sortieAssignedSupplyCount(unitId: string, usableId: string) {
|
|
return this.sortieItemAssignments[unitId]?.[usableId] ?? 0;
|
|
}
|
|
|
|
private totalAssignedSupplyCount(usableId: string) {
|
|
return Object.values(this.sortieItemAssignments).reduce((sum, stocks) => sum + (stocks[usableId] ?? 0), 0);
|
|
}
|
|
|
|
private sortieAvailableSupplyForUnit(unitId: string, supply: CampSupplyDefinition) {
|
|
const total = this.inventoryAmount(supply.label);
|
|
const assignedElsewhere = this.totalAssignedSupplyCount(supply.usableId) - this.sortieAssignedSupplyCount(unitId, supply.usableId);
|
|
return Math.max(0, total - assignedElsewhere);
|
|
}
|
|
|
|
private toggleSortieSupplyAssignment(unitId: string, usableId: string) {
|
|
const unit = this.sortieFocusedUnit();
|
|
const supply = campSupplies.find((candidate) => candidate.usableId === usableId);
|
|
if (!supply || !unit || unit.id !== unitId) {
|
|
return;
|
|
}
|
|
if (!this.isSortieSelected(unitId)) {
|
|
const hasBattle = Boolean(this.nextSortieScenario());
|
|
this.showCampNotice(`${unit.name}는 ${hasBattle ? '출전' : '동행'} 등록 후 보급을 배정할 수 있습니다.`);
|
|
return;
|
|
}
|
|
|
|
const current = this.sortieAssignedSupplyCount(unitId, usableId);
|
|
const maxAssignable = Math.min(this.sortieSupplyMaxPerUnit(usableId), this.sortieAvailableSupplyForUnit(unitId, supply));
|
|
if (current <= 0 && maxAssignable <= 0) {
|
|
this.showCampNotice(`${supply.title} 보유량이 부족합니다.`);
|
|
return;
|
|
}
|
|
|
|
const nextCount = current >= maxAssignable ? 0 : current + 1;
|
|
const nextAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
|
nextAssignments[unitId] = { ...(nextAssignments[unitId] ?? {}) };
|
|
if (nextCount > 0) {
|
|
nextAssignments[unitId][usableId] = nextCount;
|
|
} else {
|
|
delete nextAssignments[unitId][usableId];
|
|
}
|
|
if (Object.keys(nextAssignments[unitId]).length === 0) {
|
|
delete nextAssignments[unitId];
|
|
}
|
|
|
|
this.sortieItemAssignments = this.normalizedSortieItemAssignments(nextAssignments);
|
|
this.persistSortieSelection();
|
|
soundDirector.playSelect();
|
|
this.showCampNotice(nextCount > 0 ? `${unit.name}에게 ${supply.title} ${nextCount}개 배정` : `${unit.name}의 ${supply.title} 배정을 해제했습니다.`);
|
|
this.showSortiePrep();
|
|
}
|
|
|
|
private completedCampDialogues() {
|
|
if (this.campaign) {
|
|
return this.campaign.completedCampDialogues;
|
|
}
|
|
return this.report?.completedCampDialogues ?? [];
|
|
}
|
|
|
|
private completedCampVisits() {
|
|
if (this.campaign) {
|
|
return this.campaign.completedCampVisits;
|
|
}
|
|
return this.report?.completedCampVisits ?? [];
|
|
}
|
|
|
|
private inventoryLabels() {
|
|
const inventory = this.campaign?.inventory ?? {};
|
|
const entries = Object.entries(inventory).filter(([, amount]) => amount > 0);
|
|
if (entries.length > 0) {
|
|
return entries.map(([label, amount]) => `${label} ${amount}`);
|
|
}
|
|
return this.report?.itemRewards.length ? this.report.itemRewards : ['콩 1', '탁주 1'];
|
|
}
|
|
|
|
private inventoryAmount(label: string) {
|
|
return this.campaign?.inventory[label] ?? 0;
|
|
}
|
|
|
|
private equipmentInventoryEntries(slot?: EquipmentSlot) {
|
|
return Object.entries(this.campaign?.inventory ?? {})
|
|
.map(([label, amount]) => ({ label, amount, item: findItemByName(label) }))
|
|
.filter((entry): entry is { label: string; amount: number; item: ItemDefinition } => {
|
|
const item = entry.item;
|
|
return entry.amount > 0 && item !== undefined && (!slot || item.slot === slot);
|
|
})
|
|
.sort((a, b) => {
|
|
if (a.item.slot !== b.item.slot) {
|
|
return equipmentSlots.indexOf(a.item.slot) - equipmentSlots.indexOf(b.item.slot);
|
|
}
|
|
const bonusDelta = this.itemTotalBonus(b.item) - this.itemTotalBonus(a.item);
|
|
if (bonusDelta !== 0) {
|
|
return bonusDelta;
|
|
}
|
|
return a.item.name.localeCompare(b.item.name, 'ko-KR');
|
|
});
|
|
}
|
|
|
|
private nonEquipmentInventoryLabels() {
|
|
return Object.entries(this.campaign?.inventory ?? {})
|
|
.filter(([label, amount]) => amount > 0 && !campSupplyByLabel.has(label) && !findItemByName(label))
|
|
.map(([label, amount]) => `${label} ${amount}`);
|
|
}
|
|
|
|
private swapUnitEquipment(unitId: string, slot: EquipmentSlot, itemId?: string, returnToSortie = false) {
|
|
const campaign = this.campaign ?? getCampaignState();
|
|
const target = campaign.roster.find((unit) => unit.id === unitId);
|
|
if (!target) {
|
|
this.showCampNotice('장비를 바꿀 장수 장부를 찾지 못했습니다.');
|
|
return;
|
|
}
|
|
|
|
const currentState = target.equipment[slot];
|
|
const currentItem = getItem(currentState.itemId);
|
|
const nextItem = itemId ? getItem(itemId) : this.nextEquipmentItemForSlot(slot, currentItem.id);
|
|
if (!nextItem || nextItem.slot !== slot || nextItem.id === currentItem.id) {
|
|
this.showCampNotice(`${equipmentSlotLabels[slot]} 교체 가능한 장비가 없습니다.`);
|
|
return;
|
|
}
|
|
|
|
if ((campaign.inventory[nextItem.name] ?? 0) <= 0) {
|
|
this.showCampNotice(`${nextItem.name} 보유량이 없습니다.`);
|
|
return;
|
|
}
|
|
|
|
campaign.inventory[nextItem.name] -= 1;
|
|
if (campaign.inventory[nextItem.name] <= 0) {
|
|
delete campaign.inventory[nextItem.name];
|
|
}
|
|
campaign.inventory[currentItem.name] = (campaign.inventory[currentItem.name] ?? 0) + 1;
|
|
target.equipment[slot] = {
|
|
itemId: nextItem.id,
|
|
level: currentState.level,
|
|
exp: currentState.exp
|
|
};
|
|
this.syncReportUnitEquipment(campaign, target.id, target.equipment);
|
|
|
|
this.campaign = saveCampaignState(campaign);
|
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
|
soundDirector.playSelect();
|
|
this.showCampNotice(`${target.name} ${equipmentSlotLabels[slot]} 교체 · ${currentItem.name} → ${nextItem.name} (${this.itemBonusText(nextItem)})`);
|
|
if (returnToSortie || this.sortieObjects.length > 0) {
|
|
this.showSortiePrep();
|
|
} else {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
private nextEquipmentItemForSlot(slot: EquipmentSlot, currentItemId: string) {
|
|
return this.equipmentInventoryEntries(slot).find((entry) => entry.item.id !== currentItemId)?.item;
|
|
}
|
|
|
|
private syncReportUnitEquipment(campaign: CampaignState, unitId: string, equipment: UnitData['equipment']) {
|
|
const reportUnit = campaign.firstBattleReport?.units.find((unit) => unit.id === unitId);
|
|
if (reportUnit) {
|
|
reportUnit.equipment = JSON.parse(JSON.stringify(equipment)) as UnitData['equipment'];
|
|
}
|
|
}
|
|
|
|
private statWithBonus(base: number, bonus: number) {
|
|
return bonus > 0 ? `${base}+${bonus}` : `${base}`;
|
|
}
|
|
|
|
private itemTotalBonus(item: ItemDefinition) {
|
|
return (item.attackBonus ?? 0) + (item.defenseBonus ?? 0) + (item.strategyBonus ?? 0);
|
|
}
|
|
|
|
private equipmentAttackBonus(unit: UnitData) {
|
|
return equipmentSlots.reduce((sum, slot) => sum + (getItem(unit.equipment[slot].itemId).attackBonus ?? 0), 0);
|
|
}
|
|
|
|
private equipmentStrategyBonus(unit: UnitData) {
|
|
return equipmentSlots.reduce((sum, slot) => sum + (getItem(unit.equipment[slot].itemId).strategyBonus ?? 0), 0);
|
|
}
|
|
|
|
private equipmentDefenseBonus(unit: UnitData) {
|
|
return equipmentSlots.reduce((sum, slot) => sum + (getItem(unit.equipment[slot].itemId).defenseBonus ?? 0), 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);
|
|
const fill = this.track(this.add.rectangle(x, y, Math.max(2, width * Phaser.Math.Clamp(ratio, 0, 1)), height, color, 0.96));
|
|
fill.setOrigin(0);
|
|
}
|
|
|
|
private textStyle(fontSize: number, color: string, bold = false): Phaser.Types.GameObjects.Text.TextStyle {
|
|
return {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: `${fontSize}px`,
|
|
color,
|
|
fontStyle: bold ? '700' : '400'
|
|
};
|
|
}
|
|
|
|
private track<T extends Phaser.GameObjects.GameObject>(object: T) {
|
|
this.contentObjects.push(object);
|
|
return object;
|
|
}
|
|
|
|
private compactText(value: string, maxLength: number) {
|
|
if (value.length <= maxLength) {
|
|
return value;
|
|
}
|
|
return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
}
|
|
|
|
private clearContent() {
|
|
this.contentObjects.forEach((object) => object.destroy());
|
|
this.contentObjects = [];
|
|
}
|
|
|
|
private sortieDeploymentPreviewDebug() {
|
|
const scenario = this.nextSortieScenario();
|
|
if (!scenario) {
|
|
return [];
|
|
}
|
|
return [...this.sortieDeploymentPlanForScenario(scenario).values()];
|
|
}
|
|
|
|
getDebugState() {
|
|
const sortieChecklist = this.sortieChecklist();
|
|
const sortieScenario = this.nextSortieScenario();
|
|
return {
|
|
scene: this.scene.key,
|
|
activeTab: this.activeTab,
|
|
sortieVisible: this.sortieObjects.length > 0,
|
|
sortieHasBattle: Boolean(sortieScenario),
|
|
nextSortieBattleId: sortieScenario?.id ?? null,
|
|
nextSortieBattleTitle: sortieScenario?.title ?? null,
|
|
sortieRecommendationEnabled: this.canApplyRecommendedSortiePlan(sortieScenario),
|
|
selectedUnitId: this.selectedUnitId,
|
|
selectedDialogueId: this.selectedDialogueId,
|
|
selectedVisitId: this.selectedVisitId,
|
|
selectedSortieUnitIds: [...this.selectedSortieUnitIds],
|
|
sortieFormationAssignments: { ...this.sortieFormationAssignments },
|
|
reserveTrainingAssignments: { ...(this.campaign?.reserveTrainingAssignments ?? {}) },
|
|
sortieItemAssignments: this.cloneSortieItemAssignments(this.sortieItemAssignments),
|
|
sortieDeploymentPreview: this.sortieDeploymentPreviewDebug(),
|
|
sortieFocusedUnitId: this.sortieFocusedUnitId,
|
|
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
|
|
sortiePlanFeedback: this.sortiePlanFeedback,
|
|
sortieChecklist,
|
|
sortieNextAction: this.sortiePrepSubtitle(sortieChecklist),
|
|
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
|
|
reserveTrainingFocusOptions: campaignReserveTrainingFocusDefinitions.map((focus) => ({ ...focus })),
|
|
sortieRosterView: this.sortieRosterViewState(this.sortieAllies().length),
|
|
sortieRoster: this.sortieAllies().map((unit) => {
|
|
const summary = this.sortieUnitTacticalSummary(unit);
|
|
const reserveTrainingFocus = this.reserveTrainingFocusDefinitionForUnit(unit.id);
|
|
return {
|
|
id: unit.id,
|
|
name: unit.name,
|
|
selected: this.isSortieSelected(unit.id),
|
|
recruited: !foundingSortieUnitIds.has(unit.id),
|
|
required: this.isRequiredSortieUnit(unit.id),
|
|
className: unit.className,
|
|
classRole: getUnitClass(unit.classKey).role,
|
|
formationRole: this.sortieFormationRole(unit),
|
|
reserveTrainingAssigned: Boolean(this.campaign?.reserveTrainingAssignments[unit.id]),
|
|
reserveTrainingFocusId: reserveTrainingFocus.id,
|
|
reserveTrainingFocusLabel: reserveTrainingFocus.label,
|
|
recommended: Boolean(this.sortieRecommendation(unit.id)),
|
|
recommendationReason: this.sortieRecommendation(unit.id)?.reason ?? null,
|
|
statLine: summary.statLine,
|
|
equipmentLine: summary.equipmentLine,
|
|
bondLine: summary.bondLine,
|
|
terrainLine: summary.terrainLine
|
|
};
|
|
}),
|
|
equipmentInventory: this.equipmentInventoryEntries().map((entry) => ({
|
|
label: entry.label,
|
|
amount: entry.amount,
|
|
itemId: entry.item.id,
|
|
slot: entry.item.slot,
|
|
rank: entry.item.rank,
|
|
bonusText: this.itemBonusText(entry.item)
|
|
})),
|
|
sortiePlan: this.sortiePlanSummary(),
|
|
rosterCollection: this.rosterCollectionSummary(),
|
|
reserveTrainingAwards: this.latestReserveTrainingAwards(),
|
|
campaignProgress: this.campaignTimelineProgress(),
|
|
campBattleId: this.currentCampBattleId(),
|
|
campTitle: this.currentCampTitle(),
|
|
openSortiePrepOnCreate: this.openSortiePrepOnCreate,
|
|
skipIntroStoryForSortie: this.skipIntroStoryForSortie,
|
|
availableDialogueIds: this.availableCampDialogues().map((dialogue) => dialogue.id),
|
|
completedAvailableDialogues: this.completedAvailableDialogues().map((dialogue) => dialogue.id),
|
|
availableVisitIds: this.availableCampVisits().map((visit) => visit.id),
|
|
completedAvailableVisits: this.completedAvailableVisits().map((visit) => visit.id),
|
|
campaign: this.campaign
|
|
? {
|
|
step: this.campaign.step,
|
|
gold: this.campaign.gold,
|
|
inventory: this.campaign.inventory,
|
|
selectedSortieUnitIds: this.campaign.selectedSortieUnitIds,
|
|
sortieFormationAssignments: this.campaign.sortieFormationAssignments,
|
|
reserveTrainingAssignments: this.campaign.reserveTrainingAssignments,
|
|
reserveTrainingFocus: this.campaign.reserveTrainingFocus,
|
|
roster: this.campaign.roster.map((unit) => ({
|
|
id: unit.id,
|
|
name: unit.name,
|
|
hp: unit.hp,
|
|
maxHp: unit.maxHp
|
|
})),
|
|
completedCampDialogues: this.campaign.completedCampDialogues,
|
|
completedCampVisits: this.campaign.completedCampVisits
|
|
}
|
|
: null,
|
|
report: this.report
|
|
? {
|
|
rewardGold: this.report.rewardGold,
|
|
objectives: this.report.objectives,
|
|
campaignRewards: this.report.campaignRewards,
|
|
completedCampDialogues: this.report.completedCampDialogues,
|
|
completedCampVisits: this.report.completedCampVisits,
|
|
bonds: this.report.bonds.map((bond) => ({ id: bond.id, level: bond.level, exp: bond.exp, battleExp: bond.battleExp }))
|
|
}
|
|
: null
|
|
};
|
|
}
|
|
}
|