Add Xuzhou breakout chapter

This commit is contained in:
2026-06-23 02:09:38 +09:00
parent 91b8abc7e3
commit 6eacee48fc
11 changed files with 739 additions and 16 deletions

View File

@@ -545,7 +545,18 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'lubu-cavalry-a': 'aggressive',
'lubu-cavalry-b': 'aggressive',
'lubu-cavalry-c': 'aggressive',
'xuzhou-leader-cao-bao': 'guard'
'xuzhou-leader-cao-bao': 'guard',
'xuzhou-escape-cavalry-a': 'aggressive',
'xuzhou-escape-cavalry-b': 'aggressive',
'xuzhou-escape-cavalry-c': 'aggressive',
'xuzhou-escape-guard-a': 'guard',
'xuzhou-escape-guard-b': 'guard',
'xuzhou-escape-guard-c': 'guard',
'xuzhou-escape-archer-a': 'hold',
'xuzhou-escape-archer-b': 'hold',
'xuzhou-escape-raider-a': 'aggressive',
'xuzhou-escape-raider-b': 'aggressive',
'xuzhou-escape-leader-song-xian': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -7,6 +7,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 tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
@@ -76,6 +77,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-seventh', seventhBattleMapUrl);
this.load.image('battle-map-eighth', eighthBattleMapUrl);
this.load.image('battle-map-ninth', ninthBattleMapUrl);
this.load.image('battle-map-tenth', tenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -139,7 +139,8 @@ const campBattleIds = {
sixth: 'sixth-battle-jieqiao-relief',
seventh: 'seventh-battle-xuzhou-rescue',
eighth: 'eighth-battle-xiaopei-supply-road',
ninth: 'ninth-battle-xuzhou-gate-night-raid'
ninth: 'ninth-battle-xuzhou-gate-night-raid',
tenth: 'tenth-battle-xuzhou-breakout'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -928,6 +929,87 @@ const campDialogues: CampDialogue[] = [
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
}
]
}
];
@@ -1006,7 +1088,13 @@ export class CampScene extends Phaser.Scene {
private ensureCurrentCampRecruitment() {
const battleId = this.currentCampBattleId();
if ((battleId !== campBattleIds.seventh && battleId !== campBattleIds.eighth && battleId !== campBattleIds.ninth) || !this.campaign) {
if (
(battleId !== campBattleIds.seventh &&
battleId !== campBattleIds.eighth &&
battleId !== campBattleIds.ninth &&
battleId !== campBattleIds.tenth) ||
!this.campaign
) {
return;
}
@@ -1026,6 +1114,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.tenth) {
return '서주 상실 후 군영';
}
if (battleId === campBattleIds.ninth) {
return '서주 성문 군영';
}

View File

@@ -9,6 +9,7 @@ import {
secondBattleScenario,
seventhBattleScenario,
sixthBattleScenario,
tenthBattleScenario,
thirdBattleScenario
} from '../data/battles';
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
@@ -317,7 +318,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'sixth-camp' ||
campaign.step === 'seventh-camp' ||
campaign.step === 'eighth-camp' ||
campaign.step === 'ninth-camp'
campaign.step === 'ninth-camp' ||
campaign.step === 'tenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -368,6 +370,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'tenth-battle') {
this.scene.start('BattleScene', { battleId: tenthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;