Add Chengdu surrender chapter

This commit is contained in:
2026-06-23 12:38:53 +09:00
parent 9ac621da76
commit aca7d6568a
11 changed files with 879 additions and 38 deletions

View File

@@ -51,6 +51,10 @@ import {
thirtySecondBattleMap,
thirtySecondBattleUnits,
thirtySecondBattleVictoryPages,
thirtyThirdBattleBonds,
thirtyThirdBattleMap,
thirtyThirdBattleUnits,
thirtyThirdBattleVictoryPages,
thirteenthBattleBonds,
thirteenthBattleMap,
thirteenthBattleUnits,
@@ -165,7 +169,8 @@ export type BattleScenarioId =
| 'twenty-ninth-battle-luo-outer-wall'
| 'thirtieth-battle-luofeng-ambush'
| 'thirty-first-battle-luo-main-gate'
| 'thirty-second-battle-mianzhu-gate';
| 'thirty-second-battle-mianzhu-gate'
| 'thirty-third-battle-chengdu-surrender';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2041,6 +2046,69 @@ export const thirtySecondBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const thirtyThirdBattleScenario: BattleScenarioDefinition = {
id: 'thirty-third-battle-chengdu-surrender',
title: '성도 항복 권고전',
victoryConditionLabel: '황권 방어선 격파',
defeatConditionLabel: '유비 또는 이엄 퇴각',
openingObjectiveLines: [
'성도 외곽의 마지막 방어선입니다. 황권을 물리치면 유장의 항복 권고가 실제 선택지로 바뀝니다.',
'이엄은 방금 합류한 익주 장수로, 이번 전장에서 유비군의 군율을 증언하는 핵심 인물입니다. 이엄을 반드시 지키십시오.',
'성도 창고와 마을을 안정시키면 항복 조건에 대한 신뢰가 커지고, 익주 확보 이후의 수습이 쉬워집니다.'
],
map: thirtyThirdBattleMap,
units: thirtyThirdBattleUnits,
bonds: thirtyThirdBattleBonds,
mapTextureKey: 'battle-map-thirty-third',
leaderUnitId: 'chengdu-final-leader-huang-quan',
quickVictoryTurnLimit: 48,
baseVictoryGold: 5340,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '황권 방어선 격파',
rewardGold: 3360,
unitId: 'chengdu-final-leader-huang-quan'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 980,
unitId: 'liu-bei'
},
{
id: 'li-yan',
kind: 'keep-unit-alive',
label: '이엄 생존',
rewardGold: 1320,
unitId: 'li-yan'
},
{
id: 'chengdu-storehouse',
kind: 'secure-terrain',
label: '성도 창고 확보',
rewardGold: 1620,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '48턴 이내 승리',
rewardGold: 1240,
maxTurn: 48
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'li-yan' }
],
itemRewards: ['콩 20', '상처약 15', '탁주 8', '익주 안정문 +1', '황권 합류'],
victoryPages: thirtyThirdBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -2075,7 +2143,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario,
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario,
'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario,
'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario
'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario,
'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -19,6 +19,7 @@ import {
thirteenthBattleScenario,
thirtyFirstBattleScenario,
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
thirtiethBattleScenario,
twentyEighthBattleScenario,
twentyNinthBattleScenario,
@@ -72,6 +73,8 @@ import {
thirtyFirstBattleVictoryPages,
thirtySecondBattleIntroPages,
thirtySecondBattleVictoryPages,
thirtyThirdBattleIntroPages,
thirtyThirdBattleVictoryPages,
thirtiethBattleIntroPages,
thirtiethBattleVictoryPages,
twentyEighthBattleIntroPages,
@@ -445,13 +448,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[thirtySecondBattleScenario.id]: {
afterBattleId: thirtySecondBattleScenario.id,
eyebrow: '다음 장 준비',
title: '성도 항복 권고 준비',
eyebrow: '다음 장',
title: thirtyThirdBattleScenario.title,
description:
'면죽관이 열리고 이엄이 합류했습니다. 이제 성도 포위와 유장의 항복 권고, 그리고 익주 통치의 첫 선택을 준비해야 합니다.',
rewardHint: '다음 장: 성도 항복 권고 준비 중',
pages: thirtySecondBattleVictoryPages,
unavailableNotice: '성도 항복 권고와 익주 최종 수습은 다음 작업에서 이어집니다. 새로 합류한 이엄을 포함해 보급과 내정형 무장 조합을 준비하십시오.'
'면죽관이 열리고 이엄이 합류했습니다. 이제 이엄의 증언과 법정·제갈량의 권고문을 앞세워 성도 외곽의 마지막 방어선을 눌러야 합니다.',
rewardHint: `예상 보상: ${thirtyThirdBattleScenario.title} 개방 / 황권 합류`,
nextBattleId: thirtyThirdBattleScenario.id,
campaignStep: 'thirty-third-battle',
pages: [...thirtySecondBattleVictoryPages, ...thirtyThirdBattleIntroPages]
},
[thirtyThirdBattleScenario.id]: {
afterBattleId: thirtyThirdBattleScenario.id,
eyebrow: '다음 장 준비',
title: '촉한 건국 준비',
description:
'성도가 항복하고 황권이 합류했습니다. 형주와 익주의 기반이 마련되었으니, 다음 장은 나라의 틀을 세우고 한중과 촉한 건국으로 향하는 준비입니다.',
rewardHint: '다음 장: 촉한 건국 준비 중',
pages: thirtyThirdBattleVictoryPages,
unavailableNotice: '촉한 건국과 한중 방면 전개는 다음 작업에서 이어집니다. 새로 합류한 황권을 포함해 내정과 방어형 무장 조합을 준비하십시오.'
}
};

