Add Guangzong camp battle and camp events
This commit is contained in:
@@ -20,11 +20,12 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
|||||||
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, class and terrain affinity rules, ally/enemy roster tabs, unit detail stat panels, weapon/armor/accessory equipment growth, terrain-cost movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, right-click tactical menu, turn ending/reset, resonance bond tracking, and grayscale acted-unit feedback
|
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, class and terrain affinity rules, ally/enemy roster tabs, unit detail stat panels, weapon/armor/accessory equipment growth, terrain-cost movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, right-click tactical menu, turn ending/reset, resonance bond tracking, and grayscale acted-unit feedback
|
||||||
- Second battle expansion with a larger scrollable map, mixed enemy AI behaviors, minimap navigation, camp progression, and save persistence
|
- Second battle expansion with a larger scrollable map, mixed enemy AI behaviors, minimap navigation, camp progression, and save persistence
|
||||||
- Third battle expansion path toward Guangzong with a new battlefield, story bridge, campaign step persistence, and title continue support
|
- Third battle expansion path toward Guangzong with a new battlefield, story bridge, campaign step persistence, and title continue support
|
||||||
- Flow verification script from title through the third battle victory and camp save state
|
- Fourth battle Guangzong main camp confrontation with Zhang Jue, expanded enemy formation, campaign persistence, and anti-Dong Zhuo setup
|
||||||
|
- Flow verification script from title through the fourth battle victory and camp save state
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Add the next Guangzong main battle and named Yellow Turban commanders
|
1. Start the anti-Dong Zhuo chapter with a story bridge and first coalition-era battle
|
||||||
2. Broaden camp events so bonds, supplies, and equipment choices matter between battles
|
2. Broaden camp events so bonds, supplies, and equipment choices matter between battles
|
||||||
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
||||||
4. Add more distinctive victory/defeat branches and optional objectives
|
4. Add more distinctive victory/defeat branches and optional objectives
|
||||||
|
|||||||
@@ -302,6 +302,18 @@ try {
|
|||||||
throw new Error(`Expected bean supply to be consumed: ${JSON.stringify(campStateAfterSupply.campaign?.inventory)}`);
|
throw new Error(`Expected bean supply to be consumed: ${JSON.stringify(campStateAfterSupply.campaign?.inventory)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goldBeforeMerchant = campStateAfterSupply.campaign?.gold ?? 0;
|
||||||
|
await page.mouse.click(702, 386);
|
||||||
|
await page.waitForTimeout(220);
|
||||||
|
|
||||||
|
const campStateAfterMerchant = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (
|
||||||
|
(campStateAfterMerchant.campaign?.inventory?.['콩'] ?? 0) !== 1 ||
|
||||||
|
campStateAfterMerchant.campaign?.gold !== goldBeforeMerchant - 40
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected merchant purchase to add bean and spend gold: ${JSON.stringify({ goldBeforeMerchant, campStateAfterMerchant })}`);
|
||||||
|
}
|
||||||
|
|
||||||
await page.mouse.click(830, 38);
|
await page.mouse.click(830, 38);
|
||||||
await page.waitForTimeout(120);
|
await page.waitForTimeout(120);
|
||||||
await page.mouse.click(974, 482);
|
await page.mouse.click(974, 482);
|
||||||
@@ -358,7 +370,7 @@ try {
|
|||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < 14; i += 1) {
|
for (let i = 0; i < 24; i += 1) {
|
||||||
const isSecondBattleActive = await page.evaluate(() => {
|
const isSecondBattleActive = await page.evaluate(() => {
|
||||||
const state = window.__HEROS_DEBUG__?.battle();
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
return state?.scene === 'BattleScene' && state?.battleId === 'second-battle-yellow-turban-pursuit';
|
return state?.scene === 'BattleScene' && state?.battleId === 'second-battle-yellow-turban-pursuit';
|
||||||
@@ -369,7 +381,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await page.keyboard.press('Space');
|
await page.keyboard.press('Space');
|
||||||
await page.waitForTimeout(220);
|
await page.waitForTimeout(320);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
@@ -430,7 +442,7 @@ try {
|
|||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < 14; i += 1) {
|
for (let i = 0; i < 24; i += 1) {
|
||||||
const isThirdBattleActive = await page.evaluate(() => {
|
const isThirdBattleActive = await page.evaluate(() => {
|
||||||
const state = window.__HEROS_DEBUG__?.battle();
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road';
|
return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road';
|
||||||
@@ -441,7 +453,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await page.keyboard.press('Space');
|
await page.keyboard.press('Space');
|
||||||
await page.waitForTimeout(220);
|
await page.waitForTimeout(320);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
@@ -490,6 +502,78 @@ try {
|
|||||||
throw new Error(`Expected campaign save to persist third battle victory: ${JSON.stringify(campaignSaveAfterThirdBattle)}`);
|
throw new Error(`Expected campaign save to persist third battle victory: ${JSON.stringify(campaignSaveAfterThirdBattle)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.mouse.click(1120, 38);
|
||||||
|
await page.waitForTimeout(160);
|
||||||
|
const thirdCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (!thirdCampSortieState?.sortieVisible) {
|
||||||
|
throw new Error(`Expected third camp sortie preparation overlay: ${JSON.stringify(thirdCampSortieState)}`);
|
||||||
|
}
|
||||||
|
await page.mouse.click(938, 596);
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
|
return activeScenes.includes('StoryScene');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 26; i += 1) {
|
||||||
|
const isFourthBattleActive = await page.evaluate(() => {
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return state?.scene === 'BattleScene' && state?.battleId === 'fourth-battle-guangzong-camp';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFourthBattleActive) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.keyboard.press('Space');
|
||||||
|
await page.waitForTimeout(320);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return state?.scene === 'BattleScene' && state?.battleId === 'fourth-battle-guangzong-camp' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: 'dist/verification-fourth-battle.png', fullPage: true });
|
||||||
|
|
||||||
|
const fourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||||
|
const fourthEnemies = fourthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||||
|
const fourthEnemyBehaviors = new Set(fourthEnemies.map((unit) => unit.ai));
|
||||||
|
if (
|
||||||
|
fourthBattleState.camera?.mapWidth !== 24 ||
|
||||||
|
fourthBattleState.camera?.mapHeight !== 20 ||
|
||||||
|
fourthBattleState.victoryConditionLabel !== '장각 격파' ||
|
||||||
|
fourthEnemies.length < 12 ||
|
||||||
|
!fourthEnemyBehaviors.has('aggressive') ||
|
||||||
|
!fourthEnemyBehaviors.has('guard') ||
|
||||||
|
!fourthEnemyBehaviors.has('hold')
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected fourth battle map, objective, and mixed enemy AI: ${JSON.stringify(fourthBattleState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.mouse.click(738, 642);
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
|
return activeScenes.includes('CampScene');
|
||||||
|
});
|
||||||
|
|
||||||
|
const campaignSaveAfterFourthBattle = await page.evaluate(() => {
|
||||||
|
const raw = window.localStorage.getItem('heros-web:campaign-state');
|
||||||
|
return raw ? JSON.parse(raw) : undefined;
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!campaignSaveAfterFourthBattle ||
|
||||||
|
campaignSaveAfterFourthBattle.step !== 'fourth-camp' ||
|
||||||
|
campaignSaveAfterFourthBattle.latestBattleId !== 'fourth-battle-guangzong-camp' ||
|
||||||
|
!campaignSaveAfterFourthBattle.battleHistory?.['fourth-battle-guangzong-camp']
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected campaign save to persist fourth battle victory: ${JSON.stringify(campaignSaveAfterFourthBattle)}`);
|
||||||
|
}
|
||||||
|
|
||||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
@@ -501,7 +585,7 @@ try {
|
|||||||
return activeScenes.includes('CampScene');
|
return activeScenes.includes('CampScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Verified title-to-third-battle flow, result states, and debug API at ${targetUrl}`);
|
console.log(`Verified title-to-fourth-battle flow, result states, and debug API at ${targetUrl}`);
|
||||||
} finally {
|
} finally {
|
||||||
await browser?.close();
|
await browser?.close();
|
||||||
if (serverProcess && !serverProcess.killed) {
|
if (serverProcess && !serverProcess.killed) {
|
||||||
|
|||||||
79
src/assets/images/battle/fourth-battle-map.svg
Normal file
79
src/assets/images/battle/fourth-battle-map.svg
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2400 2000" width="2400" height="2000">
|
||||||
|
<defs>
|
||||||
|
<filter id="grain">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.018" numOctaves="4" seed="64" />
|
||||||
|
<feColorMatrix type="saturate" values="0.45" />
|
||||||
|
<feBlend mode="multiply" in2="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
<pattern id="field" width="66" height="66" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="66" height="66" fill="#66784d" />
|
||||||
|
<path d="M9 20h20M37 44h22M45 14l13 8M17 54l16-11" stroke="#aab36b" stroke-width="3" opacity="0.25" />
|
||||||
|
<path d="M5 42l13-17M30 13l8-11M53 60l8-19" stroke="#344b31" stroke-width="2" opacity="0.29" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="earth" width="74" height="74" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="74" height="74" fill="#967045" />
|
||||||
|
<path d="M5 18c23-12 44 8 64-3M0 51c25-14 50 12 70-2" stroke="#c89a5a" stroke-width="5" opacity="0.26" />
|
||||||
|
<path d="M18 64l9-15M43 30l16-10M60 60l8-5" stroke="#5c472e" stroke-width="3" opacity="0.34" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="forest" width="98" height="84" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="98" height="84" fill="#31462c" />
|
||||||
|
<circle cx="23" cy="28" r="23" fill="#263a24" />
|
||||||
|
<circle cx="50" cy="24" r="26" fill="#4d6a3b" />
|
||||||
|
<circle cx="74" cy="35" r="23" fill="#2a4027" />
|
||||||
|
<circle cx="42" cy="59" r="22" fill="#5d7b43" />
|
||||||
|
<path d="M14 46c22 13 53 12 74-5M30 22c15 14 38 13 53-4" stroke="#142516" stroke-width="5" opacity="0.36" />
|
||||||
|
</pattern>
|
||||||
|
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#397590" />
|
||||||
|
<stop offset="0.56" stop-color="#24516d" />
|
||||||
|
<stop offset="1" stop-color="#17394f" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="fortWall" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#75624c" />
|
||||||
|
<stop offset="1" stop-color="#433629" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="2400" height="2000" fill="url(#field)" />
|
||||||
|
<g filter="url(#grain)">
|
||||||
|
<path d="M-130 1080C210 964 435 982 690 850C910 736 980 580 1190 498C1422 408 1648 456 1880 322C2056 220 2218 72 2520-20" fill="none" stroke="#a27a48" stroke-width="238" stroke-linecap="round" opacity="0.96" />
|
||||||
|
<path d="M-130 1080C210 964 435 982 690 850C910 736 980 580 1190 498C1422 408 1648 456 1880 322C2056 220 2218 72 2520-20" fill="none" stroke="url(#earth)" stroke-width="168" stroke-linecap="round" opacity="0.93" />
|
||||||
|
<path d="M-80 1680C160 1470 458 1328 744 1210C1010 1102 1214 1020 1378 820C1526 638 1670 512 2020 460" fill="none" stroke="#997044" stroke-width="196" stroke-linecap="round" opacity="0.92" />
|
||||||
|
<path d="M-80 1680C160 1470 458 1328 744 1210C1010 1102 1214 1020 1378 820C1526 638 1670 512 2020 460" fill="none" stroke="url(#earth)" stroke-width="132" stroke-linecap="round" opacity="0.9" />
|
||||||
|
|
||||||
|
<path d="M1210 430c-68 270 5 450 168 630c155 170 188 346 140 566c-36 166-30 312 40 474" fill="none" stroke="#15384e" stroke-width="260" stroke-linecap="round" opacity="0.94" />
|
||||||
|
<path d="M1210 430c-68 270 5 450 168 630c155 170 188 346 140 566c-36 166-30 312 40 474" fill="none" stroke="url(#river)" stroke-width="178" stroke-linecap="round" opacity="0.98" />
|
||||||
|
<path d="M1244 574c78 90 90 198 36 324M1424 1140c90 112 112 236 60 380M1532 1712c32 110 28 200-12 296" fill="none" stroke="#91d1df" stroke-width="14" opacity="0.35" />
|
||||||
|
|
||||||
|
<path d="M-30 0h620c96 84 134 198 112 334C470 392 210 350-30 286Z" fill="url(#forest)" opacity="0.92" />
|
||||||
|
<path d="M740 70c210-76 400-42 570 100c20 142-54 258-225 350c-195-28-318-150-345-450Z" fill="url(#forest)" opacity="0.8" />
|
||||||
|
<path d="M60 590c310-120 565-74 762 112c-56 212-284 340-672 354C20 928-10 774 60 590Z" fill="url(#forest)" opacity="0.88" />
|
||||||
|
<path d="M1900 480c226-84 404-56 548 66v390c-220 74-430 30-620-132c-38-128-14-236 72-324Z" fill="url(#forest)" opacity="0.84" />
|
||||||
|
<path d="M610 1560c300-122 570-84 792 92c-42 210-252 328-620 346c-156-94-214-234-172-438Z" fill="url(#forest)" opacity="0.82" />
|
||||||
|
|
||||||
|
<g transform="translate(1390 80)">
|
||||||
|
<rect x="0" y="0" width="460" height="260" rx="18" fill="url(#fortWall)" />
|
||||||
|
<rect x="34" y="44" width="392" height="178" fill="#6f5940" stroke="#2a2118" stroke-width="12" />
|
||||||
|
<path d="M34 44h392M34 222h392M34 44v178M426 44v178" stroke="#33281e" stroke-width="10" />
|
||||||
|
<rect x="74" y="94" width="84" height="92" fill="#93704a" />
|
||||||
|
<rect x="192" y="70" width="92" height="116" fill="#98744d" />
|
||||||
|
<rect x="318" y="96" width="60" height="90" fill="#8d6742" />
|
||||||
|
<path d="M58 94l58-48l62 48M176 70l62-50l70 50M304 96l44-38l54 38" fill="#c18b38" stroke="#302316" stroke-width="9" />
|
||||||
|
<path d="M214 186v-62h46v62" fill="#201813" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(810 900)">
|
||||||
|
<rect x="0" y="0" width="230" height="170" rx="13" fill="#584432" />
|
||||||
|
<rect x="32" y="48" width="70" height="88" fill="#8a6848" />
|
||||||
|
<rect x="130" y="52" width="64" height="84" fill="#8a6848" />
|
||||||
|
<path d="M16 48l52-42l56 42M120 52l42-34l50 34" fill="#be8431" stroke="#302215" stroke-width="9" />
|
||||||
|
</g>
|
||||||
|
<g transform="translate(1720 560)">
|
||||||
|
<rect x="0" y="0" width="230" height="160" rx="12" fill="#604b36" />
|
||||||
|
<rect x="32" y="46" width="68" height="80" fill="#96704a" />
|
||||||
|
<rect x="128" y="34" width="72" height="92" fill="#96704a" />
|
||||||
|
<path d="M16 46l52-40l54 40M118 34l52-38l60 38" fill="#c58b32" stroke="#322317" stroke-width="9" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="2400" height="2000" fill="#0b0d12" opacity="0.08" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -8,6 +8,10 @@ import {
|
|||||||
secondBattleUnits,
|
secondBattleUnits,
|
||||||
secondBattleVictoryPages,
|
secondBattleVictoryPages,
|
||||||
thirdBattleBonds,
|
thirdBattleBonds,
|
||||||
|
fourthBattleBonds,
|
||||||
|
fourthBattleMap,
|
||||||
|
fourthBattleUnits,
|
||||||
|
fourthBattleVictoryPages,
|
||||||
thirdBattleMap,
|
thirdBattleMap,
|
||||||
thirdBattleUnits,
|
thirdBattleUnits,
|
||||||
thirdBattleVictoryPages,
|
thirdBattleVictoryPages,
|
||||||
@@ -17,7 +21,11 @@ import {
|
|||||||
type UnitData
|
type UnitData
|
||||||
} from './scenario';
|
} from './scenario';
|
||||||
|
|
||||||
export type BattleScenarioId = 'first-battle-zhuo-commandery' | 'second-battle-yellow-turban-pursuit' | 'third-battle-guangzong-road';
|
export type BattleScenarioId =
|
||||||
|
| 'first-battle-zhuo-commandery'
|
||||||
|
| 'second-battle-yellow-turban-pursuit'
|
||||||
|
| 'third-battle-guangzong-road'
|
||||||
|
| 'fourth-battle-guangzong-camp';
|
||||||
|
|
||||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||||
|
|
||||||
@@ -215,12 +223,66 @@ export const thirdBattleScenario: BattleScenarioDefinition = {
|
|||||||
nextCampScene: 'CampScene'
|
nextCampScene: 'CampScene'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fourthBattleScenario: BattleScenarioDefinition = {
|
||||||
|
id: 'fourth-battle-guangzong-camp',
|
||||||
|
title: '광종 본영전',
|
||||||
|
victoryConditionLabel: '장각 격파',
|
||||||
|
defeatConditionLabel: '유비 퇴각',
|
||||||
|
openingObjectiveLines: [
|
||||||
|
'황건 본영의 중심에는 장각이 있습니다. 장각을 격파하면 광종의 황건 세력은 무너집니다.',
|
||||||
|
'요새와 숲의 궁병이 진입로를 막고 있습니다. 측면의 기병과 중앙 수비병을 나누어 상대하십시오.',
|
||||||
|
'16턴 이내에 승리하면 황건적 토벌의 공적이 크게 오릅니다.'
|
||||||
|
],
|
||||||
|
map: fourthBattleMap,
|
||||||
|
units: fourthBattleUnits,
|
||||||
|
bonds: fourthBattleBonds,
|
||||||
|
mapTextureKey: 'battle-map-fourth',
|
||||||
|
leaderUnitId: 'guangzong-main-leader-zhang-jue',
|
||||||
|
quickVictoryTurnLimit: 16,
|
||||||
|
baseVictoryGold: 700,
|
||||||
|
objectives: [
|
||||||
|
{
|
||||||
|
id: 'leader',
|
||||||
|
kind: 'defeat-leader',
|
||||||
|
label: '장각 격파',
|
||||||
|
rewardGold: 420,
|
||||||
|
unitId: 'guangzong-main-leader-zhang-jue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-bei',
|
||||||
|
kind: 'keep-unit-alive',
|
||||||
|
label: '유비 생존',
|
||||||
|
rewardGold: 180,
|
||||||
|
unitId: 'liu-bei'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fort',
|
||||||
|
kind: 'secure-terrain',
|
||||||
|
label: '광종 본영 확보',
|
||||||
|
rewardGold: 260,
|
||||||
|
terrain: 'fort'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick',
|
||||||
|
kind: 'quick-victory',
|
||||||
|
label: '16턴 이내 승리',
|
||||||
|
rewardGold: 220,
|
||||||
|
maxTurn: 16
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
|
||||||
|
itemRewards: ['콩 2', '상처약 1', '탁주 1', '황건 토벌 공적 +1'],
|
||||||
|
victoryPages: fourthBattleVictoryPages,
|
||||||
|
nextCampScene: 'CampScene'
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
||||||
|
|
||||||
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
||||||
'first-battle-zhuo-commandery': firstBattleScenario,
|
'first-battle-zhuo-commandery': firstBattleScenario,
|
||||||
'second-battle-yellow-turban-pursuit': secondBattleScenario,
|
'second-battle-yellow-turban-pursuit': secondBattleScenario,
|
||||||
'third-battle-guangzong-road': thirdBattleScenario
|
'third-battle-guangzong-road': thirdBattleScenario,
|
||||||
|
'fourth-battle-guangzong-camp': fourthBattleScenario
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { CampaignStep } from '../state/campaignState';
|
import type { CampaignStep } from '../state/campaignState';
|
||||||
import { defaultBattleScenario, secondBattleScenario, thirdBattleScenario, type BattleScenarioId } from './battles';
|
import { defaultBattleScenario, fourthBattleScenario, secondBattleScenario, thirdBattleScenario, type BattleScenarioId } from './battles';
|
||||||
import {
|
import {
|
||||||
firstBattleVictoryPages,
|
firstBattleVictoryPages,
|
||||||
|
fourthBattleIntroPages,
|
||||||
|
fourthBattleVictoryPages,
|
||||||
secondBattleIntroPages,
|
secondBattleIntroPages,
|
||||||
secondBattleVictoryPages,
|
secondBattleVictoryPages,
|
||||||
thirdBattleIntroPages,
|
thirdBattleIntroPages,
|
||||||
|
thirdBattleVictoryPages,
|
||||||
type StoryPage
|
type StoryPage
|
||||||
} from './scenario';
|
} from './scenario';
|
||||||
|
|
||||||
@@ -43,12 +46,22 @@ const sortieFlows: Record<string, SortieFlow> = {
|
|||||||
},
|
},
|
||||||
[thirdBattleScenario.id]: {
|
[thirdBattleScenario.id]: {
|
||||||
afterBattleId: thirdBattleScenario.id,
|
afterBattleId: thirdBattleScenario.id,
|
||||||
|
eyebrow: '다음 전장',
|
||||||
|
title: fourthBattleScenario.title,
|
||||||
|
description: '광종 구원로를 연 의용군은 이제 황건 본영을 향합니다. 장각을 격파해 황건적 토벌의 큰 고비를 넘겨야 합니다.',
|
||||||
|
rewardHint: `예상 보상: ${fourthBattleScenario.title} 개방`,
|
||||||
|
nextBattleId: fourthBattleScenario.id,
|
||||||
|
campaignStep: 'fourth-battle',
|
||||||
|
pages: [...thirdBattleVictoryPages, ...fourthBattleIntroPages]
|
||||||
|
},
|
||||||
|
[fourthBattleScenario.id]: {
|
||||||
|
afterBattleId: fourthBattleScenario.id,
|
||||||
eyebrow: '다음 장 준비',
|
eyebrow: '다음 장 준비',
|
||||||
title: '광종 본전',
|
title: '반동탁의 흐름',
|
||||||
description: '광종 구원로는 열렸지만 황건 본대는 아직 건재합니다. 다음 작업에서 광종 본전과 더 큰 전장 목표를 이어 붙일 수 있습니다.',
|
description: '황건적 토벌로 이름을 알린 세 형제 앞에 조정의 새로운 혼란이 다가옵니다. 다음 큰 줄기는 반동탁 흐름으로 이어집니다.',
|
||||||
rewardHint: '다음 장: 광종 본전 준비 중',
|
rewardHint: '다음 장: 반동탁 흐름 준비 중',
|
||||||
pages: [],
|
pages: [],
|
||||||
unavailableNotice: '광종 본전은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'
|
unavailableNotice: '반동탁 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,69 @@ export const thirdBattleVictoryPages: StoryPage[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const fourthBattleIntroPages: StoryPage[] = [
|
||||||
|
{
|
||||||
|
id: 'fourth-guangzong-camp',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '광종 본영',
|
||||||
|
background: 'story-militia',
|
||||||
|
text: '전령의 길이 끊기자 황건 본대는 광종 들판에 진을 굳혔다. 노란 깃발이 겹겹이 서고, 장각의 이름이 병사들 사이에 낮게 울렸다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fourth-liu-bei-resolve',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '의용군의 결의',
|
||||||
|
background: 'story-liu-bei',
|
||||||
|
speaker: '유비',
|
||||||
|
portrait: 'liuBei',
|
||||||
|
text: '장각을 치는 일은 한 사람을 꺾는 일이 아니다. 두려움에 몰린 백성들이 다시 길을 찾게 하는 일이다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fourth-guan-yu-line',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '본영 돌파',
|
||||||
|
background: 'story-three-heroes',
|
||||||
|
speaker: '관우',
|
||||||
|
portrait: 'guanYu',
|
||||||
|
text: '적은 요새와 숲 사이에 궁병을 숨겼습니다. 전열을 흐트러뜨리지 않고 본영의 깃발까지 밀고 들어가야 합니다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fourth-zhang-fei-roar',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '황건적 토벌',
|
||||||
|
background: 'story-sortie',
|
||||||
|
speaker: '장비',
|
||||||
|
portrait: 'zhangFei',
|
||||||
|
text: '좋소! 광종의 누런 깃발을 오늘 모두 뽑아 버립시다. 뒤돌아볼 틈 없이 밀고 들어가겠소!'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const fourthBattleVictoryPages: StoryPage[] = [
|
||||||
|
{
|
||||||
|
id: 'fourth-victory-yellow-flags-fall',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '황건적 토벌',
|
||||||
|
background: 'story-militia',
|
||||||
|
text: '광종의 들판에서 황건의 본영이 무너졌다. 피난민들은 오래 닫았던 문을 열고, 쓰러진 깃발 너머로 관군과 의용군을 바라보았다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fourth-victory-liu-bei',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '이름이 퍼지다',
|
||||||
|
background: 'story-liu-bei',
|
||||||
|
speaker: '유비',
|
||||||
|
portrait: 'liuBei',
|
||||||
|
text: '난은 잠시 가라앉았지만, 천하가 바로 선 것은 아니다. 오늘의 공이 교만이 되지 않도록 마음을 다잡자.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fourth-victory-next-era',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '새로운 혼란',
|
||||||
|
background: 'story-sortie',
|
||||||
|
text: '황건의 불길이 잦아들 무렵, 조정 안에서는 또 다른 그림자가 자라났다. 세 형제의 길은 이제 반동탁의 흐름으로 이어진다.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const firstBattleMap: BattleMap = {
|
export const firstBattleMap: BattleMap = {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 18,
|
height: 18,
|
||||||
@@ -390,6 +453,33 @@ export const thirdBattleMap: BattleMap = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fourthBattleMap: BattleMap = {
|
||||||
|
width: 24,
|
||||||
|
height: 20,
|
||||||
|
terrain: [
|
||||||
|
['forest', 'forest', 'hill', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'hill', 'plain', 'plain', 'fort', 'fort', 'fort', 'plain', 'hill', 'hill', 'forest', 'forest', 'plain', 'plain'],
|
||||||
|
['forest', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'forest', 'hill', 'plain', 'plain', 'fort', 'fort', 'fort', 'plain', 'plain', 'hill', 'forest', 'forest', 'plain', 'plain', 'hill'],
|
||||||
|
['hill', 'plain', 'plain', 'forest', 'plain', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'road', 'road', 'fort', 'plain', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'hill'],
|
||||||
|
['plain', 'plain', 'forest', 'forest', 'plain', 'road', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain'],
|
||||||
|
['plain', 'forest', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'hill', 'plain', 'river', 'river', 'road', 'road', 'plain', 'hill', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain'],
|
||||||
|
['plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'road', 'plain', 'village', 'plain', 'plain', 'forest', 'forest', 'plain'],
|
||||||
|
['plain', 'forest', 'plain', 'plain', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'river', 'river', 'plain', 'plain', 'road', 'plain', 'village', 'plain', 'forest', 'forest', 'plain', 'plain'],
|
||||||
|
['plain', 'forest', 'plain', 'road', 'road', 'plain', 'hill', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'hill', 'plain'],
|
||||||
|
['plain', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'road', 'plain', 'plain', 'hill', 'hill', 'plain', 'plain'],
|
||||||
|
['plain', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'village', 'plain', 'plain', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain'],
|
||||||
|
['road', 'road', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'village', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'road', 'road', 'plain', 'plain', 'plain', 'plain', 'plain'],
|
||||||
|
['plain', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'road', 'road', 'plain', 'forest', 'plain', 'plain'],
|
||||||
|
['plain', 'road', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain', 'hill'],
|
||||||
|
['hill', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'hill', 'hill'],
|
||||||
|
['hill', 'road', 'road', 'plain', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'hill'],
|
||||||
|
['plain', 'camp', 'road', 'road', 'plain', 'hill', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain'],
|
||||||
|
['camp', 'camp', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain'],
|
||||||
|
['plain', 'camp', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'plain'],
|
||||||
|
['plain', 'plain', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain'],
|
||||||
|
['plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain']
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
export const firstBattleUnits: UnitData[] = [
|
export const firstBattleUnits: UnitData[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-bei',
|
id: 'liu-bei',
|
||||||
@@ -1194,6 +1284,270 @@ export const thirdBattleUnits: UnitData[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const fourthBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||||
|
'liu-bei': { x: 2, y: 16 },
|
||||||
|
'guan-yu': { x: 3, y: 16 },
|
||||||
|
'zhang-fei': { x: 2, y: 17 }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fourthBattleUnits: UnitData[] = [
|
||||||
|
...firstBattleUnits
|
||||||
|
.filter((unit) => unit.faction === 'ally')
|
||||||
|
.map((unit) => placeScenarioUnit(unit, fourthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-vanguard-a',
|
||||||
|
name: '황건 선봉',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '황건병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 4,
|
||||||
|
exp: 24,
|
||||||
|
hp: 28,
|
||||||
|
maxHp: 28,
|
||||||
|
attack: 9,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 63, intelligence: 36, leadership: 48, agility: 53, luck: 45 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 24 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 10 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 6,
|
||||||
|
y: 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-vanguard-b',
|
||||||
|
name: '황건 선봉',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '황건병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 4,
|
||||||
|
exp: 24,
|
||||||
|
hp: 28,
|
||||||
|
maxHp: 28,
|
||||||
|
attack: 9,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 63, intelligence: 36, leadership: 48, agility: 53, luck: 45 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 24 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 10 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 8,
|
||||||
|
y: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-bandit-a',
|
||||||
|
name: '숲길 돌격병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '도적',
|
||||||
|
classKey: 'bandit',
|
||||||
|
level: 4,
|
||||||
|
exp: 26,
|
||||||
|
hp: 29,
|
||||||
|
maxHp: 29,
|
||||||
|
attack: 10,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 66, intelligence: 34, leadership: 44, agility: 61, luck: 48 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 24 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 10 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 5,
|
||||||
|
y: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-archer-a',
|
||||||
|
name: '황건 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 4,
|
||||||
|
exp: 26,
|
||||||
|
hp: 24,
|
||||||
|
maxHp: 24,
|
||||||
|
attack: 10,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 60, intelligence: 48, leadership: 50, agility: 62, luck: 50 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 1, exp: 26 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 1, exp: 10 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 9,
|
||||||
|
y: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-cavalry-a',
|
||||||
|
name: '황건 기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '경기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 5,
|
||||||
|
exp: 28,
|
||||||
|
hp: 32,
|
||||||
|
maxHp: 32,
|
||||||
|
attack: 11,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 70, intelligence: 38, leadership: 52, agility: 72, luck: 51 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 28 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 12,
|
||||||
|
y: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-guard-a',
|
||||||
|
name: '본영 수비병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '황건병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 5,
|
||||||
|
exp: 28,
|
||||||
|
hp: 31,
|
||||||
|
maxHp: 31,
|
||||||
|
attack: 10,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 68, intelligence: 38, leadership: 54, agility: 55, luck: 49 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 28 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 14,
|
||||||
|
y: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-archer-b',
|
||||||
|
name: '본영 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 5,
|
||||||
|
exp: 30,
|
||||||
|
hp: 25,
|
||||||
|
maxHp: 25,
|
||||||
|
attack: 10,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 61, intelligence: 50, leadership: 51, agility: 63, luck: 51 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 1, exp: 30 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 16,
|
||||||
|
y: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-cavalry-b',
|
||||||
|
name: '황건 기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '경기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 5,
|
||||||
|
exp: 30,
|
||||||
|
hp: 33,
|
||||||
|
maxHp: 33,
|
||||||
|
attack: 11,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 71, intelligence: 38, leadership: 53, agility: 73, luck: 51 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 30 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 18,
|
||||||
|
y: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-guard-b',
|
||||||
|
name: '본영 수비병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '황건병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 5,
|
||||||
|
exp: 30,
|
||||||
|
hp: 32,
|
||||||
|
maxHp: 32,
|
||||||
|
attack: 10,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 69, intelligence: 39, leadership: 55, agility: 55, luck: 50 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 30 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 18,
|
||||||
|
y: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-guard-c',
|
||||||
|
name: '본영 수비병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '황건병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 5,
|
||||||
|
exp: 30,
|
||||||
|
hp: 32,
|
||||||
|
maxHp: 32,
|
||||||
|
attack: 10,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 69, intelligence: 39, leadership: 55, agility: 55, luck: 50 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 30 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 14,
|
||||||
|
y: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-archer-c',
|
||||||
|
name: '망루 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 5,
|
||||||
|
exp: 32,
|
||||||
|
hp: 26,
|
||||||
|
maxHp: 26,
|
||||||
|
attack: 11,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 62, intelligence: 52, leadership: 52, agility: 64, luck: 52 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 2, exp: 6 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 16,
|
||||||
|
y: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guangzong-main-leader-zhang-jue',
|
||||||
|
name: '장각',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '황건 대현량사',
|
||||||
|
classKey: 'rebelLeader',
|
||||||
|
level: 6,
|
||||||
|
exp: 40,
|
||||||
|
hp: 48,
|
||||||
|
maxHp: 48,
|
||||||
|
attack: 13,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 74, intelligence: 78, leadership: 82, agility: 58, luck: 64 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'leader-axe', level: 2, exp: 16 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 4 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 15,
|
||||||
|
y: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const firstBattleBonds: BattleBond[] = [
|
export const firstBattleBonds: BattleBond[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-bei__guan-yu',
|
id: 'liu-bei__guan-yu',
|
||||||
@@ -1223,6 +1577,7 @@ export const firstBattleBonds: BattleBond[] = [
|
|||||||
|
|
||||||
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||||
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||||
|
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||||
|
|
||||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
||||||
|
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
|
||||||
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
|
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
|
||||||
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
||||||
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
||||||
@@ -64,6 +65,7 @@ export class BootScene extends Phaser.Scene {
|
|||||||
this.load.image('battle-map-first', firstBattleMapUrl);
|
this.load.image('battle-map-first', firstBattleMapUrl);
|
||||||
this.load.image('battle-map-second', secondBattleMapUrl);
|
this.load.image('battle-map-second', secondBattleMapUrl);
|
||||||
this.load.image('battle-map-third', thirdBattleMapUrl);
|
this.load.image('battle-map-third', thirdBattleMapUrl);
|
||||||
|
this.load.image('battle-map-fourth', fourthBattleMapUrl);
|
||||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ type CampDialogue = {
|
|||||||
bondId: string;
|
bondId: string;
|
||||||
rewardExp: number;
|
rewardExp: number;
|
||||||
lines: string[];
|
lines: string[];
|
||||||
|
choices: CampDialogueChoice[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CampDialogueChoice = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
response: string;
|
||||||
|
rewardExp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CampTabButtonView = {
|
type CampTabButtonView = {
|
||||||
@@ -41,6 +49,13 @@ type CampSupplyDefinition = {
|
|||||||
bondExp: number;
|
bondExp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MerchantItemDefinition = {
|
||||||
|
label: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
type SortieChecklistItem = {
|
type SortieChecklistItem = {
|
||||||
label: string;
|
label: string;
|
||||||
complete: boolean;
|
complete: boolean;
|
||||||
@@ -73,11 +88,39 @@ const campSupplies: CampSupplyDefinition[] = [
|
|||||||
description: '선택 장수의 병력을 8 회복하고 연결된 공명 경험치를 2 올립니다.',
|
description: '선택 장수의 병력을 8 회복하고 연결된 공명 경험치를 2 올립니다.',
|
||||||
healHp: 8,
|
healHp: 8,
|
||||||
bondExp: 2
|
bondExp: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '상처약',
|
||||||
|
title: '상처약',
|
||||||
|
description: '선택 장수의 병력을 22 회복합니다.',
|
||||||
|
healHp: 22,
|
||||||
|
bondExp: 0
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, supply]));
|
const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, supply]));
|
||||||
|
|
||||||
|
const merchantItems: MerchantItemDefinition[] = [
|
||||||
|
{
|
||||||
|
label: '콩',
|
||||||
|
title: '콩',
|
||||||
|
description: '값싸고 가벼운 회복 도구입니다.',
|
||||||
|
price: 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '탁주',
|
||||||
|
title: '탁주',
|
||||||
|
description: '병력 회복과 작은 공명 상승에 유용합니다.',
|
||||||
|
price: 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '상처약',
|
||||||
|
title: '상처약',
|
||||||
|
description: '큰 부상을 빠르게 회복합니다.',
|
||||||
|
price: 110
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const campDialogues: CampDialogue[] = [
|
const campDialogues: CampDialogue[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-guan-after-first-battle',
|
id: 'liu-guan-after-first-battle',
|
||||||
@@ -89,6 +132,20 @@ const campDialogues: CampDialogue[] = [
|
|||||||
'유비: 운장, 오늘 그대가 앞을 막아 주지 않았다면 백성들을 지키지 못했을 것이오.',
|
'유비: 운장, 오늘 그대가 앞을 막아 주지 않았다면 백성들을 지키지 못했을 것이오.',
|
||||||
'관우: 형님의 뜻이 분명했기에 칼을 들 수 있었습니다. 다음 싸움에서도 길을 열겠습니다.',
|
'관우: 형님의 뜻이 분명했기에 칼을 들 수 있었습니다. 다음 싸움에서도 길을 열겠습니다.',
|
||||||
'유비: 우리의 맹세가 전장에서 더 단단해지는구려.'
|
'유비: 우리의 맹세가 전장에서 더 단단해지는구려.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'trust-vanguard',
|
||||||
|
label: '앞길을 맡긴다',
|
||||||
|
response: '관우는 말없이 고개를 숙였다. 다음 전장에서도 형님의 길을 열겠다는 결의가 깊어졌다.',
|
||||||
|
rewardExp: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'protect-people',
|
||||||
|
label: '백성 보호를 당부한다',
|
||||||
|
response: '관우는 칼끝보다 백성의 숨결을 먼저 살피겠다고 답했다. 두 사람의 뜻이 한층 가까워졌다.',
|
||||||
|
rewardExp: 4
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,6 +158,20 @@ const campDialogues: CampDialogue[] = [
|
|||||||
'장비: 형님, 다음엔 제가 먼저 뛰쳐나가 두령 놈을 단번에 꺾겠습니다!',
|
'장비: 형님, 다음엔 제가 먼저 뛰쳐나가 두령 놈을 단번에 꺾겠습니다!',
|
||||||
'유비: 익덕의 용맹은 든든하나, 백성이 뒤에 있음을 잊지 말아야 하오.',
|
'유비: 익덕의 용맹은 든든하나, 백성이 뒤에 있음을 잊지 말아야 하오.',
|
||||||
'장비: 하하! 형님 말이라면 참아 보겠습니다. 대신 싸울 때는 크게 치겠습니다.'
|
'장비: 하하! 형님 말이라면 참아 보겠습니다. 대신 싸울 때는 크게 치겠습니다.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'temper-courage',
|
||||||
|
label: '용맹을 다독인다',
|
||||||
|
response: '장비는 답답하다는 듯 웃었지만, 형님의 말에 맞춰 창을 거두는 법도 익히겠다고 했다.',
|
||||||
|
rewardExp: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lead-charge',
|
||||||
|
label: '선봉을 약속한다',
|
||||||
|
response: '장비의 눈빛이 밝아졌다. 믿고 맡긴다는 말은 그에게 무엇보다 큰 격려였다.',
|
||||||
|
rewardExp: 7
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -113,6 +184,20 @@ const campDialogues: CampDialogue[] = [
|
|||||||
'관우: 익덕, 적이 흩어질 때 너무 깊이 들어가면 형님이 걱정하신다.',
|
'관우: 익덕, 적이 흩어질 때 너무 깊이 들어가면 형님이 걱정하신다.',
|
||||||
'장비: 운장 형님이 옆을 받쳐 준다면 걱정할 일이 없지 않습니까?',
|
'장비: 운장 형님이 옆을 받쳐 준다면 걱정할 일이 없지 않습니까?',
|
||||||
'관우: 좋다. 다음 전장에서는 서로의 빈틈을 먼저 살피자.'
|
'관우: 좋다. 다음 전장에서는 서로의 빈틈을 먼저 살피자.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'cover-flanks',
|
||||||
|
label: '서로의 측면을 맡긴다',
|
||||||
|
response: '칼과 창이 같은 호흡으로 움직일 길을 찾았다. 두 맹장의 공명이 단단해졌다.',
|
||||||
|
rewardExp: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compete-boldly',
|
||||||
|
label: '누가 먼저 베는지 겨룬다',
|
||||||
|
response: '장비가 크게 웃고 관우도 미소를 감추지 못했다. 경쟁심마저 전장의 힘이 되었다.',
|
||||||
|
rewardExp: 4
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -613,7 +698,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(1, palette.gold, 0.58);
|
bg.setStrokeStyle(1, palette.gold, 0.58);
|
||||||
this.track(this.add.text(x + 24, y + 22, '장수 대화', this.textStyle(24, '#f2e3bf', true)));
|
this.track(this.add.text(x + 24, y + 22, '장수 대화', this.textStyle(24, '#f2e3bf', true)));
|
||||||
this.track(this.add.text(x + 24, y + 56, '대화를 보면 해당 장수들의 공명 경험치가 오릅니다.', this.textStyle(14, '#d4dce6')));
|
this.track(this.add.text(x + 24, y + 56, '출진 전 대화에서 선택지를 고르면 해당 장수들의 공명 경험치가 오릅니다.', this.textStyle(14, '#d4dce6')));
|
||||||
|
|
||||||
campDialogues.forEach((dialogue, index) => {
|
campDialogues.forEach((dialogue, index) => {
|
||||||
const completed = this.completedCampDialogues().includes(dialogue.id);
|
const completed = this.completedCampDialogues().includes(dialogue.id);
|
||||||
@@ -629,7 +714,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.render();
|
this.render();
|
||||||
});
|
});
|
||||||
this.track(this.add.text(x + 38, rowY + 8, dialogue.title, this.textStyle(15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
this.track(this.add.text(x + 38, rowY + 8, dialogue.title, this.textStyle(15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
||||||
this.track(this.add.text(x + 38, rowY + 29, `${this.unitName(dialogue.unitIds[0])} · ${this.unitName(dialogue.unitIds[1])} 공명 +${dialogue.rewardExp}`, this.textStyle(12, '#9fb0bf')));
|
const maxReward = dialogue.rewardExp + Math.max(...dialogue.choices.map((choice) => choice.rewardExp));
|
||||||
|
this.track(this.add.text(x + 38, rowY + 29, `${this.unitName(dialogue.unitIds[0])} · ${this.unitName(dialogue.unitIds[1])} 공명 +${dialogue.rewardExp}~${maxReward}`, this.textStyle(12, '#9fb0bf')));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderSelectedDialogue(x + 372, y + 96, 416, 300);
|
this.renderSelectedDialogue(x + 372, y + 96, 416, 300);
|
||||||
@@ -651,19 +737,29 @@ export class CampScene extends Phaser.Scene {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const buttonLabel = completed ? '대화 완료' : `대화 보기 공명 +${dialogue.rewardExp}`;
|
if (completed) {
|
||||||
const button = this.track(this.add.rectangle(x + width / 2, y + height - 34, 220, 34, completed ? 0x17231d : 0x1a2630, 0.96));
|
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 220, 34, 0x17231d, 0.96));
|
||||||
button.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.76);
|
done.setStrokeStyle(1, palette.green, 0.76);
|
||||||
if (!completed) {
|
const text = this.track(this.add.text(x + width / 2, y + height - 34, '대화 완료', this.textStyle(15, '#a8ffd0', true)));
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogue.choices.forEach((choice, index) => {
|
||||||
|
const choiceY = y + height - 80 + index * 42;
|
||||||
|
const button = this.track(this.add.rectangle(x + width / 2, choiceY, width - 36, 34, 0x1a2630, 0.96));
|
||||||
|
button.setStrokeStyle(1, palette.gold, 0.76);
|
||||||
button.setInteractive({ useHandCursor: true });
|
button.setInteractive({ useHandCursor: true });
|
||||||
button.on('pointerdown', () => this.completeDialogue(dialogue));
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
||||||
}
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
||||||
const text = this.track(this.add.text(x + width / 2, y + height - 34, buttonLabel, this.textStyle(15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
button.on('pointerdown', () => this.completeDialogue(dialogue, choice));
|
||||||
text.setOrigin(0.5);
|
|
||||||
if (!completed) {
|
const label = `${choice.label} 공명 +${dialogue.rewardExp + choice.rewardExp}`;
|
||||||
|
const text = this.track(this.add.text(x + width / 2, choiceY, label, this.textStyle(14, '#f2e3bf', true)));
|
||||||
|
text.setOrigin(0.5);
|
||||||
text.setInteractive({ useHandCursor: true });
|
text.setInteractive({ useHandCursor: true });
|
||||||
text.on('pointerdown', () => this.completeDialogue(dialogue));
|
text.on('pointerdown', () => this.completeDialogue(dialogue, choice));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderBondList(x: number, y: number, width: number) {
|
private renderBondList(x: number, y: number, width: number) {
|
||||||
@@ -695,30 +791,70 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.renderSupplyUseRow(supply, x + 390, y + 94 + index * 74, 390);
|
this.renderSupplyUseRow(supply, x + 390, y + 94 + index * 74, 390);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.track(this.add.text(x + 24, y + 202, '전리품과 명성', this.textStyle(19, '#f2e3bf', true)));
|
this.renderMerchantPanel(x + 24, y + 202, 342);
|
||||||
|
|
||||||
|
this.track(this.add.text(x + 390, y + 330, '전리품과 명성', this.textStyle(19, '#f2e3bf', true)));
|
||||||
const trophies = this.nonConsumableInventoryLabels();
|
const trophies = this.nonConsumableInventoryLabels();
|
||||||
if (trophies.length === 0) {
|
if (trophies.length === 0) {
|
||||||
this.track(this.add.text(x + 24, y + 236, '소모품 외 전리품 없음', this.textStyle(13, '#9fb0bf')));
|
this.track(this.add.text(x + 390, y + 364, '소모품 외 전리품 없음', this.textStyle(13, '#9fb0bf')));
|
||||||
} else {
|
} else {
|
||||||
trophies.forEach((reward, index) => {
|
trophies.forEach((reward, index) => {
|
||||||
this.renderSupplyBox(reward, x + 24 + index * 154, y + 232);
|
this.renderSupplyBox(reward, x + 390 + index * 128, y + 360);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.track(this.add.text(x + 24, y + 306, '부대 장비 요약', this.textStyle(19, '#f2e3bf', true)));
|
private renderMerchantPanel(x: number, y: number, width: number) {
|
||||||
this.currentUnits()
|
const campaign = this.campaign ?? getCampaignState();
|
||||||
.filter((unit) => unit.faction === 'ally')
|
const bg = this.track(this.add.rectangle(x, y, width, 210, 0x0d141c, 0.92));
|
||||||
.forEach((unit, index) => {
|
bg.setOrigin(0);
|
||||||
const rowY = y + 342 + index * 30;
|
bg.setStrokeStyle(1, palette.gold, 0.48);
|
||||||
this.track(this.add.text(x + 24, rowY, unit.name, this.textStyle(14, '#f2e3bf', true)));
|
this.track(this.add.text(x + 16, y + 10, `군영 상인 · 군자금 ${campaign.gold}`, this.textStyle(17, '#f2e3bf', true)));
|
||||||
equipmentSlots.forEach((slot, slotIndex) => {
|
|
||||||
const state = unit.equipment[slot];
|
merchantItems.forEach((item, index) => {
|
||||||
const item = getItem(state.itemId);
|
this.renderMerchantItem(item, x + 16, y + 44 + index * 52, width - 32);
|
||||||
const slotX = x + 104 + slotIndex * 220;
|
});
|
||||||
this.track(this.add.text(slotX, rowY - 2, `${equipmentSlotLabels[slot]} ${item.name} Lv${state.level}`, this.textStyle(11, '#d4dce6')));
|
}
|
||||||
this.drawBar(slotX, rowY + 17, 176, 6, state.exp / equipmentExpToNext(state.level), item.rank === 'treasure' ? palette.gold : palette.blue);
|
|
||||||
});
|
private renderMerchantItem(item: MerchantItemDefinition, x: number, y: number, width: number) {
|
||||||
});
|
const campaign = this.campaign ?? getCampaignState();
|
||||||
|
const canBuy = campaign.gold >= item.price;
|
||||||
|
const bg = this.track(this.add.rectangle(x, y, width, 40, canBuy ? 0x151f2a : 0x111820, canBuy ? 0.94 : 0.76));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, canBuy ? palette.gold : 0x53606c, canBuy ? 0.58 : 0.36);
|
||||||
|
this.track(this.add.text(x + 10, y + 6, item.title, this.textStyle(13, canBuy ? '#f2e3bf' : '#7f8994', true)));
|
||||||
|
this.track(this.add.text(x + 10, y + 23, `${item.price}금`, this.textStyle(11, canBuy ? '#d8b15f' : '#7f8994', true)));
|
||||||
|
|
||||||
|
const button = this.track(this.add.rectangle(x + width - 42, y + 20, 60, 26, canBuy ? 0x1a2630 : 0x121922, canBuy ? 0.96 : 0.72));
|
||||||
|
button.setStrokeStyle(1, canBuy ? palette.gold : 0x53606c, canBuy ? 0.72 : 0.4);
|
||||||
|
const text = this.track(this.add.text(x + width - 42, y + 20, '구매', this.textStyle(12, canBuy ? '#f2e3bf' : '#7f8994', true)));
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
|
||||||
|
if (canBuy) {
|
||||||
|
const action = () => this.buyMerchantItem(item);
|
||||||
|
button.setInteractive({ useHandCursor: true });
|
||||||
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
||||||
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
||||||
|
button.on('pointerdown', action);
|
||||||
|
text.setInteractive({ useHandCursor: true });
|
||||||
|
text.on('pointerdown', action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buyMerchantItem(item: MerchantItemDefinition) {
|
||||||
|
const campaign = this.campaign ?? getCampaignState();
|
||||||
|
if (campaign.gold < item.price) {
|
||||||
|
this.showCampNotice('군자금이 부족합니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
campaign.gold -= item.price;
|
||||||
|
campaign.inventory[item.label] = (campaign.inventory[item.label] ?? 0) + 1;
|
||||||
|
this.campaign = saveCampaignState(campaign);
|
||||||
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||||
|
soundDirector.playSelect();
|
||||||
|
this.showCampNotice(`${item.title} 구입 · 군자금 -${item.price}`);
|
||||||
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSupplyBox(label: string, x: number, y: number) {
|
private renderSupplyBox(label: string, x: number, y: number) {
|
||||||
@@ -932,8 +1068,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return bonuses.join(' / ') || '특수 보정 없음';
|
return bonuses.join(' / ') || '특수 보정 없음';
|
||||||
}
|
}
|
||||||
|
|
||||||
private completeDialogue(dialogue: CampDialogue) {
|
private completeDialogue(dialogue: CampDialogue, choice: CampDialogueChoice) {
|
||||||
const updated = applyCampBondExp(dialogue.id, dialogue.bondId, dialogue.rewardExp);
|
const rewardExp = dialogue.rewardExp + choice.rewardExp;
|
||||||
|
const updated = applyCampBondExp(dialogue.id, dialogue.bondId, rewardExp);
|
||||||
if (updated) {
|
if (updated) {
|
||||||
this.report = updated;
|
this.report = updated;
|
||||||
this.campaign = getCampaignState();
|
this.campaign = getCampaignState();
|
||||||
@@ -941,27 +1078,32 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.report.completedCampDialogues.push(dialogue.id);
|
this.report.completedCampDialogues.push(dialogue.id);
|
||||||
const bond = this.report.bonds.find((candidate) => candidate.id === dialogue.bondId);
|
const bond = this.report.bonds.find((candidate) => candidate.id === dialogue.bondId);
|
||||||
if (bond) {
|
if (bond) {
|
||||||
bond.battleExp += dialogue.rewardExp;
|
bond.battleExp += rewardExp;
|
||||||
bond.exp += dialogue.rewardExp;
|
bond.exp += rewardExp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
soundDirector.playEffect('exp-gain', { volume: 0.28, stopAfterMs: 700 });
|
soundDirector.playEffect('exp-gain', { volume: 0.28, stopAfterMs: 700 });
|
||||||
this.showDialogueReward(dialogue);
|
this.showDialogueReward(dialogue, choice, rewardExp);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private showDialogueReward(dialogue: CampDialogue) {
|
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
||||||
this.showCampNotice(`${dialogue.title} 완료 · 공명 +${dialogue.rewardExp}`);
|
this.showCampNotice(`${choice.label} · 공명 +${rewardExp}`);
|
||||||
|
this.time.delayedCall(120, () => this.showCampNotice(choice.response));
|
||||||
}
|
}
|
||||||
|
|
||||||
private showCampNotice(message: string) {
|
private showCampNotice(message: string) {
|
||||||
this.dialogueObjects.forEach((object) => object.destroy());
|
this.dialogueObjects.forEach((object) => object.destroy());
|
||||||
this.dialogueObjects = [];
|
this.dialogueObjects = [];
|
||||||
const box = this.add.rectangle(this.scale.width / 2, 104, 440, 52, 0x101820, 0.96);
|
const box = this.add.rectangle(this.scale.width / 2, 104, 720, 58, 0x101820, 0.96);
|
||||||
box.setStrokeStyle(2, palette.gold, 0.84);
|
box.setStrokeStyle(2, palette.gold, 0.84);
|
||||||
box.setDepth(30);
|
box.setDepth(30);
|
||||||
const text = this.add.text(this.scale.width / 2, 104, message, this.textStyle(17, '#f2e3bf', true));
|
const text = this.add.text(this.scale.width / 2, 104, message, {
|
||||||
|
...this.textStyle(16, '#f2e3bf', true),
|
||||||
|
align: 'center',
|
||||||
|
wordWrap: { width: 670, useAdvancedWrap: true }
|
||||||
|
});
|
||||||
text.setOrigin(0.5);
|
text.setOrigin(0.5);
|
||||||
text.setDepth(31);
|
text.setDepth(31);
|
||||||
this.dialogueObjects.push(box, text);
|
this.dialogueObjects.push(box, text);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { soundDirector } from '../audio/SoundDirector';
|
import { soundDirector } from '../audio/SoundDirector';
|
||||||
import { firstBattleVictoryPages } from '../data/scenario';
|
import { firstBattleVictoryPages } from '../data/scenario';
|
||||||
import { secondBattleScenario, thirdBattleScenario } from '../data/battles';
|
import { fourthBattleScenario, secondBattleScenario, thirdBattleScenario } from '../data/battles';
|
||||||
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
||||||
import { palette } from '../ui/palette';
|
import { palette } from '../ui/palette';
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ export class TitleScene extends Phaser.Scene {
|
|||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
|
|
||||||
const campaign = loadCampaignState();
|
const campaign = loadCampaignState();
|
||||||
if (campaign.step === 'first-camp' || campaign.step === 'second-camp' || campaign.step === 'third-camp') {
|
if (campaign.step === 'first-camp' || campaign.step === 'second-camp' || campaign.step === 'third-camp' || campaign.step === 'fourth-camp') {
|
||||||
this.scene.start('CampScene');
|
this.scene.start('CampScene');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,6 +319,11 @@ export class TitleScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (campaign.step === 'fourth-battle') {
|
||||||
|
this.scene.start('BattleScene', { battleId: fourthBattleScenario.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (campaign.step === 'first-victory-story') {
|
if (campaign.step === 'first-victory-story') {
|
||||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ export type CampaignStep =
|
|||||||
| 'second-battle'
|
| 'second-battle'
|
||||||
| 'second-camp'
|
| 'second-camp'
|
||||||
| 'third-battle'
|
| 'third-battle'
|
||||||
| 'third-camp';
|
| 'third-camp'
|
||||||
|
| 'fourth-battle'
|
||||||
|
| 'fourth-camp';
|
||||||
|
|
||||||
export type CampaignUnitProgressSnapshot = {
|
export type CampaignUnitProgressSnapshot = {
|
||||||
unitId: string;
|
unitId: string;
|
||||||
@@ -98,7 +100,8 @@ export const campaignSaveSlotCount = 3;
|
|||||||
const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: CampaignStep }> = {
|
const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: CampaignStep }> = {
|
||||||
'first-battle-zhuo-commandery': { victory: 'first-camp', retry: 'first-battle' },
|
'first-battle-zhuo-commandery': { victory: 'first-camp', retry: 'first-battle' },
|
||||||
'second-battle-yellow-turban-pursuit': { victory: 'second-camp', retry: 'second-battle' },
|
'second-battle-yellow-turban-pursuit': { victory: 'second-camp', retry: 'second-battle' },
|
||||||
'third-battle-guangzong-road': { victory: 'third-camp', retry: 'third-battle' }
|
'third-battle-guangzong-road': { victory: 'third-camp', retry: 'third-battle' },
|
||||||
|
'fourth-battle-guangzong-camp': { victory: 'fourth-camp', retry: 'fourth-battle' }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CampaignSaveSlotSummary = {
|
export type CampaignSaveSlotSummary = {
|
||||||
|
|||||||
Reference in New Issue
Block a user