Add Liu Biao refuge chapter and Zhao Yun recruit

This commit is contained in:
2026-06-23 04:39:49 +09:00
parent 821cebde0e
commit 8f2479e5fc
11 changed files with 1012 additions and 22 deletions

View File

@@ -27,6 +27,7 @@ const unitTexture: Record<string, string> = {
'liu-bei': 'unit-liu-bei',
'guan-yu': 'unit-guan-yu',
'zhang-fei': 'unit-zhang-fei',
'zhao-yun': 'unit-rebel-cavalry',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer',
@@ -120,7 +121,25 @@ const unitTexture: Record<string, string> = {
'yuan-refuge-guard-b': 'unit-rebel',
'yuan-refuge-guard-c': 'unit-rebel',
'yuan-refuge-strategist-a': 'unit-rebel-archer',
'yuan-refuge-leader-cai-yang': 'unit-rebel-leader'
'yuan-refuge-leader-cai-yang': 'unit-rebel-leader',
'liu-biao-road-scout-a': 'unit-rebel',
'liu-biao-road-scout-b': 'unit-rebel',
'liu-biao-road-infantry-a': 'unit-rebel',
'liu-biao-road-infantry-b': 'unit-rebel',
'liu-biao-road-infantry-c': 'unit-rebel',
'liu-biao-road-infantry-d': 'unit-rebel',
'liu-biao-road-archer-a': 'unit-rebel-archer',
'liu-biao-road-archer-b': 'unit-rebel-archer',
'liu-biao-road-archer-c': 'unit-rebel-archer',
'liu-biao-road-archer-d': 'unit-rebel-archer',
'liu-biao-road-cavalry-a': 'unit-rebel-cavalry',
'liu-biao-road-cavalry-b': 'unit-rebel-cavalry',
'liu-biao-road-cavalry-c': 'unit-rebel-cavalry',
'liu-biao-road-cavalry-d': 'unit-rebel-cavalry',
'liu-biao-road-guard-a': 'unit-rebel',
'liu-biao-road-guard-b': 'unit-rebel',
'liu-biao-road-strategist-a': 'unit-rebel-archer',
'liu-biao-road-leader-cao-ren': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -543,7 +562,8 @@ const initialItemStocks: Record<string, Record<string, number>> = {
'zhang-fei': { bean: 1, wine: 1 },
'jian-yong': { bean: 1 },
'mi-zhu': { bean: 2, salve: 1 },
'sun-qian': { bean: 1, salve: 1 }
'sun-qian': { bean: 1, salve: 1 },
'zhao-yun': { bean: 1, wine: 1 }
};
const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
@@ -689,7 +709,25 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'yuan-refuge-guard-b': 'guard',
'yuan-refuge-guard-c': 'guard',
'yuan-refuge-strategist-a': 'hold',
'yuan-refuge-leader-cai-yang': 'guard'
'yuan-refuge-leader-cai-yang': 'guard',
'liu-biao-road-scout-a': 'aggressive',
'liu-biao-road-scout-b': 'aggressive',
'liu-biao-road-infantry-a': 'guard',
'liu-biao-road-infantry-b': 'guard',
'liu-biao-road-infantry-c': 'guard',
'liu-biao-road-infantry-d': 'guard',
'liu-biao-road-archer-a': 'hold',
'liu-biao-road-archer-b': 'hold',
'liu-biao-road-archer-c': 'hold',
'liu-biao-road-archer-d': 'hold',
'liu-biao-road-cavalry-a': 'aggressive',
'liu-biao-road-cavalry-b': 'aggressive',
'liu-biao-road-cavalry-c': 'aggressive',
'liu-biao-road-cavalry-d': 'aggressive',
'liu-biao-road-guard-a': 'guard',
'liu-biao-road-guard-b': 'guard',
'liu-biao-road-strategist-a': 'hold',
'liu-biao-road-leader-cao-ren': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -10,6 +10,7 @@ import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg';
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
@@ -88,6 +89,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl);
this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl);
this.load.image('battle-map-fifteenth', fifteenthBattleMapUrl);
this.load.image('battle-map-sixteenth', sixteenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -9,6 +9,8 @@ import {
caoBreakRecruitUnits,
firstBattleBonds,
firstBattleUnits,
liuBiaoRecruitBonds,
liuBiaoRecruitUnits,
xuzhouRecruitBonds,
xuzhouRecruitUnits,
type PortraitKey,
@@ -194,7 +196,8 @@ const campBattleIds = {
twelfth: 'twelfth-battle-xiapi-outer-siege',
thirteenth: 'thirteenth-battle-xiapi-final',
fourteenth: 'fourteenth-battle-cao-break',
fifteenth: 'fifteenth-battle-yuan-refuge-road'
fifteenth: 'fifteenth-battle-yuan-refuge-road',
sixteenth: 'sixteenth-battle-liu-biao-refuge'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -349,6 +352,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'sun-qian', 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: '유표 의탁로부터는 합류 무장이 출전 제한보다 많아집니다. 조운의 기동력을 시험할지, 책사와 보급을 두껍게 둘지 직접 고르십시오.'
}
};
@@ -1621,6 +1636,87 @@ const campDialogues: CampDialogue[] = [
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
}
]
}
];
@@ -1703,10 +1799,18 @@ export class CampScene extends Phaser.Scene {
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 ||
battleId === campBattleIds.fifteenth
battleId === campBattleIds.fourteenth
) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits],
@@ -1743,6 +1847,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.sixteenth) {
return '형주 의탁 후 군영';
}
if (battleId === campBattleIds.fifteenth) {
return '원소 의탁 후 군영';
}

View File

@@ -12,6 +12,7 @@ import {
secondBattleScenario,
seventhBattleScenario,
sixthBattleScenario,
sixteenthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
twelfthBattleScenario,
@@ -329,7 +330,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'twelfth-camp' ||
campaign.step === 'thirteenth-camp' ||
campaign.step === 'fourteenth-camp' ||
campaign.step === 'fifteenth-camp'
campaign.step === 'fifteenth-camp' ||
campaign.step === 'sixteenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -410,6 +412,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'sixteenth-battle') {
this.scene.start('BattleScene', { battleId: sixteenthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;