View File

@@ -2076,6 +2076,68 @@ export const thirtySecondBattleVictoryPages: StoryPage[] = [
}
];
export const thirtyThirdBattleIntroPages: StoryPage[] = [
{
id: 'thirty-third-chengdu-encircled',
bgm: 'story-dark',
chapter: '성도 앞의 마지막 권고',
background: 'story-three-heroes',
speaker: '제갈량',
text: '면죽관이 열렸고 이엄까지 합류했습니다. 성도는 아직 문을 닫았으나, 이제 유장은 싸움과 항복 사이에서 더 이상 오래 머물 수 없습니다.'
},
{
id: 'thirty-third-li-yan-counsel',
bgm: 'battle-prep',
chapter: '이엄의 증언',
background: 'story-liu-bei',
speaker: '이엄',
text: '황권은 성도의 체면과 백성의 안위를 함께 보는 장수입니다. 그가 물러서면 유장공도 백성을 살리는 길을 택할 수 있습니다.'
},
{
id: 'thirty-third-final-plan',
bgm: 'battle-prep',
chapter: '성도 항복 권고전',
background: 'story-militia',
speaker: '법정',
text: '성도 성문을 부수는 것이 능사가 아닙니다. 황권의 방어선을 눌러 항복 조건을 받아들이게 하고, 마을과 창고를 지켜 유비군의 약속을 증명해야 합니다.'
},
{
id: 'thirty-third-sortie',
bgm: 'battle-prep',
chapter: '익주를 얻는 길',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '오늘의 승리는 성도의 문을 여는 것보다, 문 안의 백성이 두려움을 내려놓게 하는 데 있소. 칼을 들되 마음을 먼저 잃지 마시오.'
}
];
export const thirtyThirdBattleVictoryPages: StoryPage[] = [
{
id: 'thirty-third-victory-chengdu-yields',
bgm: 'militia-theme',
chapter: '성도 항복',
background: 'story-sortie',
text: '성도 외곽의 방어선은 무너졌지만 성문 안쪽은 불타지 않았습니다. 유장에게 보내진 마지막 권고문은 백성을 보전하고 장수의 체면을 남기는 조건을 담았습니다.'
},
{
id: 'thirty-third-huang-quan-joins',
bgm: 'battle-prep',
chapter: '황권의 귀순',
background: 'story-liu-bei',
speaker: '황권',
text: '익주를 지키려던 마음은 변하지 않았습니다. 다만 이제는 백성을 살리는 길이 유비공의 군율 아래 있다는 것을 보았습니다. 황권이 그 길에 서겠습니다.'
},
{
id: 'thirty-third-yizhou-secured',
bgm: 'story-dark',
chapter: '익주 확보',
background: 'story-three-heroes',
speaker: '제갈량',
text: '형주와 익주의 기반이 마련되었습니다. 유비군은 이제 유랑군이 아니라 나라의 뿌리를 갖춘 세력으로 바뀌며, 촉한 건국을 향한 다음 장을 준비합니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2413,6 +2475,12 @@ export const thirtySecondBattleMap: BattleMap = {
terrain: createThirtySecondBattleTerrain()
};
export const thirtyThirdBattleMap: BattleMap = {
width: 66,
height: 48,
terrain: createThirtyThirdBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4606,6 +4674,30 @@ export const chengduPressureRecruitUnits: UnitData[] = [
}
];
export const chengduSurrenderRecruitUnits: UnitData[] = [
{
id: 'huang-quan',
name: '황권',
faction: 'ally',
className: '익주 참모',
classKey: 'strategist',
level: 21,
exp: 0,
hp: 38,
maxHp: 38,
attack: 13,
move: 4,
stats: { might: 67, intelligence: 86, leadership: 82, agility: 64, luck: 74 },
equipment: {
weapon: { itemId: 'white-feather-fan', level: 3, exp: 18 },
armor: { itemId: 'oath-robe', level: 3, exp: 8 },
accessory: { itemId: 'war-manual', level: 2, exp: 12 }
},
x: 12,
y: 30
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -13193,6 +13285,72 @@ export const thirtySecondBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('chengdu-leader-li-yan', '이엄', '면죽관 수비장', 'rebelLeader', 61, 306, 86, 60, 18)
];
const thirtyThirdBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 4, y: 42 },
'guan-yu': { x: 5, y: 40 },
'zhang-fei': { x: 4, y: 44 },
'jian-yong': { x: 3, y: 45 },
'mi-zhu': { x: 5, y: 46 },
'sun-qian': { x: 7, y: 45 },
'zhao-yun': { x: 8, y: 41 },
'zhuge-liang': { x: 6, y: 40 },
'ma-liang': { x: 8, y: 46 },
'yi-ji': { x: 9, y: 43 },
'gong-zhi': { x: 10, y: 46 },
'huang-zhong': { x: 7, y: 42 },
'wei-yan': { x: 10, y: 40 },
'pang-tong': { x: 9, y: 41 },
'fa-zheng': { x: 11, y: 43 },
'wu-yi': { x: 12, y: 41 },
'yan-yan': { x: 11, y: 39 },
'li-yan': { x: 13, y: 40 }
};
export const thirtyThirdBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits
].map((unit) => placeScenarioUnit(unit, thirtyThirdBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('chengdu-final-scout-a', '성도 척후', '남문 척후', 'bandit', 60, 182, 66, 19, 41),
createThirtyFirstEnemyUnit('chengdu-final-scout-b', '성도 척후', '서문 척후', 'bandit', 60, 182, 66, 25, 39),
createThirtyFirstEnemyUnit('chengdu-final-scout-c', '성도 척후', '수로 척후', 'bandit', 61, 190, 68, 32, 37),
createThirtyFirstEnemyUnit('chengdu-final-infantry-a', '성도 보병', '외곽 보병', 'yellowTurban', 61, 266, 82, 31, 34),
createThirtyFirstEnemyUnit('chengdu-final-infantry-b', '성도 보병', '외곽 보병', 'yellowTurban', 61, 266, 82, 36, 33),
createThirtyFirstEnemyUnit('chengdu-final-infantry-c', '성도 보병', '남문 보병', 'yellowTurban', 62, 276, 84, 43, 28),
createThirtyFirstEnemyUnit('chengdu-final-infantry-d', '성도 보병', '남문 보병', 'yellowTurban', 62, 276, 84, 47, 27),
createThirtyFirstEnemyUnit('chengdu-final-infantry-e', '유장 친위대', '성문 친위대', 'yellowTurban', 63, 288, 86, 55, 23),
createThirtyFirstEnemyUnit('chengdu-final-infantry-f', '유장 친위대', '창고 친위대', 'yellowTurban', 63, 288, 86, 60, 25),
createThirtyFirstEnemyUnit('chengdu-final-archer-a', '성도 궁병', '성벽 궁병', 'archer', 61, 182, 66, 34, 31),
createThirtyFirstEnemyUnit('chengdu-final-archer-b', '성도 궁병', '남문 궁병', 'archer', 62, 190, 68, 46, 23),
createThirtyFirstEnemyUnit('chengdu-final-archer-c', '성도 궁병', '고지 궁병', 'archer', 62, 190, 68, 52, 18),
createThirtyFirstEnemyUnit('chengdu-final-archer-d', '성도 궁병', '창고 궁병', 'archer', 63, 198, 70, 61, 20),
createThirtyFirstEnemyUnit('chengdu-final-cavalry-a', '성도 기병', '우익 기병', 'cavalry', 62, 276, 86, 28, 43),
createThirtyFirstEnemyUnit('chengdu-final-cavalry-b', '성도 기병', '좌익 기병', 'cavalry', 62, 276, 86, 43, 38),
createThirtyFirstEnemyUnit('chengdu-final-cavalry-c', '성도 기병', '내성 기병', 'cavalry', 63, 288, 88, 58, 31),
createThirtyFirstEnemyUnit('chengdu-final-guard-a', '황권 친위대', '권고 반대파', 'yellowTurban', 63, 300, 88, 50, 22),
createThirtyFirstEnemyUnit('chengdu-final-guard-b', '황권 친위대', '권고 반대파', 'yellowTurban', 63, 300, 88, 54, 21),
createThirtyFirstEnemyUnit('chengdu-final-guard-c', '황권 친위대', '성도 친위대', 'yellowTurban', 64, 312, 90, 60, 18),
createThirtyFirstEnemyUnit('chengdu-final-strategist-a', '성도 책사', '항복 반대파', 'strategist', 62, 190, 68, 48, 19),
createThirtyFirstEnemyUnit('chengdu-final-strategist-b', '성도 책사', '유장 사자', 'strategist', 63, 198, 70, 57, 16),
createThirtyFirstEnemyUnit('chengdu-final-ambusher-a', '성도 복병', '남벽 복병', 'bandit', 61, 190, 70, 37, 41),
createThirtyFirstEnemyUnit('chengdu-final-ambusher-b', '성도 복병', '동문 복병', 'bandit', 61, 190, 70, 51, 35),
createThirtyFirstEnemyUnit('chengdu-final-ambusher-c', '성도 복병', '북벽 복병', 'bandit', 62, 198, 72, 62, 12),
createThirtyFirstEnemyUnit('chengdu-final-officer-a', '성도 부장', '남문 부장', 'rebelLeader', 64, 318, 92, 58, 22),
createThirtyFirstEnemyUnit('chengdu-final-leader-huang-quan', '황권', '성도 수비 참모', 'rebelLeader', 65, 336, 94, 62, 18)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -13571,6 +13729,33 @@ export const chengduPressureRecruitBonds: BattleBond[] = [
}
];
export const chengduSurrenderRecruitBonds: BattleBond[] = [
{
id: 'liu-bei__huang-quan',
unitIds: ['liu-bei', 'huang-quan'],
title: '항복과 보전',
level: 31,
exp: 0,
description: '유비의 보전 약속과 황권의 현실적인 판단이 맞닿아, 성도 항복 뒤 백성과 장수의 불안을 낮춥니다.'
},
{
id: 'zhuge-liang__huang-quan',
unitIds: ['zhuge-liang', 'huang-quan'],
title: '성도 수습책',
level: 29,
exp: 0,
description: '제갈량의 큰 내정 구상과 황권의 현지 행정 감각이 만나, 익주 통치의 첫 질서를 세웁니다.'
},
{
id: 'fa-zheng__huang-quan',
unitIds: ['fa-zheng', 'huang-quan'],
title: '익주 책사들의 균형',
level: 28,
exp: 0,
description: '법정의 결단력과 황권의 신중함이 서로를 보완하며, 익주 장수들을 다루는 말길을 넓힙니다.'
}
];
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -13610,6 +13795,9 @@ export const thirtyFirstBattleBonds: BattleBond[] = [...thirtiethBattleBonds].ma
export const thirtySecondBattleBonds: BattleBond[] = [...thirtyFirstBattleBonds, ...luoCastleProperRecruitBonds].map(
cloneBattleBondForScenario
);
export const thirtyThirdBattleBonds: BattleBond[] = [...thirtySecondBattleBonds, ...chengduPressureRecruitBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -14937,6 +15125,72 @@ function createThirtySecondBattleTerrain(): TerrainType[][] {
);
}
function createThirtyThirdBattleTerrain(): TerrainType[][] {
return Array.from({ length: 48 }, (_, y) =>
Array.from({ length: 66 }, (_, x): TerrainType => {
if (x <= 4 && y >= 41) {
return 'camp';
}
if (
(x >= 50 && x <= 64 && y >= 10 && y <= 27) ||
(x >= 46 && x <= 56 && y >= 7 && y <= 18) ||
(x >= 56 && x <= 65 && y >= 18 && y <= 32) ||
(x >= 53 && x <= 61 && y >= 29 && y <= 35)
) {
return 'fort';
}
if (
(x >= 18 && x <= 22 && y >= 39 && y <= 41) ||
(x >= 36 && x <= 39 && y >= 33 && y <= 35) ||
(x >= 46 && x <= 48 && y >= 28 && y <= 30) ||
(x >= 57 && x <= 60 && y >= 31 && y <= 34)
) {
return 'village';
}
if (
(y >= 43 && x >= 4 && x <= 20) ||
(x >= 19 && x <= 32 && y >= 39 && y <= 43) ||
(x >= 31 && x <= 44 && y >= 34 && y <= 39) ||
(x >= 43 && x <= 55 && y >= 28 && y <= 34) ||
(x >= 54 && x <= 63 && y >= 22 && y <= 28) ||
(x >= 59 && x <= 65 && y >= 19 && y <= 23)
) {
return 'road';
}
if ((x === 15 || x === 16) && y >= 0 && y <= 37) {
return 'river';
}
if ((x === 36 || x === 37) && y >= 4 && y <= 32) {
return 'river';
}
if ((x === 48 || x === 49) && y >= 34 && y <= 47) {
return 'river';
}
if (
(x >= 4 && x <= 22 && y >= 8 && y <= 32) ||
(x >= 23 && x <= 35 && y >= 14 && y <= 33) ||
(x >= 38 && x <= 52 && y >= 35 && y <= 47) ||
(x >= 51 && x <= 65 && y >= 31 && y <= 46)
) {
return 'forest';
}
if (
(x >= 0 && x <= 13 && y <= 17) ||
(x >= 23 && x <= 35 && y <= 13) ||
(x >= 37 && x <= 54 && y >= 2 && y <= 14) ||
(x >= 57 && x <= 65 && y <= 12) ||
(x >= 27 && x <= 40 && y >= 40 && y <= 47)
) {
return 'hill';
}
if ((x <= 2 && y <= 40) || (x >= 64 && y >= 28) || (x >= 50 && x <= 53 && y <= 8)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),