Add Xiapi final sortie chapter
This commit is contained in:
@@ -97,10 +97,15 @@ type SortiePlanSummary = {
|
||||
terrainScore: number;
|
||||
terrainGrade: string;
|
||||
activeBondCount: number;
|
||||
recruitedCount: number;
|
||||
selectedRecruitedCount: number;
|
||||
reserveCount: number;
|
||||
recommendedSelectedCount: number;
|
||||
recommendedTotal: number;
|
||||
deploymentLine: string;
|
||||
recommendationLine: string;
|
||||
recruitedLine: string;
|
||||
reserveLine: string;
|
||||
objectiveLine: string;
|
||||
formationSlots: SortieFormationSlot[];
|
||||
warnings: string[];
|
||||
@@ -177,10 +182,12 @@ const campBattleIds = {
|
||||
ninth: 'ninth-battle-xuzhou-gate-night-raid',
|
||||
tenth: 'tenth-battle-xuzhou-breakout',
|
||||
eleventh: 'eleventh-battle-xudu-refuge-road',
|
||||
twelfth: 'twelfth-battle-xiapi-outer-siege'
|
||||
twelfth: 'twelfth-battle-xiapi-outer-siege',
|
||||
thirteenth: 'thirteenth-battle-xiapi-final'
|
||||
} as const;
|
||||
|
||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||
const foundingSortieUnitIds = new Set(['liu-bei', 'guan-yu', 'zhang-fei']);
|
||||
const maxSortieUnits = 6;
|
||||
const sortieFormationSlotDefinitions: Omit<SortieFormationSlot, 'unitNames'>[] = [
|
||||
{ role: 'front', label: '전열', description: '길목을 막고 반격을 받아냄' },
|
||||
@@ -299,6 +306,16 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
|
||||
{ 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: '여포 처분과 조조의 시선을 둘러싼 명분 조율에 필요합니다.' }
|
||||
],
|
||||
note: '하비 결전은 전열과 돌파가 강해야 하지만, 전후 조조와의 긴장을 생각하면 책사 한 명을 챙기는 편성이 좋습니다.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1328,6 +1345,87 @@ const campDialogues: CampDialogue[] = [
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1412,7 +1510,8 @@ export class CampScene extends Phaser.Scene {
|
||||
battleId !== campBattleIds.ninth &&
|
||||
battleId !== campBattleIds.tenth &&
|
||||
battleId !== campBattleIds.eleventh &&
|
||||
battleId !== campBattleIds.twelfth) ||
|
||||
battleId !== campBattleIds.twelfth &&
|
||||
battleId !== campBattleIds.thirteenth) ||
|
||||
!this.campaign
|
||||
) {
|
||||
return;
|
||||
@@ -1434,6 +1533,9 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private currentCampTitle() {
|
||||
const battleId = this.currentCampBattleId();
|
||||
if (battleId === campBattleIds.thirteenth) {
|
||||
return '하비 결전 후 군영';
|
||||
}
|
||||
if (battleId === campBattleIds.twelfth) {
|
||||
return '하비 포위 군영';
|
||||
}
|
||||
@@ -1682,6 +1784,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const rowY = y + 50 + index * rowGap;
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const required = requiredSortieUnitIds.has(unit.id);
|
||||
const recruited = !foundingSortieUnitIds.has(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82));
|
||||
@@ -1692,7 +1795,7 @@ export class CampScene extends Phaser.Scene {
|
||||
row.on('pointerdown', () => this.toggleSortieUnit(unit.id));
|
||||
|
||||
const marker = selected ? '출전' : '대기';
|
||||
const markerLabel = required ? '필수' : recommendation ? '추천' : marker;
|
||||
const markerLabel = required ? '필수' : recommendation ? '추천' : recruited ? '합류' : marker;
|
||||
this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(12, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(x + 84, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(x + 214, rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, 19), this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
||||
@@ -1724,17 +1827,18 @@ export class CampScene extends Phaser.Scene {
|
||||
const summaryLines = [
|
||||
plan.deploymentLine,
|
||||
`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개`,
|
||||
plan.recruitedLine,
|
||||
plan.objectiveLine
|
||||
];
|
||||
summaryLines.forEach((line, index) => {
|
||||
this.trackSortie(this.add.text(x + 18, y + 150 + index * 18, this.compactText(line, 31), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 146 + index * 16, this.compactText(line, 32), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
|
||||
});
|
||||
|
||||
plan.formationSlots.forEach((slot, index) => {
|
||||
const column = index % 2;
|
||||
const row = Math.floor(index / 2);
|
||||
const rowX = x + 18 + column * 176;
|
||||
const rowY = y + 208 + row * 28;
|
||||
const rowY = y + 218 + row * 28;
|
||||
const names = slot.unitNames.length > 0 ? slot.unitNames.join(', ') : '미배치';
|
||||
const color = slot.unitNames.length > 0 ? '#a8ffd0' : '#9fb0bf';
|
||||
this.trackSortie(this.add.text(rowX, rowY, slot.label, this.textStyle(12, color, true))).setDepth(depth + 1);
|
||||
@@ -1742,7 +1846,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.trackSortie(this.add.text(rowX + 42, rowY + 13, this.compactText(slot.description, 14), this.textStyle(10, '#87919c'))).setDepth(depth + 1);
|
||||
});
|
||||
|
||||
const warning = plan.warnings[0] ?? '전열, 돌파, 지원의 균형이 좋습니다.';
|
||||
const warning = plan.warnings[0] ?? plan.reserveLine;
|
||||
this.trackSortie(this.add.text(x + 18, y + height - 28, this.compactText(`조언: ${warning}`, 42), this.textStyle(12, plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true))).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
@@ -1914,13 +2018,18 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
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 availableIds = new Set(this.sortieAllies().map((unit) => unit.id));
|
||||
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 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 terrainScores = selectedUnits.map((unit) => this.sortieTerrainScore(unit, scenario));
|
||||
const terrainScore = terrainScores.length
|
||||
? Math.round(terrainScores.reduce((sum, score) => sum + score, 0) / terrainScores.length)
|
||||
@@ -1970,10 +2079,18 @@ export class CampScene extends Phaser.Scene {
|
||||
terrainScore,
|
||||
terrainGrade,
|
||||
activeBondCount,
|
||||
recruitedCount: recruitedUnits.length,
|
||||
selectedRecruitedCount,
|
||||
reserveCount: reserveUnits.length,
|
||||
recommendedSelectedCount,
|
||||
recommendedTotal: recommended.length,
|
||||
deploymentLine: `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support} · 예비 ${roleCounts.reserve}`,
|
||||
recommendationLine: recommended.length > 0 ? `추천 ${recommendedSelectedCount}/${recommended.length}` : '추천 없음',
|
||||
recruitedLine:
|
||||
recruitedUnits.length > 0
|
||||
? `합류 무장 ${selectedRecruitedCount}/${recruitedUnits.length} · 대기 ${reserveUnits.length}`
|
||||
: '합류 무장 없음',
|
||||
reserveLine: reserveUnits.length > 0 ? `대기 무장: ${reserveNames}` : '전원 출전 중입니다.',
|
||||
objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : '다음 전투 정보 없음',
|
||||
formationSlots,
|
||||
warnings
|
||||
@@ -2903,6 +3020,8 @@ export class CampScene extends Phaser.Scene {
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
selected: this.isSortieSelected(unit.id),
|
||||
recruited: !foundingSortieUnitIds.has(unit.id),
|
||||
required: requiredSortieUnitIds.has(unit.id),
|
||||
className: unit.className,
|
||||
classRole: getUnitClass(unit.classKey).role,
|
||||
formationRole: this.sortieFormationRole(unit),
|
||||
|
||||
Reference in New Issue
Block a user