Add Jiameng Pass Ma Chao chapter

This commit is contained in:
2026-06-23 13:01:25 +09:00
parent aca7d6568a
commit edbdaac773
11 changed files with 878 additions and 40 deletions

View File

@@ -43,6 +43,10 @@ import {
tenthBattleMap,
tenthBattleUnits,
tenthBattleVictoryPages,
thirtyFourthBattleBonds,
thirtyFourthBattleMap,
thirtyFourthBattleUnits,
thirtyFourthBattleVictoryPages,
thirtyFirstBattleBonds,
thirtyFirstBattleMap,
thirtyFirstBattleUnits,
@@ -170,7 +174,8 @@ export type BattleScenarioId =
| 'thirtieth-battle-luofeng-ambush'
| 'thirty-first-battle-luo-main-gate'
| 'thirty-second-battle-mianzhu-gate'
| 'thirty-third-battle-chengdu-surrender';
| 'thirty-third-battle-chengdu-surrender'
| 'thirty-fourth-battle-jiameng-pass';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2109,6 +2114,69 @@ export const thirtyThirdBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const thirtyFourthBattleScenario: BattleScenarioDefinition = {
id: 'thirty-fourth-battle-jiameng-pass',
title: '가맹관 마초 대면전',
victoryConditionLabel: '마초 돌격 저지',
defeatConditionLabel: '유비 또는 황권 퇴각',
openingObjectiveLines: [
'가맹관은 익주와 한중을 잇는 목입니다. 이 길을 열면 촉한 건국 이후 북방 전선의 첫 기반이 마련됩니다.',
'황권은 이번 전장이 첫 출전입니다. 한중 길과 익주 내정의 균형을 잡으려면 황권을 지켜야 합니다.',
'마초의 돌격을 받아내고 방덕의 친위대를 제압하면, 서량 기병을 유비군으로 끌어들일 말길이 열립니다.'
],
map: thirtyFourthBattleMap,
units: thirtyFourthBattleUnits,
bonds: thirtyFourthBattleBonds,
mapTextureKey: 'battle-map-thirty-fourth',
leaderUnitId: 'jiameng-leader-ma-chao',
quickVictoryTurnLimit: 50,
baseVictoryGold: 5660,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '마초 돌격 저지',
rewardGold: 3560,
unitId: 'jiameng-leader-ma-chao'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 1000,
unitId: 'liu-bei'
},
{
id: 'huang-quan',
kind: 'keep-unit-alive',
label: '황권 생존',
rewardGold: 1360,
unitId: 'huang-quan'
},
{
id: 'jiameng-storehouse',
kind: 'secure-terrain',
label: '가맹관 보급 창고 확보',
rewardGold: 1680,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '50턴 이내 승리',
rewardGold: 1300,
maxTurn: 50
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'huang-quan' }
],
itemRewards: ['콩 22', '상처약 16', '탁주 8', '서량 기병 전술서 +1', '마초 합류'],
victoryPages: thirtyFourthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -2144,7 +2212,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario,
'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario,
'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario,
'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario
'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario,
'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -17,6 +17,7 @@ import {
seventeenthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
thirtyFourthBattleScenario,
thirtyFirstBattleScenario,
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
@@ -69,6 +70,8 @@ import {
tenthBattleVictoryPages,
thirteenthBattleIntroPages,
thirteenthBattleVictoryPages,
thirtyFourthBattleIntroPages,
thirtyFourthBattleVictoryPages,
thirtyFirstBattleIntroPages,
thirtyFirstBattleVictoryPages,
thirtySecondBattleIntroPages,
@@ -459,13 +462,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[thirtyThirdBattleScenario.id]: {
afterBattleId: thirtyThirdBattleScenario.id,
eyebrow: '다음 장 준비',
title: '촉한 건국 준비',
eyebrow: '다음 장',
title: thirtyFourthBattleScenario.title,
description:
'성도가 항복하고 황권이 합류했습니다. 형주와 익주의 기반이 마련되었으니, 다음 장은 나라의 틀을 세우고 한중과 촉한 건국으로 향하는 준비입니다.',
rewardHint: '다음 장: 촉한 건국 준비 중',
pages: thirtyThirdBattleVictoryPages,
unavailableNotice: '촉한 건국과 한중 방면 전개는 다음 작업에서 이어집니다. 새로 합류한 황권을 포함해 내정과 방어형 무장 조합을 준비하십시오.'
'성도가 항복하고 황권이 합류했습니다. 이제 익주의 북문인 가맹관에서 한중 방면을 살피고, 서량의 기병장 마초를 유비군의 깃발 아래로 끌어들일 차례입니다.',
rewardHint: `예상 보상: ${thirtyFourthBattleScenario.title} 개방 / 마초 합류`,
nextBattleId: thirtyFourthBattleScenario.id,
campaignStep: 'thirty-fourth-battle',
pages: [...thirtyThirdBattleVictoryPages, ...thirtyFourthBattleIntroPages]
},
[thirtyFourthBattleScenario.id]: {
afterBattleId: thirtyFourthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '한중 전선 준비',
description:
'가맹관에서 마초가 합류했습니다. 촉한 건국의 군사 기반을 다지려면 이제 한중 전선을 본격적으로 준비해야 합니다.',
rewardHint: '다음 장: 한중 전선 준비 중',
pages: thirtyFourthBattleVictoryPages,
unavailableNotice: '한중 전선과 촉한 건국 선포는 다음 작업에서 이어집니다. 새로 합류한 마초를 포함해 기병 돌파와 내정 안정 조합을 준비하십시오.'
}
};

