Add Yuan Shao refuge road chapter

This commit is contained in:
2026-06-23 04:23:14 +09:00
parent 09c1390ba5
commit 821cebde0e
11 changed files with 873 additions and 18 deletions

View File

@@ -103,7 +103,24 @@ const unitTexture: Record<string, string> = {
'cao-break-cavalry-c': 'unit-rebel-cavalry',
'cao-break-guard-a': 'unit-rebel',
'cao-break-guard-b': 'unit-rebel',
'cao-break-leader-che-zhou': 'unit-rebel-leader'
'cao-break-leader-che-zhou': 'unit-rebel-leader',
'yuan-refuge-scout-a': 'unit-rebel',
'yuan-refuge-scout-b': 'unit-rebel',
'yuan-refuge-infantry-a': 'unit-rebel',
'yuan-refuge-infantry-b': 'unit-rebel',
'yuan-refuge-infantry-c': 'unit-rebel',
'yuan-refuge-infantry-d': 'unit-rebel',
'yuan-refuge-archer-a': 'unit-rebel-archer',
'yuan-refuge-archer-b': 'unit-rebel-archer',
'yuan-refuge-archer-c': 'unit-rebel-archer',
'yuan-refuge-cavalry-a': 'unit-rebel-cavalry',
'yuan-refuge-cavalry-b': 'unit-rebel-cavalry',
'yuan-refuge-cavalry-c': 'unit-rebel-cavalry',
'yuan-refuge-guard-a': 'unit-rebel',
'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'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -655,7 +672,24 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'cao-break-cavalry-c': 'aggressive',
'cao-break-guard-a': 'guard',
'cao-break-guard-b': 'guard',
'cao-break-leader-che-zhou': 'guard'
'cao-break-leader-che-zhou': 'guard',
'yuan-refuge-scout-a': 'aggressive',
'yuan-refuge-scout-b': 'aggressive',
'yuan-refuge-infantry-a': 'guard',
'yuan-refuge-infantry-b': 'guard',
'yuan-refuge-infantry-c': 'guard',
'yuan-refuge-infantry-d': 'guard',
'yuan-refuge-archer-a': 'hold',
'yuan-refuge-archer-b': 'hold',
'yuan-refuge-archer-c': 'hold',
'yuan-refuge-cavalry-a': 'aggressive',
'yuan-refuge-cavalry-b': 'aggressive',
'yuan-refuge-cavalry-c': 'aggressive',
'yuan-refuge-guard-a': 'guard',
'yuan-refuge-guard-b': 'guard',
'yuan-refuge-guard-c': 'guard',
'yuan-refuge-strategist-a': 'hold',
'yuan-refuge-leader-cai-yang': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -2,6 +2,7 @@ import Phaser from 'phaser';
import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg';
import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg';
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
import fifteenthBattleMapUrl from '../../assets/images/battle/fifteenth-battle-map.svg';
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
@@ -86,6 +87,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-twelfth', twelfthBattleMapUrl);
this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl);
this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl);
this.load.image('battle-map-fifteenth', fifteenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -193,7 +193,8 @@ const campBattleIds = {
eleventh: 'eleventh-battle-xudu-refuge-road',
twelfth: 'twelfth-battle-xiapi-outer-siege',
thirteenth: 'thirteenth-battle-xiapi-final',
fourteenth: 'fourteenth-battle-cao-break'
fourteenth: 'fourteenth-battle-cao-break',
fifteenth: 'fifteenth-battle-yuan-refuge-road'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -337,6 +338,17 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'mi-zhu', reason: '보급과 회복 도구 운용으로 장기전을 안정시킵니다.' }
],
note: '차주의 관문은 길과 강이 겹쳐 전열, 기동, 책사, 보급이 모두 필요합니다. 새로 합류한 손건도 출전 후보로 살펴보십시오.'
},
'fifteenth-battle-yuan-refuge-road': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '원소 진영에 의탁할 명분과 생존 조건을 모두 짊어집니다.' },
{ unitId: 'guan-yu', reason: '채양의 수비선을 정면에서 무너뜨릴 전열 핵심입니다.' },
{ unitId: 'zhang-fei', reason: '추격 기병의 돌파를 받아치고 북상로를 벌리는 데 강합니다.' },
{ unitId: 'jian-yong', reason: '원소 사자와의 접선 명분을 조율하고 책략 지원을 맡습니다.' },
{ unitId: 'sun-qian', reason: '문서와 외교 판단으로 접선지 확보 목표에 직접 힘을 보탭니다.' }
],
note: '원소 의탁로는 추격 기병과 고지 궁병이 함께 압박합니다. 전열을 두껍게 세우되 책사와 보급 장수도 빼지 않는 편이 안정적입니다.'
}
};
@@ -1528,6 +1540,87 @@ const campDialogues: CampDialogue[] = [
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: 'liu-bei__mi-zhu',
rewardExp: 18,
lines: [
'장비: 원소군 밥은 양은 많아도 속이 편치 않소. 남의 군량을 먹으니 창끝까지 남의 눈치를 보는 기분이오.',
'미축: 그래서 우리 몫의 장부를 따로 두려 합니다. 받은 것은 받은 대로, 쓴 것은 쓴 대로 남겨야 훗날 떳떳하게 떠날 수 있습니다.',
'장비: 좋소. 그럼 나는 병사들이 주눅 들지 않게 술 대신 목소리를 나눠 주겠소. 배가 차도 기가 죽으면 싸움은 못 하니까.'
],
choices: [
{
id: 'separate-supply-ledger',
label: '보급 장부를 따로 둔다',
response: '미축은 유비군의 군량 장부를 따로 정리해 원소 진영에 빚이 쌓이는 일을 줄였고, 장비는 병사들에게 그 뜻을 크게 알렸다.',
rewardExp: 8
},
{
id: 'raise-camp-spirits',
label: '병사들의 사기를 다독인다',
response: '장비는 낯선 군영에서 움츠러든 병사들을 불러 세워 웃음과 호통으로 사기를 끌어올렸고, 미축은 작은 술과 밥을 나눴다.',
rewardExp: 8
}
]
}
];
@@ -1610,7 +1703,11 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.thirteenth || battleId === campBattleIds.fourteenth) {
if (
battleId === campBattleIds.thirteenth ||
battleId === campBattleIds.fourteenth ||
battleId === campBattleIds.fifteenth
) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds]
@@ -1646,6 +1743,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.fifteenth) {
return '원소 의탁 후 군영';
}
if (battleId === campBattleIds.fourteenth) {
return '서주 재기 후 군영';
}

View File

@@ -5,6 +5,7 @@ import {
eighthBattleScenario,
eleventhBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
fourthBattleScenario,
ninthBattleScenario,
@@ -327,7 +328,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'eleventh-camp' ||
campaign.step === 'twelfth-camp' ||
campaign.step === 'thirteenth-camp' ||
campaign.step === 'fourteenth-camp'
campaign.step === 'fourteenth-camp' ||
campaign.step === 'fifteenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -403,6 +405,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'fifteenth-battle') {
this.scene.start('BattleScene', { battleId: fifteenthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;