Enhance tactical sortie preparation
This commit is contained in:
@@ -29,6 +29,7 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
|||||||
- Xu Province camp recruitment for Jian Yong and Mi Zhu, including new strategist/quartermaster roles, bond dialogue, and selectable sortie participation
|
- Xu Province camp recruitment for Jian Yong and Mi Zhu, including new strategist/quartermaster roles, bond dialogue, and selectable sortie participation
|
||||||
- Eighth battle Xiaopei supply-road defense, where the expanded Xu Province roster can be selected into battle and Lu Bu's refuge arc is foreshadowed
|
- Eighth battle Xiaopei supply-road defense, where the expanded Xu Province roster can be selected into battle and Lu Bu's refuge arc is foreshadowed
|
||||||
- Ninth battle Xuzhou gate night raid, opening Lu Bu's refuge and the internal instability that leads toward the loss of Xu Province
|
- Ninth battle Xuzhou gate night raid, opening Lu Bu's refuge and the internal instability that leads toward the loss of Xu Province
|
||||||
|
- Tactical sortie preparation panel with class role, named equipment, core stats, bond partner, and next-map terrain suitability for each deployable officer
|
||||||
- Flow verification script from title through the ninth battle victory, recruit sortie selection, and camp save state
|
- Flow verification script from title through the ninth battle victory, recruit sortie selection, and camp save state
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|||||||
@@ -360,9 +360,10 @@ try {
|
|||||||
if (!campStateWithSortiePrep?.sortieVisible) {
|
if (!campStateWithSortiePrep?.sortieVisible) {
|
||||||
throw new Error(`Expected sortie preparation overlay from next story button: ${JSON.stringify(campStateWithSortiePrep)}`);
|
throw new Error(`Expected sortie preparation overlay from next story button: ${JSON.stringify(campStateWithSortiePrep)}`);
|
||||||
}
|
}
|
||||||
|
assertSortieTacticalRoster(campStateWithSortiePrep, ['liu-bei', 'guan-yu', 'zhang-fei']);
|
||||||
await page.screenshot({ path: 'dist/verification-camp-sortie.png', fullPage: true });
|
await page.screenshot({ path: 'dist/verification-camp-sortie.png', fullPage: true });
|
||||||
|
|
||||||
await page.mouse.click(792, 596);
|
await page.mouse.click(922, 646);
|
||||||
await page.waitForTimeout(140);
|
await page.waitForTimeout(140);
|
||||||
const campStateAfterSortieClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
const campStateAfterSortieClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
if (campStateAfterSortieClose?.sortieVisible) {
|
if (campStateAfterSortieClose?.sortieVisible) {
|
||||||
@@ -371,7 +372,7 @@ try {
|
|||||||
|
|
||||||
await page.mouse.click(1120, 38);
|
await page.mouse.click(1120, 38);
|
||||||
await page.waitForTimeout(120);
|
await page.waitForTimeout(120);
|
||||||
await page.mouse.click(938, 596);
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -443,7 +444,7 @@ try {
|
|||||||
if (!secondCampSortieState?.sortieVisible) {
|
if (!secondCampSortieState?.sortieVisible) {
|
||||||
throw new Error(`Expected second camp sortie preparation overlay: ${JSON.stringify(secondCampSortieState)}`);
|
throw new Error(`Expected second camp sortie preparation overlay: ${JSON.stringify(secondCampSortieState)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -515,7 +516,7 @@ try {
|
|||||||
if (!thirdCampSortieState?.sortieVisible) {
|
if (!thirdCampSortieState?.sortieVisible) {
|
||||||
throw new Error(`Expected third camp sortie preparation overlay: ${JSON.stringify(thirdCampSortieState)}`);
|
throw new Error(`Expected third camp sortie preparation overlay: ${JSON.stringify(thirdCampSortieState)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -587,7 +588,7 @@ try {
|
|||||||
if (!fourthCampSortieState?.sortieVisible) {
|
if (!fourthCampSortieState?.sortieVisible) {
|
||||||
throw new Error(`Expected fourth camp sortie preparation overlay: ${JSON.stringify(fourthCampSortieState)}`);
|
throw new Error(`Expected fourth camp sortie preparation overlay: ${JSON.stringify(fourthCampSortieState)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -669,7 +670,7 @@ try {
|
|||||||
if (!fifthCampSortieState?.sortieVisible) {
|
if (!fifthCampSortieState?.sortieVisible) {
|
||||||
throw new Error(`Expected fifth camp sortie preparation overlay: ${JSON.stringify(fifthCampSortieState)}`);
|
throw new Error(`Expected fifth camp sortie preparation overlay: ${JSON.stringify(fifthCampSortieState)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -754,6 +755,7 @@ try {
|
|||||||
if (!sixthCampSortieState.selectedSortieUnitIds?.includes('liu-bei') || sixthCampSortieState.selectedSortieUnitIds.length < 3) {
|
if (!sixthCampSortieState.selectedSortieUnitIds?.includes('liu-bei') || sixthCampSortieState.selectedSortieUnitIds.length < 3) {
|
||||||
throw new Error(`Expected sortie prep to default to the core brother roster: ${JSON.stringify(sixthCampSortieState)}`);
|
throw new Error(`Expected sortie prep to default to the core brother roster: ${JSON.stringify(sixthCampSortieState)}`);
|
||||||
}
|
}
|
||||||
|
assertSortieTacticalRoster(sixthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei']);
|
||||||
|
|
||||||
await page.mouse.click(254, 450);
|
await page.mouse.click(254, 450);
|
||||||
await page.waitForTimeout(220);
|
await page.waitForTimeout(220);
|
||||||
@@ -766,7 +768,7 @@ try {
|
|||||||
) {
|
) {
|
||||||
throw new Error(`Expected sortie selection toggle to bench Guan Yu while keeping Liu Bei/Zhang Fei: ${JSON.stringify(sixthCampAfterSortieToggle)}`);
|
throw new Error(`Expected sortie selection toggle to bench Guan Yu while keeping Liu Bei/Zhang Fei: ${JSON.stringify(sixthCampAfterSortieToggle)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -862,7 +864,8 @@ try {
|
|||||||
if (!seventhCampRecruitSortieState.selectedSortieUnitIds?.includes('jian-yong')) {
|
if (!seventhCampRecruitSortieState.selectedSortieUnitIds?.includes('jian-yong')) {
|
||||||
throw new Error(`Expected recruit to be selectable in seventh camp sortie prep: ${JSON.stringify(seventhCampRecruitSortieState)}`);
|
throw new Error(`Expected recruit to be selectable in seventh camp sortie prep: ${JSON.stringify(seventhCampRecruitSortieState)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
assertSortieTacticalRoster(seventhCampRecruitSortieState, ['liu-bei', 'zhang-fei', 'jian-yong', 'mi-zhu']);
|
||||||
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -944,7 +947,8 @@ try {
|
|||||||
if (!eighthCampMiZhuSortieState.selectedSortieUnitIds?.includes('mi-zhu')) {
|
if (!eighthCampMiZhuSortieState.selectedSortieUnitIds?.includes('mi-zhu')) {
|
||||||
throw new Error(`Expected Mi Zhu to be selectable for ninth battle sortie: ${JSON.stringify(eighthCampMiZhuSortieState)}`);
|
throw new Error(`Expected Mi Zhu to be selectable for ninth battle sortie: ${JSON.stringify(eighthCampMiZhuSortieState)}`);
|
||||||
}
|
}
|
||||||
await page.mouse.click(938, 596);
|
assertSortieTacticalRoster(eighthCampMiZhuSortieState, ['liu-bei', 'zhang-fei', 'jian-yong', 'mi-zhu']);
|
||||||
|
await page.mouse.click(1068, 646);
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return activeScenes.includes('StoryScene');
|
return activeScenes.includes('StoryScene');
|
||||||
@@ -1076,3 +1080,34 @@ async function canReach(url) {
|
|||||||
function delay(ms) {
|
function delay(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertSortieTacticalRoster(state, requiredUnitIds) {
|
||||||
|
if (!state?.sortieVisible || !Array.isArray(state.sortieRoster)) {
|
||||||
|
throw new Error(`Expected sortie tactical roster to be exposed: ${JSON.stringify(state)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingIds = requiredUnitIds.filter((unitId) => !state.sortieRoster.some((unit) => unit.id === unitId));
|
||||||
|
if (missingIds.length > 0) {
|
||||||
|
throw new Error(`Expected sortie roster to include ${missingIds.join(', ')}: ${JSON.stringify(state.sortieRoster)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidUnits = state.sortieRoster.filter((unit) => {
|
||||||
|
return (
|
||||||
|
!unit.classRole ||
|
||||||
|
!unit.statLine?.includes('무 ') ||
|
||||||
|
!unit.statLine?.includes('지 ') ||
|
||||||
|
!unit.equipmentLine?.includes('Lv') ||
|
||||||
|
!unit.bondLine?.includes('공명') ||
|
||||||
|
!unit.terrainLine?.includes('지형')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invalidUnits.length > 0) {
|
||||||
|
throw new Error(`Expected sortie roster to include class, stats, equipment, bond, and terrain advice: ${JSON.stringify(invalidUnits)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const liuBei = state.sortieRoster.find((unit) => unit.id === 'liu-bei');
|
||||||
|
if (!liuBei?.equipmentLine?.includes('자웅일대검')) {
|
||||||
|
throw new Error(`Expected Liu Bei sortie row to expose named treasure weapon: ${JSON.stringify(liuBei)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { soundDirector } from '../audio/SoundDirector';
|
import { soundDirector } from '../audio/SoundDirector';
|
||||||
import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type EquipmentSlot } from '../data/battleItems';
|
import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type EquipmentSlot } from '../data/battleItems';
|
||||||
import { getUnitClass } from '../data/battleRules';
|
import { getUnitClass, terrainRules, type TerrainType } from '../data/battleRules';
|
||||||
import { defaultBattleScenario } from '../data/battles';
|
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition } from '../data/battles';
|
||||||
import { getSortieFlow } from '../data/campaignFlow';
|
import { getSortieFlow } from '../data/campaignFlow';
|
||||||
import { firstBattleBonds, firstBattleUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, type PortraitKey, type UnitData } from '../data/scenario';
|
import { firstBattleBonds, firstBattleUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, type PortraitKey, type UnitData } from '../data/scenario';
|
||||||
import {
|
import {
|
||||||
@@ -64,6 +64,13 @@ type SortieChecklistItem = {
|
|||||||
detail: string;
|
detail: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SortieUnitTacticalSummary = {
|
||||||
|
statLine: string;
|
||||||
|
equipmentLine: string;
|
||||||
|
bondLine: string;
|
||||||
|
terrainLine: string;
|
||||||
|
};
|
||||||
|
|
||||||
const portraitKeys: Record<PortraitKey, string> = {
|
const portraitKeys: Record<PortraitKey, string> = {
|
||||||
liuBei: 'portrait-liu-bei',
|
liuBei: 'portrait-liu-bei',
|
||||||
guanYu: 'portrait-guan-yu',
|
guanYu: 'portrait-guan-yu',
|
||||||
@@ -1159,8 +1166,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
||||||
|
|
||||||
const depth = 40;
|
const depth = 40;
|
||||||
const width = 900;
|
const width = 1160;
|
||||||
const height = 560;
|
const height = 660;
|
||||||
const x = Math.floor((this.scale.width - width) / 2);
|
const x = Math.floor((this.scale.width - width) / 2);
|
||||||
const y = Math.floor((this.scale.height - height) / 2);
|
const y = Math.floor((this.scale.height - height) / 2);
|
||||||
|
|
||||||
@@ -1181,10 +1188,10 @@ export class CampScene extends Phaser.Scene {
|
|||||||
);
|
);
|
||||||
subtitle.setDepth(depth + 2);
|
subtitle.setDepth(depth + 2);
|
||||||
|
|
||||||
this.renderSortieBriefing(x + 34, y + 112, 396, 152, depth + 2);
|
this.renderSortieBriefing(x + 34, y + 112, 520, 178, depth + 2);
|
||||||
this.renderSortieChecklist(x + 462, y + 112, 404, 170, depth + 2);
|
this.renderSortieChecklist(x + 576, y + 112, 550, 178, depth + 2);
|
||||||
this.renderSortieUnitSummary(x + 34, y + 292, 832, 176, depth + 2);
|
this.renderSortieUnitSummary(x + 34, y + 310, 1092, 252, depth + 2);
|
||||||
this.renderSortieRewardHint(x + 34, y + 480, 548, 52, depth + 2);
|
this.renderSortieRewardHint(x + 34, y + 576, 696, 52, depth + 2);
|
||||||
|
|
||||||
this.addSortieButton('군영으로', x + width - 298, y + height - 44, 124, () => {
|
this.addSortieButton('군영으로', x + width - 298, y + height - 44, 124, () => {
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
@@ -1239,8 +1246,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
bg.setStrokeStyle(1, palette.blue, 0.48);
|
bg.setStrokeStyle(1, palette.blue, 0.48);
|
||||||
const allies = this.sortieAllies();
|
const allies = this.sortieAllies();
|
||||||
const selectedCount = this.selectedSortieUnitIds.length;
|
const selectedCount = this.selectedSortieUnitIds.length;
|
||||||
const rowGap = allies.length > 4 ? 24 : 30;
|
const rowGap = allies.length > 4 ? 32 : 36;
|
||||||
const rowHeight = allies.length > 4 ? 22 : 26;
|
const rowHeight = allies.length > 4 ? 31 : 32;
|
||||||
this.trackSortie(
|
this.trackSortie(
|
||||||
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${Math.min(maxSortieUnits, allies.length)}`, this.textStyle(18, '#f2e3bf', true))
|
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${Math.min(maxSortieUnits, allies.length)}`, this.textStyle(18, '#f2e3bf', true))
|
||||||
).setDepth(depth + 1);
|
).setDepth(depth + 1);
|
||||||
@@ -1250,6 +1257,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const rowY = y + 48 + index * rowGap;
|
const rowY = y + 48 + index * rowGap;
|
||||||
const selected = this.isSortieSelected(unit.id);
|
const selected = this.isSortieSelected(unit.id);
|
||||||
const required = requiredSortieUnitIds.has(unit.id);
|
const required = requiredSortieUnitIds.has(unit.id);
|
||||||
|
const summary = this.sortieUnitTacticalSummary(unit);
|
||||||
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82));
|
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82));
|
||||||
row.setOrigin(0);
|
row.setOrigin(0);
|
||||||
row.setDepth(depth + 1);
|
row.setDepth(depth + 1);
|
||||||
@@ -1260,20 +1268,123 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const marker = selected ? '출전' : '대기';
|
const marker = selected ? '출전' : '대기';
|
||||||
this.trackSortie(this.add.text(x + 30, rowY, required ? '필수' : marker, this.textStyle(12, required ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2);
|
this.trackSortie(this.add.text(x + 30, rowY, required ? '필수' : marker, this.textStyle(12, required ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2);
|
||||||
this.trackSortie(this.add.text(x + 84, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
|
this.trackSortie(this.add.text(x + 84, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
|
||||||
this.trackSortie(this.add.text(x + 198, rowY, unit.className, this.textStyle(13, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
this.trackSortie(this.add.text(x + 218, rowY, `${unit.className} · ${getUnitClass(unit.classKey).role}`, this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
||||||
this.trackSortie(this.add.text(x + 310, rowY, `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(13, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
this.trackSortie(this.add.text(x + 462, rowY, `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
||||||
this.drawSortieBar(x + 414, rowY + 6, 102, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2);
|
this.drawSortieBar(x + 554, rowY + 6, 86, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2);
|
||||||
|
this.trackSortie(this.add.text(x + 666, rowY, summary.terrainLine, this.textStyle(11, selected ? '#d8b15f' : '#77818c', true))).setDepth(depth + 2);
|
||||||
|
|
||||||
const equipment = equipmentSlots
|
this.renderSortieEquipmentIcons(unit, x + 84, rowY + 19, selected, depth + 2);
|
||||||
.map((slot) => {
|
this.trackSortie(this.add.text(x + 162, rowY + 18, summary.equipmentLine, this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2);
|
||||||
const state = unit.equipment[slot];
|
this.trackSortie(this.add.text(x + 522, rowY + 18, summary.statLine, this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2);
|
||||||
return `${equipmentSlotLabels[slot]} Lv${state.level}`;
|
this.trackSortie(this.add.text(x + 760, rowY + 18, summary.bondLine, this.textStyle(11, selected ? '#a8ffd0' : '#68727e', selected))).setDepth(depth + 2);
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
this.trackSortie(this.add.text(x + 542, rowY, equipment, this.textStyle(12, selected ? '#d4dce6' : '#77818c'))).setDepth(depth + 2);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderSortieEquipmentIcons(unit: UnitData, x: number, y: number, selected: boolean, depth: number) {
|
||||||
|
equipmentSlots.forEach((slot, index) => {
|
||||||
|
const state = unit.equipment[slot];
|
||||||
|
const item = getItem(state.itemId);
|
||||||
|
const iconFrame = this.trackSortie(this.add.rectangle(x + index * 23, y, 18, 18, selected && item.rank === 'treasure' ? 0x2c2630 : 0x0a1017, 0.92));
|
||||||
|
iconFrame.setStrokeStyle(1, item.rank === 'treasure' ? palette.gold : 0x53606c, selected ? 0.62 : 0.34);
|
||||||
|
iconFrame.setDepth(depth);
|
||||||
|
const icon = this.trackSortie(this.add.image(x + index * 23, y, `item-${item.id}`));
|
||||||
|
icon.setDisplaySize(14, 14);
|
||||||
|
icon.setAlpha(selected ? 1 : 0.52);
|
||||||
|
icon.setDepth(depth + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortieUnitTacticalSummary(unit: UnitData): SortieUnitTacticalSummary {
|
||||||
|
return {
|
||||||
|
statLine: `무 ${unit.stats.might} 지 ${unit.stats.intelligence} 통 ${unit.stats.leadership} 민 ${unit.stats.agility} 운 ${unit.stats.luck}`,
|
||||||
|
equipmentLine: this.sortieEquipmentLine(unit),
|
||||||
|
bondLine: this.sortieBondLine(unit),
|
||||||
|
terrainLine: this.sortieTerrainLine(unit)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortieEquipmentLine(unit: UnitData) {
|
||||||
|
return equipmentSlots
|
||||||
|
.map((slot) => {
|
||||||
|
const state = unit.equipment[slot];
|
||||||
|
const item = getItem(state.itemId);
|
||||||
|
return `${item.name} Lv${state.level}`;
|
||||||
|
})
|
||||||
|
.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortieBondLine(unit: UnitData) {
|
||||||
|
const selected = new Set(this.selectedSortieUnitIds);
|
||||||
|
const activeBonds = this.currentBonds()
|
||||||
|
.filter((bond) => bond.unitIds.includes(unit.id))
|
||||||
|
.map((bond) => {
|
||||||
|
const partnerId = bond.unitIds.find((unitId) => unitId !== unit.id) ?? bond.unitIds[0];
|
||||||
|
return {
|
||||||
|
bond,
|
||||||
|
partnerId,
|
||||||
|
partnerName: this.unitName(partnerId),
|
||||||
|
partnerSelected: selected.has(partnerId)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => Number(b.partnerSelected) - Number(a.partnerSelected) || b.bond.level - a.bond.level || b.bond.exp - a.bond.exp);
|
||||||
|
|
||||||
|
const deployedBond = activeBonds.find((entry) => entry.partnerSelected);
|
||||||
|
if (deployedBond) {
|
||||||
|
return `공명 ${deployedBond.partnerName} Lv${deployedBond.bond.level}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strongestBond = activeBonds[0];
|
||||||
|
if (strongestBond) {
|
||||||
|
return `대기 공명 ${strongestBond.partnerName} Lv${strongestBond.bond.level}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '공명 관계 없음';
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortieTerrainLine(unit: UnitData) {
|
||||||
|
const scenario = this.nextSortieScenario();
|
||||||
|
if (!scenario) {
|
||||||
|
return `${getUnitClass(unit.classKey).role}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terrainCounts = this.passableTerrainCounts(scenario);
|
||||||
|
const unitClass = getUnitClass(unit.classKey);
|
||||||
|
const entries = Object.entries(terrainCounts) as [TerrainType, number][];
|
||||||
|
const total = entries.reduce((sum, [, count]) => sum + count, 0);
|
||||||
|
if (total <= 0) {
|
||||||
|
return `${unitClass.role}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const weightedScore =
|
||||||
|
entries.reduce((sum, [terrain, count]) => sum + (unitClass.terrainRatings[terrain] ?? 0) * count, 0) / total;
|
||||||
|
const grade = weightedScore >= 110 ? '최적' : weightedScore >= 102 ? '유리' : weightedScore >= 95 ? '보통' : '주의';
|
||||||
|
const bestTerrains = entries
|
||||||
|
.sort((a, b) => (unitClass.terrainRatings[b[0]] ?? 0) - (unitClass.terrainRatings[a[0]] ?? 0) || b[1] - a[1])
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(([terrain]) => terrainRules[terrain].label)
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
return `지형 ${grade}${bestTerrains ? ` · ${bestTerrains}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private passableTerrainCounts(scenario: BattleScenarioDefinition) {
|
||||||
|
const counts = {} as Record<TerrainType, number>;
|
||||||
|
scenario.map.terrain.forEach((row) => {
|
||||||
|
row.forEach((terrain) => {
|
||||||
|
if (terrainRules[terrain].passable === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
counts[terrain] = (counts[terrain] ?? 0) + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextSortieScenario() {
|
||||||
|
const nextBattleId = getSortieFlow(this.campaign?.latestBattleId).nextBattleId;
|
||||||
|
return nextBattleId ? getBattleScenario(nextBattleId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private renderSortieRewardHint(x: number, y: number, width: number, height: number, depth: number) {
|
private renderSortieRewardHint(x: number, y: number, width: number, height: number, depth: number) {
|
||||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.9));
|
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.9));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
@@ -2143,6 +2254,20 @@ export class CampScene extends Phaser.Scene {
|
|||||||
selectedUnitId: this.selectedUnitId,
|
selectedUnitId: this.selectedUnitId,
|
||||||
selectedDialogueId: this.selectedDialogueId,
|
selectedDialogueId: this.selectedDialogueId,
|
||||||
selectedSortieUnitIds: [...this.selectedSortieUnitIds],
|
selectedSortieUnitIds: [...this.selectedSortieUnitIds],
|
||||||
|
sortieRoster: this.sortieAllies().map((unit) => {
|
||||||
|
const summary = this.sortieUnitTacticalSummary(unit);
|
||||||
|
return {
|
||||||
|
id: unit.id,
|
||||||
|
name: unit.name,
|
||||||
|
selected: this.isSortieSelected(unit.id),
|
||||||
|
className: unit.className,
|
||||||
|
classRole: getUnitClass(unit.classKey).role,
|
||||||
|
statLine: summary.statLine,
|
||||||
|
equipmentLine: summary.equipmentLine,
|
||||||
|
bondLine: summary.bondLine,
|
||||||
|
terrainLine: summary.terrainLine
|
||||||
|
};
|
||||||
|
}),
|
||||||
campBattleId: this.currentCampBattleId(),
|
campBattleId: this.currentCampBattleId(),
|
||||||
campTitle: this.currentCampTitle(),
|
campTitle: this.currentCampTitle(),
|
||||||
availableDialogueIds: this.availableCampDialogues().map((dialogue) => dialogue.id),
|
availableDialogueIds: this.availableCampDialogues().map((dialogue) => dialogue.id),
|
||||||
|
|||||||
Reference in New Issue
Block a user