View File

@@ -2138,6 +2138,68 @@ export const thirtyThirdBattleVictoryPages: StoryPage[] = [
}
];
export const thirtyFourthBattleIntroPages: StoryPage[] = [
{
id: 'thirty-fourth-hanzhong-road',
bgm: 'story-dark',
chapter: '한중으로 향하는 길',
background: 'story-three-heroes',
speaker: '제갈량',
text: '성도는 안정되었으나 북쪽 한중은 아직 장로와 조조의 그늘 아래 흔들립니다. 촉한의 이름을 세우려면 북문을 지킬 길부터 열어야 합니다.'
},
{
id: 'thirty-fourth-huang-quan-first-counsel',
bgm: 'battle-prep',
chapter: '황권의 첫 출전',
background: 'story-liu-bei',
speaker: '황권',
text: '가맹관은 익주와 한중을 잇는 목입니다. 그곳에 마초의 기병이 나타났다면, 싸움만이 아니라 그가 누구의 길을 택할지도 보아야 합니다.'
},
{
id: 'thirty-fourth-ma-chao-appears',
bgm: 'battle-prep',
chapter: '서량의 기병',
background: 'story-militia',
speaker: '법정',
text: '마초는 조조에게 쫓기고 장로에게도 마음을 다 주지 못했습니다. 그의 돌격을 막아 내고, 유비군이 서량의 창을 품을 그릇임을 보여 주십시오.'
},
{
id: 'thirty-fourth-sortie',
bgm: 'battle-prep',
chapter: '가맹관 마초 대면전',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '마초의 용맹을 꺾으려 하지 말고, 그 창끝이 향할 뜻을 바꾸어야 하오. 황권과 법정은 길을 열고, 장수들은 기병의 돌파를 받아내시오.'
}
];
export const thirtyFourthBattleVictoryPages: StoryPage[] = [
{
id: 'thirty-fourth-victory-charge-stopped',
bgm: 'militia-theme',
chapter: '서량 돌격을 막다',
background: 'story-sortie',
text: '가맹관의 좁은 길에서 서량 기병의 첫 돌격은 멈췄습니다. 마초는 패배보다도 유비군의 군율과 익주 장수들의 안정된 태도를 더 오래 바라보았습니다.'
},
{
id: 'thirty-fourth-ma-chao-joins',
bgm: 'battle-prep',
chapter: '마초의 합류',
background: 'story-liu-bei',
speaker: '마초',
text: '창을 맡길 주군을 찾지 못해 오래 떠돌았습니다. 유비공이 백성을 보전하고 장수를 예우한다면, 서량의 창 또한 그 깃발 아래 서겠습니다.'
},
{
id: 'thirty-fourth-next-hanzhong',
bgm: 'story-dark',
chapter: '한중 전선의 개막',
background: 'story-three-heroes',
speaker: '제갈량',
text: '마초가 합류하자 북쪽 길이 한층 넓어졌습니다. 이제 촉한의 기반은 성도 안쪽에서 끝나지 않고, 한중을 향한 전선으로 나아가게 됩니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2481,6 +2543,12 @@ export const thirtyThirdBattleMap: BattleMap = {
terrain: createThirtyThirdBattleTerrain()
};
export const thirtyFourthBattleMap: BattleMap = {
width: 68,
height: 50,
terrain: createThirtyFourthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4698,6 +4766,30 @@ export const chengduSurrenderRecruitUnits: UnitData[] = [
}
];
export const hanzhongOpeningRecruitUnits: UnitData[] = [
{
id: 'ma-chao',
name: '마초',
faction: 'ally',
className: '서량 기병',
classKey: 'cavalry',
level: 23,
exp: 0,
hp: 52,
maxHp: 52,
attack: 18,
move: 6,
stats: { might: 92, intelligence: 62, leadership: 86, agility: 84, luck: 70 },
equipment: {
weapon: { itemId: 'sky-piercer-halberd', level: 4, exp: 22 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 18 },
accessory: { itemId: 'bravery-token', level: 2, exp: 14 }
},
x: 13,
y: 32
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -13351,6 +13443,70 @@ export const thirtyThirdBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('chengdu-final-leader-huang-quan', '황권', '성도 수비 참모', 'rebelLeader', 65, 336, 94, 62, 18)
];
const thirtyFourthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 4, y: 44 },
'guan-yu': { x: 5, y: 42 },
'zhang-fei': { x: 4, y: 46 },
'jian-yong': { x: 3, y: 47 },
'mi-zhu': { x: 5, y: 48 },
'sun-qian': { x: 7, y: 47 },
'zhao-yun': { x: 8, y: 43 },
'zhuge-liang': { x: 6, y: 42 },
'ma-liang': { x: 8, y: 48 },
'yi-ji': { x: 9, y: 45 },
'gong-zhi': { x: 10, y: 48 },
'huang-zhong': { x: 7, y: 44 },
'wei-yan': { x: 10, y: 42 },
'pang-tong': { x: 9, y: 43 },
'fa-zheng': { x: 11, y: 45 },
'wu-yi': { x: 12, y: 43 },
'yan-yan': { x: 11, y: 41 },
'li-yan': { x: 13, y: 42 },
'huang-quan': { x: 14, y: 44 }
};
export const thirtyFourthBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits,
...chengduSurrenderRecruitUnits
].map((unit) => placeScenarioUnit(unit, thirtyFourthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('jiameng-scout-a', '한중 척후', '산길 척후', 'bandit', 64, 198, 72, 18, 43),
createThirtyFirstEnemyUnit('jiameng-scout-b', '한중 척후', '관문 척후', 'bandit', 64, 198, 72, 26, 41),
createThirtyFirstEnemyUnit('jiameng-scout-c', '한중 척후', '수로 척후', 'bandit', 65, 206, 74, 34, 39),
createThirtyFirstEnemyUnit('jiameng-infantry-a', '장로 보병', '외곽 보병', 'yellowTurban', 65, 288, 88, 33, 36),
createThirtyFirstEnemyUnit('jiameng-infantry-b', '장로 보병', '외곽 보병', 'yellowTurban', 65, 288, 88, 38, 35),
createThirtyFirstEnemyUnit('jiameng-infantry-c', '가맹관 보병', '관문 보병', 'yellowTurban', 66, 300, 90, 45, 30),
createThirtyFirstEnemyUnit('jiameng-infantry-d', '가맹관 보병', '관문 보병', 'yellowTurban', 66, 300, 90, 49, 29),
createThirtyFirstEnemyUnit('jiameng-archer-a', '한중 궁병', '산허리 궁병', 'archer', 65, 206, 74, 36, 32),
createThirtyFirstEnemyUnit('jiameng-archer-b', '한중 궁병', '관문 궁병', 'archer', 66, 214, 76, 47, 24),
createThirtyFirstEnemyUnit('jiameng-archer-c', '한중 궁병', '고지 궁병', 'archer', 66, 214, 76, 55, 19),
createThirtyFirstEnemyUnit('jiameng-cavalry-a', '서량 기병', '좌익 기병', 'cavalry', 66, 304, 92, 30, 45),
createThirtyFirstEnemyUnit('jiameng-cavalry-b', '서량 기병', '우익 기병', 'cavalry', 66, 304, 92, 44, 40),
createThirtyFirstEnemyUnit('jiameng-cavalry-c', '서량 기병', '돌격 기병', 'cavalry', 67, 316, 94, 56, 33),
createThirtyFirstEnemyUnit('jiameng-cavalry-d', '서량 기병', '친위 기병', 'cavalry', 67, 316, 94, 61, 26),
createThirtyFirstEnemyUnit('jiameng-guard-a', '마초 친위대', '돌격 친위대', 'yellowTurban', 67, 326, 96, 53, 24),
createThirtyFirstEnemyUnit('jiameng-guard-b', '마초 친위대', '관문 친위대', 'yellowTurban', 67, 326, 96, 58, 23),
createThirtyFirstEnemyUnit('jiameng-strategist-a', '한중 책사', '장로 사자', 'strategist', 66, 214, 76, 50, 20),
createThirtyFirstEnemyUnit('jiameng-strategist-b', '한중 책사', '관문 책사', 'strategist', 67, 222, 78, 60, 18),
createThirtyFirstEnemyUnit('jiameng-ambusher-a', '한중 복병', '남령 복병', 'bandit', 65, 206, 76, 39, 43),
createThirtyFirstEnemyUnit('jiameng-ambusher-b', '한중 복병', '동령 복병', 'bandit', 65, 206, 76, 52, 37),
createThirtyFirstEnemyUnit('jiameng-officer-pang-de', '방덕', '서량 부장', 'rebelLeader', 68, 344, 98, 59, 25),
createThirtyFirstEnemyUnit('jiameng-leader-ma-chao', '마초', '서량 기병장', 'rebelLeader', 69, 364, 102, 63, 20)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -13756,6 +13912,33 @@ export const chengduSurrenderRecruitBonds: BattleBond[] = [
}
];
export const hanzhongOpeningRecruitBonds: BattleBond[] = [
{
id: 'liu-bei__ma-chao',
unitIds: ['liu-bei', 'ma-chao'],
title: '떠도는 창과 새 깃발',
level: 30,
exp: 0,
description: '유비의 포용과 마초의 돌격심이 맞물려, 서량 기병의 창끝이 촉한의 깃발 아래로 향합니다.'
},
{
id: 'zhao-yun__ma-chao',
unitIds: ['zhao-yun', 'ma-chao'],
title: '백마와 금마초',
level: 28,
exp: 0,
description: '조운의 절제된 기동과 마초의 거친 돌격이 서로를 자극하며, 기병 운용의 폭을 넓힙니다.'
},
{
id: 'zhang-fei__ma-chao',
unitIds: ['zhang-fei', 'ma-chao'],
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);
@@ -13798,6 +13981,9 @@ export const thirtySecondBattleBonds: BattleBond[] = [...thirtyFirstBattleBonds,
export const thirtyThirdBattleBonds: BattleBond[] = [...thirtySecondBattleBonds, ...chengduPressureRecruitBonds].map(
cloneBattleBondForScenario
);
export const thirtyFourthBattleBonds: BattleBond[] = [...thirtyThirdBattleBonds, ...chengduSurrenderRecruitBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -15191,6 +15377,77 @@ function createThirtyThirdBattleTerrain(): TerrainType[][] {
);
}
function createThirtyFourthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 50 }, (_, y) =>
Array.from({ length: 68 }, (_, x): TerrainType => {
if (x <= 4 && y >= 43) {
return 'camp';
}
if (
(x >= 51 && x <= 66 && y >= 11 && y <= 28) ||
(x >= 47 && x <= 58 && y >= 7 && y <= 18) ||
(x >= 57 && x <= 67 && y >= 20 && y <= 34) ||
(x >= 54 && x <= 61 && y >= 31 && y <= 37)
) {
return 'fort';
}
if (
(x >= 18 && x <= 22 && y >= 41 && y <= 43) ||
(x >= 37 && x <= 40 && y >= 35 && y <= 37) ||
(x >= 47 && x <= 49 && y >= 30 && y <= 32) ||
(x >= 58 && x <= 61 && y >= 33 && y <= 36)
) {
return 'village';
}
if (
(y >= 45 && x >= 4 && x <= 22) ||
(x >= 21 && x <= 34 && y >= 41 && y <= 45) ||
(x >= 33 && x <= 46 && y >= 36 && y <= 41) ||
(x >= 45 && x <= 57 && y >= 30 && y <= 36) ||
(x >= 56 && x <= 65 && y >= 24 && y <= 30) ||
(x >= 60 && x <= 67 && y >= 20 && y <= 24)
) {
return 'road';
}
if ((x === 15 || x === 16) && y >= 0 && y <= 39) {
return 'river';
}
if ((x === 37 || x === 38) && y >= 3 && y <= 34) {
return 'river';
}
if ((x === 49 || x === 50) && y >= 36 && y <= 49) {
return 'river';
}
if (
(x >= 4 && x <= 23 && y >= 8 && y <= 34) ||
(x >= 24 && x <= 36 && y >= 15 && y <= 35) ||
(x >= 40 && x <= 54 && y >= 37 && y <= 49) ||
(x >= 52 && x <= 67 && y >= 33 && y <= 48)
) {
return 'forest';
}
if (
(x >= 0 && x <= 14 && y <= 19) ||
(x >= 24 && x <= 36 && y <= 14) ||
(x >= 38 && x <= 55 && y >= 1 && y <= 15) ||
(x >= 57 && x <= 67 && y <= 13) ||
(x >= 28 && x <= 42 && y >= 42 && y <= 49)
) {
return 'hill';
}
if (
(x <= 2 && y <= 42) ||
(x >= 65 && y >= 29) ||
(x >= 51 && x <= 54 && y <= 8) ||
(x >= 23 && x <= 27 && y >= 22 && y <= 32)
) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -481,7 +481,30 @@ const unitTexture: Record<string, string> = {
'chengdu-final-ambusher-b': 'unit-rebel',
'chengdu-final-ambusher-c': 'unit-rebel',
'chengdu-final-officer-a': 'unit-rebel-leader',
'chengdu-final-leader-huang-quan': 'unit-rebel-leader'
'chengdu-final-leader-huang-quan': 'unit-rebel-leader',
'ma-chao': 'unit-rebel-cavalry',
'jiameng-scout-a': 'unit-rebel',
'jiameng-scout-b': 'unit-rebel',
'jiameng-scout-c': 'unit-rebel',
'jiameng-infantry-a': 'unit-rebel',
'jiameng-infantry-b': 'unit-rebel',
'jiameng-infantry-c': 'unit-rebel',
'jiameng-infantry-d': 'unit-rebel',
'jiameng-archer-a': 'unit-rebel-archer',
'jiameng-archer-b': 'unit-rebel-archer',
'jiameng-archer-c': 'unit-rebel-archer',
'jiameng-cavalry-a': 'unit-rebel-cavalry',
'jiameng-cavalry-b': 'unit-rebel-cavalry',
'jiameng-cavalry-c': 'unit-rebel-cavalry',
'jiameng-cavalry-d': 'unit-rebel-cavalry',
'jiameng-guard-a': 'unit-rebel',
'jiameng-guard-b': 'unit-rebel',
'jiameng-strategist-a': 'unit-rebel-archer',
'jiameng-strategist-b': 'unit-rebel-archer',
'jiameng-ambusher-a': 'unit-rebel',
'jiameng-ambusher-b': 'unit-rebel',
'jiameng-officer-pang-de': 'unit-rebel-leader',
'jiameng-leader-ma-chao': 'unit-rebel-cavalry'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -1423,7 +1446,29 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'chengdu-final-ambusher-b': 'aggressive',
'chengdu-final-ambusher-c': 'aggressive',
'chengdu-final-officer-a': 'guard',
'chengdu-final-leader-huang-quan': 'guard'
'chengdu-final-leader-huang-quan': 'guard',
'jiameng-scout-a': 'aggressive',
'jiameng-scout-b': 'aggressive',
'jiameng-scout-c': 'aggressive',
'jiameng-infantry-a': 'guard',
'jiameng-infantry-b': 'guard',
'jiameng-infantry-c': 'guard',
'jiameng-infantry-d': 'guard',
'jiameng-archer-a': 'hold',
'jiameng-archer-b': 'hold',
'jiameng-archer-c': 'hold',
'jiameng-cavalry-a': 'aggressive',
'jiameng-cavalry-b': 'aggressive',
'jiameng-cavalry-c': 'aggressive',
'jiameng-cavalry-d': 'aggressive',
'jiameng-guard-a': 'guard',
'jiameng-guard-b': 'guard',
'jiameng-strategist-a': 'hold',
'jiameng-strategist-b': 'hold',
'jiameng-ambusher-a': 'aggressive',
'jiameng-ambusher-b': 'aggressive',
'jiameng-officer-pang-de': 'guard',
'jiameng-leader-ma-chao': 'aggressive'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -16,6 +16,7 @@ import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-m
import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
import thirtyFourthBattleMapUrl from '../../assets/images/battle/thirty-fourth-battle-map.svg';
import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg';
import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg';
import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg';
@@ -124,6 +125,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-thirty-first', thirtyFirstBattleMapUrl);
this.load.image('battle-map-thirty-second', thirtySecondBattleMapUrl);
this.load.image('battle-map-thirty-third', thirtyThirdBattleMapUrl);
this.load.image('battle-map-thirty-fourth', thirtyFourthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -19,6 +19,8 @@ import {
fuPassRecruitUnits,
guiyangRecruitBonds,
guiyangRecruitUnits,
hanzhongOpeningRecruitBonds,
hanzhongOpeningRecruitUnits,
jingzhouRecruitBonds,
jingzhouRecruitUnits,
liuBiaoRecruitBonds,
@@ -337,7 +339,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '촉한 건국',
period: '한실을 잇는 깃발',
description: '여러 전장을 지나 촉한을 세우고 북벌을 바라보는 장기 목표입니다.',
battleIds: [],
battleIds: ['thirty-fourth-battle-jiameng-pass'],
nextHints: ['왕업 선언', '오호대장군', '북벌 준비']
}
];
@@ -375,7 +377,8 @@ const campBattleIds = {
thirtieth: 'thirtieth-battle-luofeng-ambush',
thirtyFirst: 'thirty-first-battle-luo-main-gate',
thirtySecond: 'thirty-second-battle-mianzhu-gate',
thirtyThird: 'thirty-third-battle-chengdu-surrender'
thirtyThird: 'thirty-third-battle-chengdu-surrender',
thirtyFourth: 'thirty-fourth-battle-jiameng-pass'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -746,6 +749,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'wu-yi', reason: '성도 외곽 길과 창고 위치를 아는 현지 장수입니다.' }
],
note: '성도 항복 권고전은 열여덟 명 중 여섯 명만 출전합니다. 새로 합류한 이엄을 데려가면 설득과 생존 목표가 맞물리고, 책사 비중을 늘리면 항복 명분이 강해집니다.'
},
'thirty-fourth-battle-jiameng-pass': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '가맹관에서 마초를 받아들일 명분과 패배 조건의 중심입니다.' },
{ unitId: 'huang-quan', reason: '황권의 첫 출전입니다. 익주 북문과 한중 길을 해석하는 현지 참모입니다.' },
{ unitId: 'zhao-yun', reason: '서량 기병의 돌격을 기동으로 받아내고 역습하기 좋습니다.' },
{ unitId: 'zhang-fei', reason: '마초의 돌격을 정면에서 받아낼 전열 압박이 필요합니다.' },
{ unitId: 'fa-zheng', reason: '마초가 장로에게 마음을 다 주지 못한 틈을 읽는 책사입니다.' },
{ unitId: 'huang-zhong', reason: '산허리 궁병과 기병 돌격을 안정적으로 견제합니다.' }
],
note: '가맹관 마초 대면전은 열아홉 명 중 여섯 명만 출전합니다. 황권을 데려가면 한중 길 해석과 생존 목표가 맞물리고, 기병 대응 무장을 고르는 압박이 커집니다.'
}
};
@@ -3503,6 +3518,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 12
}
]
},
{
id: 'liu-ma-after-jiameng',
title: '떠도는 창과 새 깃발',
availableAfterBattleIds: [campBattleIds.thirtyFourth],
unitIds: ['liu-bei', 'ma-chao'],
bondId: 'liu-bei__ma-chao',
rewardExp: 40,
lines: [
'마초: 저는 조조를 미워했고, 장로에게 기대었으나 마음 둘 곳을 찾지 못했습니다.',
'유비: 떠돌던 창도 지킬 백성과 뜻을 만나면 깃발 아래 설 수 있소. 그대의 용맹을 사람을 살리는 데 쓰시오.',
'마초: 그렇다면 서량의 창은 더는 원한만 좇지 않겠습니다. 유비공의 깃발 아래에서 북쪽 길을 열겠습니다.'
],
choices: [
{
id: 'accept-ma-chao-oath',
label: '마초의 맹세를 받아들인다',
response: '유비가 직접 마초의 창을 들어 올리자, 서량 기병들은 새 깃발 앞에서 고개를 숙였습니다.',
rewardExp: 15
},
{
id: 'ask-ma-chao-hanzhong-road',
label: '한중 길을 묻는다',
response: '마초는 장로군의 관문, 기병이 달릴 수 있는 능선, 조조군의 감시 지점을 지도에 표시했습니다.',
rewardExp: 14
}
]
},
{
id: 'zhao-ma-after-jiameng',
title: '백마와 금마초',
availableAfterBattleIds: [campBattleIds.thirtyFourth],
unitIds: ['zhao-yun', 'ma-chao'],
bondId: 'zhao-yun__ma-chao',
rewardExp: 36,
lines: [
'조운: 서량 기병의 돌격은 빠르나, 빠른 창일수록 돌아올 길도 보아야 합니다.',
'마초: 백마 장군의 말은 조용하지만 빈틈이 없군. 내 창은 때로 너무 멀리 나갔습니다.',
'조운: 다음 전장에서는 서로의 말발굽이 너무 멀어지지 않게 합시다.'
],
choices: [
{
id: 'practice-cavalry-turn',
label: '기병 회전을 연습한다',
response: '조운의 절제된 말놀림과 마초의 폭발적인 돌격이 이어지며, 기병대의 움직임이 한층 깊어졌습니다.',
rewardExp: 13
},
{
id: 'share-scouting-pace',
label: '정찰 속도를 맞춘다',
response: '두 장수는 빠른 돌파와 안전한 귀환의 간격을 맞추며 한중 전선의 정찰 기준을 세웠습니다.',
rewardExp: 12
}
]
},
{
id: 'zhang-ma-after-jiameng',
title: '호통과 돌격',
availableAfterBattleIds: [campBattleIds.thirtyFourth],
unitIds: ['zhang-fei', 'ma-chao'],
bondId: 'zhang-fei__ma-chao',
rewardExp: 36,
lines: [
'장비: 하하! 네놈 창끝이 제법 매섭더군. 그래도 내 앞에서는 한 번 멈췄지 않느냐!',
'마초: 장비 장군의 호통도 말발굽을 흔들 만큼 거칠었습니다. 다음에는 같은 방향으로 흔들어 보지요.',
'장비: 좋다! 누가 먼저 적진을 갈라 놓는지 겨뤄 보자.'
],
choices: [
{
id: 'promise-front-duel',
label: '전열 돌파를 겨룬다',
response: '장비와 마초가 웃으며 창을 맞대자, 병사들은 다음 전열 돌파가 조금 두려우면서도 기대된다고 수군거렸습니다.',
rewardExp: 13
},
{
id: 'split-shock-roles',
label: '돌격 역할을 나눈다',
response: '장비는 좁은 길을 부수고, 마초는 열린 측면을 파고드는 방식으로 서로의 돌격을 나누었습니다.',
rewardExp: 12
}
]
}
];
@@ -4612,6 +4708,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['한중 길 초안 1']
}
]
},
{
id: 'jiameng-stable-mustering',
title: '가맹관 마구간 정비',
location: '가맹관 마구간',
availableAfterBattleIds: [campBattleIds.thirtyFourth],
bondId: 'liu-bei__ma-chao',
description: '마초가 합류한 뒤 서량 기병의 말과 유비군의 보급 체계를 함께 맞춥니다.',
lines: [
'마초: 서량 말은 성질이 급합니다. 배불리 먹이고 달릴 길을 먼저 보여 주어야 힘을 냅니다.',
'유비: 우리 군영의 말과 사람도 그대의 길에 맞추어 배워야 하오. 북쪽 길은 이제 함께 열 길이오.',
'마구간에는 새로 들어온 서량 말의 숨소리가 가득했고, 병사들은 다음 출전 명단을 떠올리며 고삐를 고쳤습니다.'
],
choices: [
{
id: 'align-cavalry-feed',
label: '말먹이 기준을 맞춘다',
response: '서량 말의 먹이와 휴식 기준이 정리되자, 마초의 기병대가 유비군의 행군 속도에 맞춰 움직이기 시작했습니다.',
bondExp: 30,
itemRewards: ['콩 8', '탁주 3']
},
{
id: 'mark-cavalry-routes',
label: '기병 돌파로를 표시한다',
response: '마초가 지도 위에 기병이 달릴 수 있는 능선과 피해야 할 늪길을 표시했습니다.',
bondExp: 28,
gold: 520,
itemRewards: ['서량 말먹이 1']
}
]
},
{
id: 'hanzhong-front-council',
title: '한중 전선 회의',
location: '가맹관 본막',
availableAfterBattleIds: [campBattleIds.thirtyFourth],
bondId: 'zhao-yun__ma-chao',
description: '조운과 마초가 한중으로 이어지는 관문, 산길, 기병 운용을 놓고 다음 출전 구성을 논의합니다.',
lines: [
'조운: 가맹관을 넘었다고 길이 열린 것은 아닙니다. 좁은 산길에서는 빠른 말도 한 번 멈춰야 합니다.',
'마초: 멈출 곳을 알면 더 세게 달릴 수 있지요. 장로의 군세와 조조의 눈이 모두 북쪽 길에 걸려 있습니다.',
'본막의 지도 위에는 한중 전선과 촉한 건국의 다음 숙제가 한 줄로 이어졌습니다.'
],
choices: [
{
id: 'plan-hanzhong-scouting',
label: '한중 정찰을 준비한다',
response: '조운의 정찰 기준과 마초의 기병 감각이 합쳐져, 다음 장의 출전 후보가 더 분명해졌습니다.',
bondExp: 30,
itemRewards: ['한중 정찰도 1']
},
{
id: 'divide-pass-roles',
label: '관문 돌파 역할을 나눈다',
response: '조운은 후방을 지키고 마초는 전열을 찌르는 방식으로, 두 기병장의 역할이 정리되었습니다.',
bondExp: 28,
gold: 540,
itemRewards: ['가맹관 전술 기록 1']
}
]
}
];
@@ -4698,6 +4854,47 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.thirtyFourth) {
this.campaign = ensureCampaignRosterUnits(
[
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits,
...yizhouRecruitUnits,
...fuPassRecruitUnits,
...luoCastleRecruitUnits,
...luoCastleProperRecruitUnits,
...chengduPressureRecruitUnits,
...chengduSurrenderRecruitUnits,
...hanzhongOpeningRecruitUnits
],
[
...xuzhouRecruitBonds,
...caoBreakRecruitBonds,
...liuBiaoRecruitBonds,
...zhugeRecruitBonds,
...jingzhouRecruitBonds,
...guiyangRecruitBonds,
...wulingRecruitBonds,
...changshaRecruitBonds,
...yizhouRecruitBonds,
...fuPassRecruitBonds,
...luoCastleRecruitBonds,
...luoCastleProperRecruitBonds,
...chengduPressureRecruitBonds,
...chengduSurrenderRecruitBonds,
...hanzhongOpeningRecruitBonds
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (battleId === campBattleIds.thirtyThird) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -5050,6 +5247,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.thirtyFourth) {
return '가맹관 대면전 후 군영';
}
if (battleId === campBattleIds.thirtyThird) {
return '성도 항복 후 군영';
}

View File

@@ -18,6 +18,7 @@ import {
seventeenthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
thirtyFourthBattleScenario,
thirtyFirstBattleScenario,
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
@@ -365,7 +366,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'thirtieth-camp' ||
campaign.step === 'thirty-first-camp' ||
campaign.step === 'thirty-second-camp' ||
campaign.step === 'thirty-third-camp'
campaign.step === 'thirty-third-camp' ||
campaign.step === 'thirty-fourth-camp'
) {
this.scene.start('CampScene');
return;
@@ -536,6 +538,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'thirty-fourth-battle') {
this.scene.start('BattleScene', { battleId: thirtyFourthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -107,7 +107,9 @@ export type CampaignStep =
| 'thirty-second-battle'
| 'thirty-second-camp'
| 'thirty-third-battle'
| 'thirty-third-camp';
| 'thirty-third-camp'
| 'thirty-fourth-battle'
| 'thirty-fourth-camp';
export type CampaignUnitProgressSnapshot = {
unitId: string;
@@ -202,7 +204,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'thirtieth-battle-luofeng-ambush': { victory: 'thirtieth-camp', retry: 'thirtieth-battle' },
'thirty-first-battle-luo-main-gate': { victory: 'thirty-first-camp', retry: 'thirty-first-battle' },
'thirty-second-battle-mianzhu-gate': { victory: 'thirty-second-camp', retry: 'thirty-second-battle' },
'thirty-third-battle-chengdu-surrender': { victory: 'thirty-third-camp', retry: 'thirty-third-battle' }
'thirty-third-battle-chengdu-surrender': { victory: 'thirty-third-camp', retry: 'thirty-third-battle' },
'thirty-fourth-battle-jiameng-pass': { victory: 'thirty-fourth-camp', retry: 'thirty-fourth-battle' }
};
export type CampaignSaveSlotSummary = {