Add Fan Castle vanguard sortie chapter
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
hanzhongMainRecruitUnits,
|
||||
hanzhongScoutRecruitBonds,
|
||||
hanzhongScoutRecruitUnits,
|
||||
fanCastleBonds,
|
||||
jingDefenseBonds,
|
||||
jingzhouRecruitBonds,
|
||||
jingzhouRecruitUnits,
|
||||
@@ -142,6 +143,7 @@ type SortieRecommendation = {
|
||||
|
||||
type SortieRuleDefinition = {
|
||||
maxUnits: number;
|
||||
requiredUnitIds?: string[];
|
||||
recommended: SortieRecommendation[];
|
||||
note: string;
|
||||
};
|
||||
@@ -350,7 +352,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
||||
'thirty-fifth-battle-yangping-scout',
|
||||
'thirty-sixth-battle-dingjun-vanguard',
|
||||
'thirty-seventh-battle-hanzhong-decisive',
|
||||
'thirty-eighth-battle-jing-defense'
|
||||
'thirty-eighth-battle-jing-defense',
|
||||
'thirty-ninth-battle-fan-castle-vanguard'
|
||||
],
|
||||
nextHints: ['왕업 선언', '오호대장군', '북벌 준비']
|
||||
}
|
||||
@@ -394,10 +397,11 @@ const campBattleIds = {
|
||||
thirtyFifth: 'thirty-fifth-battle-yangping-scout',
|
||||
thirtySixth: 'thirty-sixth-battle-dingjun-vanguard',
|
||||
thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive',
|
||||
thirtyEighth: 'thirty-eighth-battle-jing-defense'
|
||||
thirtyEighth: 'thirty-eighth-battle-jing-defense',
|
||||
thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard'
|
||||
} as const;
|
||||
|
||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||
const defaultRequiredSortieUnitIds = ['liu-bei'];
|
||||
const foundingSortieUnitIds = new Set(['liu-bei', 'guan-yu', 'zhang-fei']);
|
||||
const maxSortieUnits = 6;
|
||||
const sortieFormationSlotDefinitions: Omit<SortieFormationSlot, 'unitNames'>[] = [
|
||||
@@ -409,6 +413,7 @@ const sortieFormationSlotDefinitions: Omit<SortieFormationSlot, 'unitNames'>[] =
|
||||
|
||||
const defaultSortieRule: SortieRuleDefinition = {
|
||||
maxUnits: maxSortieUnits,
|
||||
requiredUnitIds: defaultRequiredSortieUnitIds,
|
||||
recommended: [
|
||||
{ unitId: 'liu-bei', reason: '패배 조건인 주장이며 공명 중심입니다.' }
|
||||
],
|
||||
@@ -817,6 +822,7 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
|
||||
},
|
||||
'thirty-eighth-battle-jing-defense': {
|
||||
maxUnits: 7,
|
||||
requiredUnitIds: ['liu-bei', 'guan-yu'],
|
||||
recommended: [
|
||||
{ unitId: 'liu-bei', reason: '촉한의 이름을 세운 뒤 형주 방위 명분을 직접 확인합니다.' },
|
||||
{ unitId: 'guan-yu', reason: '형주 방위 전초전의 핵심 장수이며 생존 목표입니다.' },
|
||||
@@ -827,6 +833,20 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
|
||||
{ unitId: 'zhao-yun', reason: '강변 기동대와 기병 대응을 안정적으로 맡습니다.' }
|
||||
],
|
||||
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: '형주 민심과 보급선을 안정시키며 관우 전선을 보조합니다.' }
|
||||
],
|
||||
note: '번성 외곽 압박전은 스물두 명 중 일곱 명만 출전합니다. 유비와 관우는 필수 출전하며, 나머지 다섯 자리에 기동, 원거리, 책략, 보급을 어떻게 배치할지 고르는 전투입니다.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3989,6 +4009,87 @@ const campDialogues: CampDialogue[] = [
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -5398,6 +5499,66 @@ const campVisits: CampVisitDefinition[] = [
|
||||
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']
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -5484,6 +5645,54 @@ export class CampScene extends Phaser.Scene {
|
||||
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(
|
||||
[
|
||||
@@ -6064,6 +6273,9 @@ export class CampScene extends Phaser.Scene {
|
||||
if (this.campaign?.step === 'hanzhong-king-camp') {
|
||||
return '한중왕 즉위 준비 군영';
|
||||
}
|
||||
if (battleId === campBattleIds.thirtyNinth) {
|
||||
return '번성 외곽 압박전 후 군영';
|
||||
}
|
||||
if (battleId === campBattleIds.thirtyEighth) {
|
||||
return '형주 방위 전초전 후 군영';
|
||||
}
|
||||
@@ -6412,7 +6624,7 @@ export class CampScene extends Phaser.Scene {
|
||||
allies.forEach((unit, index) => {
|
||||
const rowY = y + (denseRoster ? 48 : 50) + index * rowGap;
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const required = requiredSortieUnitIds.has(unit.id);
|
||||
const required = this.isRequiredSortieUnit(unit.id);
|
||||
const recruited = !foundingSortieUnitIds.has(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
@@ -6601,6 +6813,14 @@ export class CampScene extends Phaser.Scene {
|
||||
return sortieRulesByBattleId[scenario.id] ?? defaultSortieRule;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -7016,7 +7236,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private unitRosterStatus(unit: UnitData) {
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
if (requiredSortieUnitIds.has(unit.id)) {
|
||||
if (this.isRequiredSortieUnit(unit.id)) {
|
||||
return { label: '필수', tone: 'recommended' as const };
|
||||
}
|
||||
if (selected) {
|
||||
@@ -7981,6 +8201,7 @@ export class CampScene extends Phaser.Scene {
|
||||
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);
|
||||
@@ -8009,7 +8230,7 @@ export class CampScene extends Phaser.Scene {
|
||||
if (!unit) {
|
||||
return;
|
||||
}
|
||||
if (requiredSortieUnitIds.has(unitId)) {
|
||||
if (this.isRequiredSortieUnit(unitId)) {
|
||||
this.showCampNotice(`${unit.name}는 반드시 출전해야 합니다.`);
|
||||
return;
|
||||
}
|
||||
@@ -8032,7 +8253,9 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private ensureSortieSelectionSaved() {
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.selectedSortieUnitIds);
|
||||
if (!this.selectedSortieUnitIds.some((id) => requiredSortieUnitIds.has(id)) || this.selectedSortieUnitIds.length === 0) {
|
||||
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();
|
||||
@@ -8138,7 +8361,7 @@ export class CampScene extends Phaser.Scene {
|
||||
name: unit.name,
|
||||
selected: this.isSortieSelected(unit.id),
|
||||
recruited: !foundingSortieUnitIds.has(unit.id),
|
||||
required: requiredSortieUnitIds.has(unit.id),
|
||||
required: this.isRequiredSortieUnit(unit.id),
|
||||
className: unit.className,
|
||||
classRole: getUnitClass(unit.classKey).role,
|
||||
formationRole: this.sortieFormationRole(unit),
|
||||
|
||||
Reference in New Issue
Block a user