diff --git a/frontend/src/pages/OptimizerPage.tsx b/frontend/src/pages/OptimizerPage.tsx index 054215e..f974ab7 100644 --- a/frontend/src/pages/OptimizerPage.tsx +++ b/frontend/src/pages/OptimizerPage.tsx @@ -8,6 +8,7 @@ import { Divider, InputNumber, Layout, + Modal, Pagination, Row, Select, @@ -18,68 +19,27 @@ import { import {AppstoreOutlined, FilterOutlined, ReloadOutlined, UserOutlined} from '@ant-design/icons'; import * as App from '../../wailsjs/go/service/App'; import {useMessage} from '../utils/useMessage'; +import { + BonusStats, + Filters, + MainStatKey, + RawHero, + RawItem, + SetFilters, + StatKey, + emptyBonusStats, + emptyFilters, + emptySetFilters, + isFourPieceSet, + isTwoPieceSet, +} from '../utils/optimizer'; import './optimizer.css'; const {Content} = Layout; - -type RawItem = { - id?: string | number; - gear?: string; - set?: string; - main?: { type?: string; value?: number }; - substats?: Array<{ type?: string; value?: number }>; -}; - -type RawHero = { - id?: string | number; - name?: string; - atk?: number; - def?: number; - hp?: number; - spd?: number; - cr?: number; - cd?: number; - eff?: number; - res?: number; - baseAtk?: number; - baseDef?: number; - baseHp?: number; - baseSpd?: number; - baseCr?: number; - baseCd?: number; - baseEff?: number; - baseRes?: number; -}; - -type StatKey = 'atk' | 'def' | 'hp' | 'spd' | 'cr' | 'cd' | 'acc' | 'res'; -type StatRange = { min?: number; max?: number }; -type Filters = Record; -type MainStatKey = - | 'Attack' - | 'Defense' - | 'Health' - | 'AttackPercent' - | 'DefensePercent' - | 'HealthPercent' - | 'Speed' - | 'CriticalHitChancePercent' - | 'CriticalHitDamagePercent' - | 'EffectivenessPercent' - | 'EffectResistancePercent'; - -type ItemStats = { - atk: number; - def: number; - hp: number; - spd: number; - cr: number; - cd: number; - acc: number; - res: number; - atkPct: number; - defPct: number; - hpPct: number; -}; +const MAX_ITEMS_PER_SLOT = 150; +const MAX_COMBOS = 200000; +const MAX_RESULTS = 200; +const GEAR_SLOTS = ['Weapon', 'Helmet', 'Armor', 'Necklace', 'Ring', 'Boots'] as const; type BuildResult = { key: string; @@ -96,290 +56,39 @@ type BuildResult = { items: RawItem[]; }; -const STAT_LABELS: Array<{ key: StatKey; label: string }> = [ - {key: 'atk', label: '攻击'}, - {key: 'def', label: '防御'}, - {key: 'hp', label: '生命'}, - {key: 'spd', label: '速度'}, - {key: 'cr', label: '暴击'}, - {key: 'cd', label: '爆伤'}, - {key: 'acc', label: '命中'}, - {key: 'res', label: '抵抗'}, +const STAT_LABELS: Array<{key: StatKey; label: string}> = [ + {key: 'atk', label: '\u653b\u51fb'}, + {key: 'def', label: '\u9632\u5fa1'}, + {key: 'hp', label: '\u751f\u547d'}, + {key: 'spd', label: '\u901f\u5ea6'}, + {key: 'cr', label: '\u66b4\u51fb'}, + {key: 'cd', label: '\u7206\u4f24'}, + {key: 'acc', label: '\u547d\u4e2d'}, + {key: 'res', label: '\u62b5\u6297'}, ]; -const GEAR_SLOTS = ['Weapon', 'Helmet', 'Armor', 'Necklace', 'Ring', 'Boots']; -const MAX_ITEMS_PER_SLOT = 8; -const MAX_RESULTS = 200; -const MAX_COMBOS = 200000; - -const FOUR_PIECE_SETS = new Set([ - 'AttackSet', - 'SpeedSet', - 'DestructionSet', - 'LifestealSet', - 'ProtectionSet', - 'CounterSet', - 'RageSet', - 'RevengeSet', - 'InjurySet', - 'ReversalSet', - 'RiposteSet', - 'WarfareSet', -]); - -const TWO_PIECE_SETS = new Set([ - 'HealthSet', - 'DefenseSet', - 'CriticalSet', - 'HitSet', - 'ResistSet', - 'UnitySet', - 'ImmunitySet', - 'PenetrationSet', - 'TorrentSet', - 'PursuitSet', -]); - -type SetFilters = { - set1: string[]; - set2: string[]; - set3: string[]; -}; - -const emptyFilters: Filters = { - atk: {}, - def: {}, - hp: {}, - spd: {}, - cr: {}, - cd: {}, - acc: {}, - res: {}, -}; - -const emptySetFilters: SetFilters = { - set1: [], - set2: [], - set3: [], -}; - -const toNumber = (value: any) => { - if (typeof value !== 'number' || Number.isNaN(value)) return 0; - return value; -}; - -const buildItemStats = (item: RawItem): ItemStats => { - const stats: ItemStats = { - atk: 0, - def: 0, - hp: 0, - spd: 0, - cr: 0, - cd: 0, - acc: 0, - res: 0, - atkPct: 0, - defPct: 0, - hpPct: 0, - }; - - const applyStat = (type?: string, value?: number) => { - if (!type) return; - const v = toNumber(value); - switch (type) { - case 'Attack': - stats.atk += v; - break; - case 'Defense': - stats.def += v; - break; - case 'Health': - stats.hp += v; - break; - case 'Speed': - stats.spd += v; - break; - case 'CriticalHitChancePercent': - stats.cr += v; - break; - case 'CriticalHitDamagePercent': - stats.cd += v; - break; - case 'EffectivenessPercent': - stats.acc += v; - break; - case 'EffectResistancePercent': - stats.res += v; - break; - case 'AttackPercent': - stats.atkPct += v; - break; - case 'DefensePercent': - stats.defPct += v; - break; - case 'HealthPercent': - stats.hpPct += v; - break; - default: - break; - } - }; - - if (item.main) { - applyStat(item.main.type, item.main.value); - } - if (Array.isArray(item.substats)) { - item.substats.forEach(sub => applyStat(sub.type, sub.value)); - } - - return stats; -}; - -const buildBaseStats = (hero?: RawHero) => { - return { - atk: toNumber(hero?.baseAtk ?? hero?.atk), - def: toNumber(hero?.baseDef ?? hero?.def), - hp: toNumber(hero?.baseHp ?? hero?.hp), - spd: toNumber(hero?.baseSpd ?? hero?.spd), - cr: toNumber(hero?.baseCr ?? hero?.cr), - cd: toNumber(hero?.baseCd ?? hero?.cd), - acc: toNumber(hero?.baseEff ?? hero?.eff), - res: toNumber(hero?.baseRes ?? hero?.res), - }; -}; - const getHeroStatValue = (hero: RawHero | null, key: StatKey) => { if (!hero) return 0; - if (key === 'acc') return toNumber(hero.eff); - return toNumber((hero as any)[key]); -}; - -const scoreStats = (stats: ItemStats, baseStats: ReturnType) => { - const atk = stats.atk + baseStats.atk * (stats.atkPct / 100); - const def = stats.def + baseStats.def * (stats.defPct / 100); - const hp = stats.hp + baseStats.hp * (stats.hpPct / 100); - - return ( - stats.spd * 2 + - stats.cr + - stats.cd + - stats.acc + - stats.res + - (atk + def + hp) / 100 - ); -}; - -const buildTotalStats = (statsList: ItemStats[], baseStats: ReturnType) => { - const totals: ItemStats = { - atk: 0, - def: 0, - hp: 0, - spd: 0, - cr: 0, - cd: 0, - acc: 0, - res: 0, - atkPct: 0, - defPct: 0, - hpPct: 0, - }; - - statsList.forEach(stats => { - totals.atk += stats.atk; - totals.def += stats.def; - totals.hp += stats.hp; - totals.spd += stats.spd; - totals.cr += stats.cr; - totals.cd += stats.cd; - totals.acc += stats.acc; - totals.res += stats.res; - totals.atkPct += stats.atkPct; - totals.defPct += stats.defPct; - totals.hpPct += stats.hpPct; - }); - - return { - atk: totals.atk + baseStats.atk + baseStats.atk * (totals.atkPct / 100), - def: totals.def + baseStats.def + baseStats.def * (totals.defPct / 100), - hp: totals.hp + baseStats.hp + baseStats.hp * (totals.hpPct / 100), - spd: totals.spd + baseStats.spd, - cr: totals.cr + baseStats.cr, - cd: totals.cd + baseStats.cd, - acc: totals.acc + baseStats.acc, - res: totals.res + baseStats.res, - }; -}; - -const passesFilters = (totals: ReturnType, filters: Filters) => { - return STAT_LABELS.every(stat => { - const value = totals[stat.key]; - const range = filters[stat.key]; - if (range.min !== undefined && value < range.min) return false; - if (range.max !== undefined && value > range.max) return false; - return true; - }); -}; - -const isFourPieceSet = (setName: string) => FOUR_PIECE_SETS.has(setName); -const isTwoPieceSet = (setName: string) => TWO_PIECE_SETS.has(setName); - -const getCompletedSets = (items: RawItem[]) => { - const counts = new Map(); - items.forEach(item => { - if (!item.set) return; - counts.set(item.set, (counts.get(item.set) || 0) + 1); - }); - - const completed: string[] = []; - counts.forEach((count, setName) => { - if (FOUR_PIECE_SETS.has(setName) && count >= 4) { - completed.push(setName); - return; - } - if (TWO_PIECE_SETS.has(setName) && count >= 2) { - const stacks = Math.floor(count / 2); - for (let i = 0; i < stacks; i += 1) { - completed.push(setName); - } - } - }); - - return completed; -}; - -const passesSetFilter = (items: RawItem[], setFilters: SetFilters) => { - if (setFilters.set1.length === 0 && setFilters.set2.length === 0 && setFilters.set3.length === 0) { - return true; + switch (key) { + case 'atk': + return hero.atk ?? hero.baseAtk ?? 0; + case 'def': + return hero.def ?? hero.baseDef ?? 0; + case 'hp': + return hero.hp ?? hero.baseHp ?? 0; + case 'spd': + return hero.spd ?? hero.baseSpd ?? 0; + case 'cr': + return hero.cr ?? hero.baseCr ?? 0; + case 'cd': + return hero.cd ?? hero.baseCd ?? 0; + case 'acc': + return hero.eff ?? hero.baseEff ?? 0; + case 'res': + return hero.res ?? hero.baseRes ?? 0; + default: + return 0; } - - const counts = new Map(); - items.forEach(item => { - if (!item.set) return; - counts.set(item.set, (counts.get(item.set) || 0) + 1); - }); - - const hasAnyMatch = (sets: string[], required: number) => { - if (sets.length === 0) return true; - return sets.some(setName => (counts.get(setName) || 0) >= required); - }; - - const set1HasFour = setFilters.set1.some(isFourPieceSet); - const set1HasTwo = setFilters.set1.some(isTwoPieceSet); - - if (set1HasFour && !set1HasTwo) { - if (!hasAnyMatch(setFilters.set1, 4)) return false; - if (!hasAnyMatch(setFilters.set2, 2)) return false; - return true; - } - - if (set1HasTwo) { - if (!hasAnyMatch(setFilters.set1, 2)) return false; - if (!hasAnyMatch(setFilters.set2, 2)) return false; - if (!hasAnyMatch(setFilters.set3, 2)) return false; - return true; - } - - return true; }; export default function OptimizerPage() { @@ -408,6 +117,9 @@ export default function OptimizerPage() { const [selectedResult, setSelectedResult] = useState(null); const [totalCombos, setTotalCombos] = useState(0); const {success, error, info} = useMessage(); + const [bonusByHeroId, setBonusByHeroId] = useState>({}); + const [bonusEditorOpen, setBonusEditorOpen] = useState(false); + const [bonusDraft, setBonusDraft] = useState(emptyBonusStats); const leftPanelsRef = useRef(null); const [leftPanelsHeight, setLeftPanelsHeight] = useState(null); @@ -423,24 +135,75 @@ export default function OptimizerPage() { const filterCardRef = useRef(null); const rightPanelRef = useRef(null); - const setPickerOptions = [ + const INGAME_SET_MAP: Record = { + set_acc: 'HitSet', + set_att: 'AttackSet', + set_coop: 'UnitySet', + set_counter: 'CounterSet', + set_cri_dmg: 'DestructionSet', + set_cri: 'CriticalSet', + set_def: 'DefenseSet', + set_immune: 'ImmunitySet', + set_max_hp: 'HealthSet', + set_penetrate: 'PenetrationSet', + set_rage: 'RageSet', + set_res: 'ResistSet', + set_revenge: 'RevengeSet', + set_scar: 'InjurySet', + set_speed: 'SpeedSet', + set_vampire: 'LifestealSet', + set_shield: 'ProtectionSet', + set_torrent: 'TorrentSet', + set_revenant: 'ReversalSet', + set_riposte: 'RiposteSet', + set_chase: 'PursuitSet', + set_opener: 'WarfareSet', + }; + +const setPickerOptions = [ {key: 'All', label: '全部', setKey: ''}, {key: 'AttackSet', label: '攻击套装', setKey: 'AttackSet'}, {key: 'DefenseSet', label: '防御套装', setKey: 'DefenseSet'}, - {key: 'HealthSet', label: '生命套装', setKey: 'HealthSet'}, + {key: 'HealthSet', label: '生命值套装', setKey: 'HealthSet'}, {key: 'SpeedSet', label: '速度套装', setKey: 'SpeedSet'}, {key: 'CriticalSet', label: '暴击套装', setKey: 'CriticalSet'}, {key: 'HitSet', label: '命中套装', setKey: 'HitSet'}, {key: 'ResistSet', label: '抵抗套装', setKey: 'ResistSet'}, {key: 'LifestealSet', label: '吸血套装', setKey: 'LifestealSet'}, + {key: 'UnitySet', label: '夹攻套装', setKey: 'UnitySet'}, + {key: 'RageSet', label: '愤怒套装', setKey: 'RageSet'}, + {key: 'RevengeSet', label: '憨恨套装', setKey: 'RevengeSet'}, {key: 'DestructionSet', label: '破灭套装', setKey: 'DestructionSet'}, {key: 'CounterSet', label: '反击套装', setKey: 'CounterSet'}, {key: 'ImmunitySet', label: '免疫套装', setKey: 'ImmunitySet'}, {key: 'PenetrationSet', label: '穿透套装', setKey: 'PenetrationSet'}, - {key: 'RevengeSet', label: '复仇套装', setKey: 'RevengeSet'}, {key: 'InjurySet', label: '伤口套装', setKey: 'InjurySet'}, + {key: 'ProtectionSet', label: '守护套装', setKey: 'ProtectionSet'}, + {key: 'TorrentSet', label: '激流套装', setKey: 'TorrentSet'}, + {key: 'ReversalSet', label: '逆袭套装', setKey: 'ReversalSet'}, + {key: 'RiposteSet', label: '回击套装', setKey: 'RiposteSet'}, + {key: 'WarfareSet', label: '开战套装', setKey: 'WarfareSet'}, + {key: 'PursuitSet', label: '追击套装', setKey: 'PursuitSet'}, ]; + const setPickerRows = Math.ceil(setPickerOptions.length / 2); + const setPickerListHeight = 28 + setPickerRows * 46 + Math.max(0, setPickerRows - 1) * 8; + + + const setCounts = useMemo(() => { + const counts: Record = {}; + let total = 0; + parsedItems.forEach(item => { + const setKey = item.set || (item.f ? INGAME_SET_MAP[item.f as string] : undefined); + if (!setKey) return; + counts[setKey] = (counts[setKey] || 0) + 1; + total += 1; + }); + counts[''] = total; + return counts; + }, [parsedItems]); + + const getSetLabel = (values: string[]) => { const key = values[0]; if (!key) return '套装'; @@ -450,7 +213,7 @@ export default function OptimizerPage() { const openSetPicker = (target: 'set1' | 'set2' | 'set3', event: React.MouseEvent) => { const modalWidth = 520; - const modalHeight = 520; + const modalHeight = Math.min(window.innerHeight - 20, setPickerListHeight); const offset = 0; let left = 0; let top = 0; @@ -571,18 +334,44 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat return parsedHeroes.find(hero => hero.id === selectedHeroId) || null; }, [parsedHeroes, selectedHeroId]); + const selectedHeroKey = selectedHeroId == null ? '' : String(selectedHeroId); + const selectedHeroBonus = bonusByHeroId[selectedHeroKey] || emptyBonusStats; + const loadLatestData = async () => { setLoading(true); try { - const parsed = await App.GetLatestParsedDataFromDatabase(); + const [parsed, templates] = await Promise.all([ + App.GetLatestParsedDataFromDatabase(), + App.GetHeroTemplates(), + ]); const items = (parsed?.items || []) as RawItem[]; - const heroes = (parsed?.heroes || []) as RawHero[]; + const heroTemplates = (templates || []).map(t => ({ + id: t.code, + code: t.code, + name: t.name, + baseAtk: t.baseAtk, + baseDef: t.baseDef, + baseHp: t.baseHp, + baseSpd: t.baseSpd, + baseCr: t.baseCr, + baseCd: t.baseCd, + baseEff: t.baseEff, + baseRes: t.baseRes, + atk: t.baseAtk, + def: t.baseDef, + hp: t.baseHp, + spd: t.baseSpd, + cr: t.baseCr, + cd: t.baseCd, + eff: t.baseEff, + res: t.baseRes, + })) as RawHero[]; setParsedItems(items); - setParsedHeroes(heroes); - if (heroes.length > 0) { - setSelectedHeroId(heroes[0].id || ''); + setParsedHeroes(heroTemplates); + if (heroTemplates.length > 0) { + setSelectedHeroId(heroTemplates[0].id || ''); } - if (items.length === 0 && heroes.length === 0) { + if (items.length === 0 && heroTemplates.length === 0) { info('暂无解析数据'); } else { success('数据加载成功'); @@ -650,108 +439,51 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat setTotalCombos(0); }; - const buildResults = () => { + const buildResults = async () => { + console.log('OptimizerPage buildResults clicked'); if (!selectedHero) { - info('请先选择英雄'); + info('\u8bf7\u5148\u9009\u62e9\u82f1\u96c4'); return; } if (parsedItems.length === 0) { - info('暂无装备数据'); + info('\u6682\u65e0\u88c5\u5907\u6570\u636e'); return; } - - const baseStats = buildBaseStats(selectedHero); - const items = parsedItems; - - const itemsByGear = new Map(); - GEAR_SLOTS.forEach(slot => itemsByGear.set(slot, [])); - items.forEach(item => { - if (item.gear && itemsByGear.has(item.gear)) { - itemsByGear.get(item.gear)!.push(item); - } - }); - - const statsCache = new Map(); - const topItemsByGear: RawItem[][] = []; - - for (const slot of GEAR_SLOTS) { - const slotItems = itemsByGear.get(slot) || []; - const scored = slotItems.map(item => { - const stats = buildItemStats(item); - statsCache.set(item, stats); - return { - item, - score: scoreStats(stats, baseStats), - }; + try { + console.log('OptimizeBuilds calling', { + heroId: String(selectedHero.id ?? ''), + setFilters, + statFilters, + mainStatFilters, + weightValues, + bonusStats: selectedHeroBonus, }); - scored.sort((a, b) => b.score - a.score); - topItemsByGear.push(scored.slice(0, MAX_ITEMS_PER_SLOT).map(x => x.item)); - } - - const combos = topItemsByGear.reduce((acc, list) => acc * Math.max(list.length, 1), 1); - setTotalCombos(combos); - - const resultsBuffer: BuildResult[] = []; - let comboCount = 0; - - const [weapons, helmets, armors, necklaces, rings, boots] = topItemsByGear; - for (const w of weapons) { - for (const h of helmets) { - for (const a of armors) { - for (const n of necklaces) { - for (const r of rings) { - for (const b of boots) { - comboCount += 1; - if (comboCount > MAX_COMBOS) break; - const itemsList = [w, h, a, n, r, b]; - if (!passesSetFilter(itemsList, setFilters)) continue; - const statsList = itemsList.map(item => statsCache.get(item) || buildItemStats(item)); - const totals = buildTotalStats(statsList, baseStats); - if (!passesFilters(totals, statFilters)) continue; - - const completedSets = getCompletedSets(itemsList); - const setNames = completedSets.length > 0 ? completedSets.join('/') : '无套装'; - - const score = - totals.spd * 2 + - totals.cr + - totals.cd + - totals.acc + - totals.res + - (totals.atk + totals.def + totals.hp) / 100; - - resultsBuffer.push({ - key: itemsList.map(item => item.id).join('-'), - sets: setNames || '未知套装', - atk: Math.round(totals.atk), - def: Math.round(totals.def), - hp: Math.round(totals.hp), - spd: Math.round(totals.spd), - cr: Math.round(totals.cr), - cd: Math.round(totals.cd), - acc: Math.round(totals.acc), - res: Math.round(totals.res), - score, - items: itemsList, - }); - } - if (comboCount > MAX_COMBOS) break; - } - if (comboCount > MAX_COMBOS) break; - } - if (comboCount > MAX_COMBOS) break; - } - if (comboCount > MAX_COMBOS) break; + const resp = await App.OptimizeBuilds({ + heroId: String(selectedHero.code ?? ''), + setFilters, + statFilters, + mainStatFilters, + weightValues, + bonusStats: selectedHeroBonus, + maxItemsPerSlot: MAX_ITEMS_PER_SLOT, + maxCombos: MAX_COMBOS, + maxResults: MAX_RESULTS, + } as any); + console.log('OptimizeBuilds returned', resp); + const nextResults = (resp?.results || []) as BuildResult[]; + setTotalCombos(resp?.totalCombos || 0); + setResults(nextResults); + setSelectedResult(nextResults[0] || null); + if (nextResults.length > 0) { + success(`\u914d\u88c5\u5b8c\u6210\uff0c\u5171 ${nextResults.length} \u6761\u7ed3\u679c`); } - if (comboCount > MAX_COMBOS) break; - } - - resultsBuffer.sort((a, b) => b.score - a.score); - const finalResults = resultsBuffer.slice(0, MAX_RESULTS); - setResults(finalResults); - setSelectedResult(finalResults[0] || null); - if (finalResults.length === 0) { - info('没有符合条件的配装结果'); + if (nextResults.length === 0) { + info('\u6ca1\u6709\u7b26\u5408\u6761\u4ef6\u7684\u914d\u88c5\u7ed3\u679c'); + } + } catch (err) { + const msg = err instanceof Error ? err.message : '\u914d\u88c5\u5931\u8d25'; + error(msg); + console.error('OptimizeBuilds error:', err); } }; @@ -784,6 +516,16 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat options={heroOptions} placeholder="请选择英雄" /> + @@ -900,8 +642,9 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat
-
+
+ 攻击 + setBonusEditorOpen(false)} + onOk={() => { + if (!selectedHeroKey) return; + setBonusByHeroId(prev => ({...prev, [selectedHeroKey]: bonusDraft})); + setBonusEditorOpen(false); + }} + > +
+
+
{'\u653b\u51fb'}
+ setBonusDraft(prev => ({...prev, atk: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u653b\u51fb%'}
+ setBonusDraft(prev => ({...prev, atkPct: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u9632\u5fa1'}
+ setBonusDraft(prev => ({...prev, def: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u9632\u5fa1%'}
+ setBonusDraft(prev => ({...prev, defPct: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u751f\u547d'}
+ setBonusDraft(prev => ({...prev, hp: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u751f\u547d%'}
+ setBonusDraft(prev => ({...prev, hpPct: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u901f\u5ea6'}
+ setBonusDraft(prev => ({...prev, spd: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u66b4\u51fb\u7387'}
+ setBonusDraft(prev => ({...prev, cr: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u66b4\u51fb\u4f24\u5bb3'}
+ setBonusDraft(prev => ({...prev, cd: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u6548\u679c\u547d\u4e2d'}
+ setBonusDraft(prev => ({...prev, eff: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u6548\u679c\u6297\u6027'}
+ setBonusDraft(prev => ({...prev, res: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u6700\u7ec8\u653b\u51fb\u500d\u7387%'}
+ setBonusDraft(prev => ({...prev, finalAtkMultiplier: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u6700\u7ec8\u9632\u5fa1\u500d\u7387%'}
+ setBonusDraft(prev => ({...prev, finalDefMultiplier: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{'\u6700\u7ec8\u751f\u547d\u500d\u7387%'}
+ setBonusDraft(prev => ({...prev, finalHpMultiplier: Number(v || 0)}))} style={{width: '100%'}} /> +
+
+
{setPickerOpen && createPortal(
event.stopPropagation()} > -
+
{setPickerOptions.map(option => ( ))}
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 7c07670..d1683f5 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1,5 +1,43 @@ export namespace model { + export class BonusStats { + atk: number; + def: number; + hp: number; + atkPct: number; + defPct: number; + hpPct: number; + spd: number; + cr: number; + cd: number; + eff: number; + res: number; + finalAtkMultiplier: number; + finalDefMultiplier: number; + finalHpMultiplier: number; + + static createFrom(source: any = {}) { + return new BonusStats(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.atk = source["atk"]; + this.def = source["def"]; + this.hp = source["hp"]; + this.atkPct = source["atkPct"]; + this.defPct = source["defPct"]; + this.hpPct = source["hpPct"]; + this.spd = source["spd"]; + this.cr = source["cr"]; + this.cd = source["cd"]; + this.eff = source["eff"]; + this.res = source["res"]; + this.finalAtkMultiplier = source["finalAtkMultiplier"]; + this.finalDefMultiplier = source["finalDefMultiplier"]; + this.finalHpMultiplier = source["finalHpMultiplier"]; + } + } export class CaptureStatus { is_capturing: boolean; status: string; @@ -16,6 +54,36 @@ export namespace model { this.error = source["error"]; } } + export class HeroTemplate { + code: string; + name: string; + baseAtk: number; + baseDef: number; + baseHp: number; + baseSpd: number; + baseCr: number; + baseCd: number; + baseEff: number; + baseRes: number; + + static createFrom(source: any = {}) { + return new HeroTemplate(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.code = source["code"]; + this.name = source["name"]; + this.baseAtk = source["baseAtk"]; + this.baseDef = source["baseDef"]; + this.baseHp = source["baseHp"]; + this.baseSpd = source["baseSpd"]; + this.baseCr = source["baseCr"]; + this.baseCd = source["baseCd"]; + this.baseEff = source["baseEff"]; + this.baseRes = source["baseRes"]; + } + } export class NetworkInterface { name: string; description: string; @@ -34,6 +102,222 @@ export namespace model { this.is_loopback = source["is_loopback"]; } } + export class OptimizeStat { + type: string; + value: number; + + static createFrom(source: any = {}) { + return new OptimizeStat(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.type = source["type"]; + this.value = source["value"]; + } + } + export class OptimizeItem { + id: any; + gear: string; + set: string; + f: string; + main?: OptimizeStat; + substats?: OptimizeStat[]; + + static createFrom(source: any = {}) { + return new OptimizeItem(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.gear = source["gear"]; + this.set = source["set"]; + this.f = source["f"]; + this.main = this.convertValues(source["main"], OptimizeStat); + this.substats = this.convertValues(source["substats"], OptimizeStat); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class StatRange { + min?: number; + max?: number; + + static createFrom(source: any = {}) { + return new StatRange(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.min = source["min"]; + this.max = source["max"]; + } + } + export class SetFilters { + set1: string[]; + set2: string[]; + set3: string[]; + + static createFrom(source: any = {}) { + return new SetFilters(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.set1 = source["set1"]; + this.set2 = source["set2"]; + this.set3 = source["set3"]; + } + } + export class OptimizeRequest { + heroId: string; + setFilters: SetFilters; + statFilters: Record; + mainStatFilters: Record; + weightValues: Record; + bonusStats: BonusStats; + maxItemsPerSlot: number; + maxCombos: number; + maxResults: number; + + static createFrom(source: any = {}) { + return new OptimizeRequest(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.heroId = source["heroId"]; + this.setFilters = this.convertValues(source["setFilters"], SetFilters); + this.statFilters = this.convertValues(source["statFilters"], StatRange, true); + this.mainStatFilters = source["mainStatFilters"]; + this.weightValues = source["weightValues"]; + this.bonusStats = this.convertValues(source["bonusStats"], BonusStats); + this.maxItemsPerSlot = source["maxItemsPerSlot"]; + this.maxCombos = source["maxCombos"]; + this.maxResults = source["maxResults"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class OptimizeResult { + key: string; + sets: string; + atk: number; + def: number; + hp: number; + spd: number; + cr: number; + cd: number; + acc: number; + res: number; + score: number; + items: OptimizeItem[]; + + static createFrom(source: any = {}) { + return new OptimizeResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.key = source["key"]; + this.sets = source["sets"]; + this.atk = source["atk"]; + this.def = source["def"]; + this.hp = source["hp"]; + this.spd = source["spd"]; + this.cr = source["cr"]; + this.cd = source["cd"]; + this.acc = source["acc"]; + this.res = source["res"]; + this.score = source["score"]; + this.items = this.convertValues(source["items"], OptimizeItem); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class OptimizeResponse { + totalCombos: number; + results: OptimizeResult[]; + + static createFrom(source: any = {}) { + return new OptimizeResponse(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.totalCombos = source["totalCombos"]; + this.results = this.convertValues(source["results"], OptimizeResult); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + export class ParsedResult { items: any[]; heroes: any[]; @@ -64,6 +348,7 @@ export namespace model { this.created_at = source["created_at"]; } } + } diff --git a/frontend/wailsjs/go/service/App.d.ts b/frontend/wailsjs/go/service/App.d.ts index d31de32..f09087b 100644 --- a/frontend/wailsjs/go/service/App.d.ts +++ b/frontend/wailsjs/go/service/App.d.ts @@ -18,6 +18,8 @@ export function GetCapturedData():Promise>; export function GetCurrentDataForExport():Promise; +export function GetHeroTemplates():Promise>; + export function GetLatestParsedDataFromDatabase():Promise; export function GetNetworkInterfaces():Promise>; @@ -26,6 +28,8 @@ export function GetParsedDataByID(arg1:number):Promise; export function GetParsedSessions():Promise>; +export function OptimizeBuilds(arg1:model.OptimizeRequest):Promise; + export function ParseData(arg1:Array):Promise; export function ReadRawJsonFile():Promise; diff --git a/frontend/wailsjs/go/service/App.js b/frontend/wailsjs/go/service/App.js index ce4dee4..e971b7f 100644 --- a/frontend/wailsjs/go/service/App.js +++ b/frontend/wailsjs/go/service/App.js @@ -34,6 +34,10 @@ export function GetCurrentDataForExport() { return window['go']['service']['App']['GetCurrentDataForExport'](); } +export function GetHeroTemplates() { + return window['go']['service']['App']['GetHeroTemplates'](); +} + export function GetLatestParsedDataFromDatabase() { return window['go']['service']['App']['GetLatestParsedDataFromDatabase'](); } @@ -50,6 +54,10 @@ export function GetParsedSessions() { return window['go']['service']['App']['GetParsedSessions'](); } +export function OptimizeBuilds(arg1) { + return window['go']['service']['App']['OptimizeBuilds'](arg1); +} + export function ParseData(arg1) { return window['go']['service']['App']['ParseData'](arg1); } diff --git a/internal/model/hero_template.go b/internal/model/hero_template.go new file mode 100644 index 0000000..cbf3edc --- /dev/null +++ b/internal/model/hero_template.go @@ -0,0 +1,15 @@ +package model + +// HeroTemplate represents template hero data from herodata.json. +type HeroTemplate struct { + Code string `json:"code"` + Name string `json:"name"` + BaseAtk float64 `json:"baseAtk"` + BaseDef float64 `json:"baseDef"` + BaseHp float64 `json:"baseHp"` + BaseSpd float64 `json:"baseSpd"` + BaseCr float64 `json:"baseCr"` + BaseCd float64 `json:"baseCd"` + BaseEff float64 `json:"baseEff"` + BaseRes float64 `json:"baseRes"` +} diff --git a/internal/model/optimizer.go b/internal/model/optimizer.go new file mode 100644 index 0000000..c794a37 --- /dev/null +++ b/internal/model/optimizer.go @@ -0,0 +1,106 @@ +package model + +// OptimizeStat represents a stat type/value pair. +type OptimizeStat struct { + Type string `json:"type"` + Value float64 `json:"value"` +} + +// OptimizeItem represents the minimal item shape needed by the optimizer. +type OptimizeItem struct { + ID interface{} `json:"id"` + Gear string `json:"gear"` + Set string `json:"set"` + F string `json:"f"` + Main *OptimizeStat `json:"main,omitempty"` + Substats []OptimizeStat `json:"substats,omitempty"` +} + +// OptimizeHero represents the minimal hero shape needed by the optimizer. +type OptimizeHero struct { + ID interface{} `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Atk float64 `json:"atk"` + Def float64 `json:"def"` + Hp float64 `json:"hp"` + Spd float64 `json:"spd"` + Cr float64 `json:"cr"` + Cd float64 `json:"cd"` + Eff float64 `json:"eff"` + Res float64 `json:"res"` + BaseAtk float64 `json:"baseAtk"` + BaseDef float64 `json:"baseDef"` + BaseHp float64 `json:"baseHp"` + BaseSpd float64 `json:"baseSpd"` + BaseCr float64 `json:"baseCr"` + BaseCd float64 `json:"baseCd"` + BaseEff float64 `json:"baseEff"` + BaseRes float64 `json:"baseRes"` +} + +// StatRange defines a min/max filter. +type StatRange struct { + Min *float64 `json:"min,omitempty"` + Max *float64 `json:"max,omitempty"` +} + +// SetFilters defines set filter groups. +type SetFilters struct { + Set1 []string `json:"set1"` + Set2 []string `json:"set2"` + Set3 []string `json:"set3"` +} + +// BonusStats are hero-specific bonus modifiers. +type BonusStats struct { + Atk float64 `json:"atk"` + Def float64 `json:"def"` + Hp float64 `json:"hp"` + AtkPct float64 `json:"atkPct"` + DefPct float64 `json:"defPct"` + HpPct float64 `json:"hpPct"` + Spd float64 `json:"spd"` + Cr float64 `json:"cr"` + Cd float64 `json:"cd"` + Eff float64 `json:"eff"` + Res float64 `json:"res"` + FinalAtkMultiplier float64 `json:"finalAtkMultiplier"` + FinalDefMultiplier float64 `json:"finalDefMultiplier"` + FinalHpMultiplier float64 `json:"finalHpMultiplier"` +} + +// OptimizeRequest represents optimizer input. +type OptimizeRequest struct { + HeroID string `json:"heroId"` + SetFilters SetFilters `json:"setFilters"` + StatFilters map[string]StatRange `json:"statFilters"` + MainStatFilters map[string]string `json:"mainStatFilters"` + WeightValues map[string]float64 `json:"weightValues"` + BonusStats BonusStats `json:"bonusStats"` + MaxItemsPerSlot int `json:"maxItemsPerSlot"` + MaxCombos int `json:"maxCombos"` + MaxResults int `json:"maxResults"` +} + +// OptimizeResult is a single build result. +type OptimizeResult struct { + Key string `json:"key"` + Sets string `json:"sets"` + Atk float64 `json:"atk"` + Def float64 `json:"def"` + Hp float64 `json:"hp"` + Spd float64 `json:"spd"` + Cr float64 `json:"cr"` + Cd float64 `json:"cd"` + Acc float64 `json:"acc"` + Res float64 `json:"res"` + Score float64 `json:"score"` + Items []OptimizeItem `json:"items"` +} + +// OptimizeResponse is the optimizer output. +type OptimizeResponse struct { + TotalCombos int `json:"totalCombos"` + Results []OptimizeResult `json:"results"` +} diff --git a/internal/service/main_service.go b/internal/service/main_service.go index 26ae2f9..442a4ca 100644 --- a/internal/service/main_service.go +++ b/internal/service/main_service.go @@ -10,6 +10,7 @@ import ( "equipment-analyzer/internal/capture" "equipment-analyzer/internal/config" "equipment-analyzer/internal/model" + "equipment-analyzer/internal/optimizer" "equipment-analyzer/internal/utils" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -83,6 +84,71 @@ func (a *App) Shutdown(ctx context.Context) { } } +// OptimizeBuilds runs the optimizer on latest parsed data. +func (a *App) OptimizeBuilds(req model.OptimizeRequest) (*model.OptimizeResponse, error) { + log.Printf("[service] OptimizeBuilds entry heroId=%s", req.HeroID) + parsedResult, err := a.GetLatestParsedDataFromDatabase() + if err != nil { + log.Printf("[service] OptimizeBuilds get data failed: %v", err) + return nil, err + } + if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) { + log.Printf("[service] OptimizeBuilds no data items=%d heroes=%d", len(parsedResult.Items), len(parsedResult.Heroes)) + return &model.OptimizeResponse{TotalCombos: 0, Results: []model.OptimizeResult{}}, nil + } + + items, err := optimizer.ParseItems(parsedResult.Items) + if err != nil { + log.Printf("[service] OptimizeBuilds parse items failed: %v", err) + return nil, fmt.Errorf("parse items failed: %w", err) + } + logMissingSetStats(parsedResult.Items) + heroes, err := optimizer.ParseHeroes(parsedResult.Heroes) + if err != nil { + log.Printf("[service] OptimizeBuilds parse heroes failed: %v", err) + return nil, fmt.Errorf("parse heroes failed: %w", err) + } + templates, err := a.parserService.GetHeroTemplates() + if err != nil { + log.Printf("[service] OptimizeBuilds hero templates missing: %v", err) + return nil, fmt.Errorf("未读取到英雄信息数据") + } + templateHeroes := make([]model.OptimizeHero, 0, len(templates)) + for _, t := range templates { + templateHeroes = append(templateHeroes, model.OptimizeHero{ + Code: t.Code, + Name: t.Name, + BaseAtk: t.BaseAtk, + BaseDef: t.BaseDef, + BaseHp: t.BaseHp, + BaseSpd: t.BaseSpd, + BaseCr: t.BaseCr, + BaseCd: t.BaseCd, + BaseEff: t.BaseEff, + BaseRes: t.BaseRes, + Atk: t.BaseAtk, + Def: t.BaseDef, + Hp: t.BaseHp, + Spd: t.BaseSpd, + Cr: t.BaseCr, + Cd: t.BaseCd, + Eff: t.BaseEff, + Res: t.BaseRes, + }) + } + // Use template heroes for optimizer to match the template-based selection. + heroes = templateHeroes + + log.Printf("[service] OptimizeBuilds parsed items=%d heroes=%d", len(items), len(heroes)) + resp, err := optimizer.Optimize(items, heroes, req) + if err != nil { + log.Printf("[service] OptimizeBuilds optimize failed: %v", err) + return nil, err + } + log.Printf("[service] OptimizeBuilds done results=%d totalCombos=%d", len(resp.Results), resp.TotalCombos) + return resp, nil +} + // GetNetworkInterfaces returns available network interfaces. func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) { interfaces, err := capture.GetNetworkInterfaces() @@ -93,6 +159,11 @@ func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) { return interfaces, nil } +// GetHeroTemplates returns template heroes from local herodata.json. +func (a *App) GetHeroTemplates() ([]model.HeroTemplate, error) { + return a.parserService.GetHeroTemplates() +} + // StartCapture starts capture on the given interface. func (a *App) StartCapture(interfaceName string) error { if a.captureService.IsCapturing() { @@ -329,6 +400,7 @@ func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) { return nil, fmt.Errorf("failed to unmarshal items: %w", err) } } + logMissingSetStats(items) var heroes []interface{} if heroesJSON != "" { @@ -424,6 +496,95 @@ func (a *App) GetAllAppSettings() (map[string]string, error) { return a.databaseService.GetAllAppSettings() } +func logMissingSetStats(items []interface{}) { + if len(items) == 0 { + return + } + var total int + var emptyF int + var emptySet int + var healthF int + var healthSet int + var mapHp int + setCounts := map[string]int{} + uncategorized := 0 + emptySetByF := map[string]int{} + emptySetByGear := map[string]int{} + ingameSetMap := map[string]string{ + "set_acc": "HitSet", + "set_att": "AttackSet", + "set_coop": "UnitySet", + "set_counter": "CounterSet", + "set_cri_dmg": "DestructionSet", + "set_cri": "CriticalSet", + "set_def": "DefenseSet", + "set_immune": "ImmunitySet", + "set_max_hp": "HealthSet", + "set_penetrate": "PenetrationSet", + "set_rage": "RageSet", + "set_res": "ResistSet", + "set_revenge": "RevengeSet", + "set_scar": "InjurySet", + "set_speed": "SpeedSet", + "set_vampire": "LifestealSet", + "set_shield": "ProtectionSet", + "set_torrent": "TorrentSet", + "set_revenant": "ReversalSet", + "set_riposte": "RiposteSet", + "set_chase": "PursuitSet", + "set_opener": "WarfareSet", + } + for _, raw := range items { + item, ok := raw.(map[string]interface{}) + if !ok { + continue + } + total++ + if f, ok := item["f"]; !ok || f == nil || f == "" { + emptyF++ + } else { + if f == "set_max_hp" { + healthF++ + } + } + setValue, _ := item["set"].(string) + if setValue == "" { + emptySet++ + fValue, _ := item["f"].(string) + if fValue != "" { + emptySetByF[fValue] = emptySetByF[fValue] + 1 + } + if gear, ok := item["gear"].(string); ok && gear != "" { + emptySetByGear[gear] = emptySetByGear[gear] + 1 + } + if fValue != "" { + if mapped, ok := ingameSetMap[fValue]; ok { + setCounts[mapped] = setCounts[mapped] + 1 + } else { + uncategorized++ + } + } else { + uncategorized++ + } + } else { + setCounts[setValue] = setCounts[setValue] + 1 + } + if set, ok := item["set"].(string); ok && set == "HealthSet" { + healthSet++ + } + if f, ok := item["f"]; ok && f == "set_max_hp" { + mapHp++ + } + } + log.Printf("[service] set stats: total=%d empty_f=%d empty_set=%d set_max_hp=%d HealthSet=%d mapped_hp=%d", total, emptyF, emptySet, healthF, healthSet, mapHp) + log.Printf("[service] set counts: %+v", setCounts) + log.Printf("[service] uncategorized items: %d", uncategorized) + if emptySet > 0 { + log.Printf("[service] empty set by f: %+v", emptySetByF) + log.Printf("[service] empty set by gear: %+v", emptySetByGear) + } +} + // StartCaptureWithFilter allows frontend to provide a custom BPF filter. func (a *App) StartCaptureWithFilter(interfaceName string, filter string) error { a.logger.Info("StartCaptureWithFilter requested", "interface", interfaceName, "filter", filter) diff --git a/internal/service/parser_service.go b/internal/service/parser_service.go index b0756f9..52d578c 100644 --- a/internal/service/parser_service.go +++ b/internal/service/parser_service.go @@ -10,8 +10,12 @@ import ( "fmt" "io/ioutil" "math" + "os" + "path/filepath" + "sort" "net/http" "strings" + "sync" "time" ) @@ -19,6 +23,9 @@ type ParserService struct { config *config.Config logger *utils.Logger hexParser *parser.HexParser + heroBase map[string]heroBaseStats + heroOnce sync.Once + heroErr error } func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService { @@ -26,6 +33,7 @@ func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService { config: cfg, logger: logger, hexParser: parser.NewHexParser(), + heroBase: make(map[string]heroBaseStats), } } @@ -207,6 +215,8 @@ func (ps *ParserService) convertSingleItem(item map[string]interface{}) map[stri // convertUnits 转换英雄数据 func (ps *ParserService) convertUnits(rawUnits []interface{}) []map[string]interface{} { var convertedUnits []map[string]interface{} + ps.ensureHeroBaseData() + for _, rawUnit := range rawUnits { if unitMap, ok := rawUnit.(map[string]interface{}); ok { @@ -225,6 +235,12 @@ func (ps *ParserService) convertUnits(rawUnits []interface{}) []map[string]inter convertedUnit["awaken"] = z } + if code, ok := unitMap["code"].(string); ok && code != "" { + if base, ok := ps.heroBase[code]; ok { + ps.applyHeroBase(convertedUnit, base) + } + } + convertedUnits = append(convertedUnits, convertedUnit) } } @@ -520,3 +536,232 @@ func (ps *ParserService) convertItemsAllWithLog(rawItems []interface{}) []map[st } return convertedItems } + + +type heroBaseStats struct { + Atk float64 + Def float64 + Hp float64 + Spd float64 + Cr float64 + Cd float64 + Eff float64 + Res float64 +} + +type heroDataEntry struct { + Code string `json:"code"` + Name string `json:"name"` + CalculatedStatus struct { + Lv60SixStarFullyAwakened struct { + Atk float64 `json:"atk"` + Def float64 `json:"def"` + Hp float64 `json:"hp"` + Spd float64 `json:"spd"` + Chc float64 `json:"chc"` + Chd float64 `json:"chd"` + Eff float64 `json:"eff"` + Efr float64 `json:"efr"` + } `json:"lv60SixStarFullyAwakened"` + } `json:"calculatedStatus"` +} + +func (ps *ParserService) ensureHeroBaseData() { + ps.heroOnce.Do(func() { + homeDir, err := os.UserHomeDir() + if err != nil { + ps.heroErr = err + ps.logger.Error("load hero data failed", "error", err) + return + } + path := filepath.Join(homeDir, ".equipment-analyzer", "herodata.json") + body, err := ioutil.ReadFile(path) + if err != nil { + ps.heroErr = err + ps.logger.Error("load hero data failed", "error", err) + return + } + var raw map[string]heroDataEntry + if err := json.Unmarshal(body, &raw); err != nil { + ps.heroErr = err + ps.logger.Error("load hero data failed", "error", err) + return + } + for _, entry := range raw { + if entry.Code == "" { + continue + } + stats := entry.CalculatedStatus.Lv60SixStarFullyAwakened + ps.heroBase[entry.Code] = heroBaseStats{ + Atk: stats.Atk, + Def: stats.Def, + Hp: stats.Hp, + Spd: stats.Spd, + Cr: stats.Chc * 100, + Cd: stats.Chd * 100, + Eff: stats.Eff * 100, + Res: stats.Efr * 100, + } + } + ps.logger.Info("hero base data loaded", "count", len(ps.heroBase)) + }) +} + + +func (ps *ParserService) FillHeroBase(heroes []model.OptimizeHero) error { + ps.ensureHeroBaseData() + if len(ps.heroBase) == 0 { + return fmt.Errorf("hero base data not loaded") + } + for i := range heroes { + code := heroes[i].Code + if code == "" { + continue + } + base, ok := ps.heroBase[code] + if !ok { + continue + } + if heroes[i].BaseAtk == 0 { + heroes[i].BaseAtk = base.Atk + } + if heroes[i].BaseDef == 0 { + heroes[i].BaseDef = base.Def + } + if heroes[i].BaseHp == 0 { + heroes[i].BaseHp = base.Hp + } + if heroes[i].BaseSpd == 0 { + heroes[i].BaseSpd = base.Spd + } + if heroes[i].BaseCr == 0 { + heroes[i].BaseCr = base.Cr + } + if heroes[i].BaseCd == 0 { + heroes[i].BaseCd = base.Cd + } + if heroes[i].BaseEff == 0 { + heroes[i].BaseEff = base.Eff + } + if heroes[i].BaseRes == 0 { + heroes[i].BaseRes = base.Res + } + + if heroes[i].Atk == 0 { + heroes[i].Atk = base.Atk + } + if heroes[i].Def == 0 { + heroes[i].Def = base.Def + } + if heroes[i].Hp == 0 { + heroes[i].Hp = base.Hp + } + if heroes[i].Spd == 0 { + heroes[i].Spd = base.Spd + } + if heroes[i].Cr == 0 { + heroes[i].Cr = base.Cr + } + if heroes[i].Cd == 0 { + heroes[i].Cd = base.Cd + } + if heroes[i].Eff == 0 { + heroes[i].Eff = base.Eff + } + if heroes[i].Res == 0 { + heroes[i].Res = base.Res + } + } + return nil +} + +func (ps *ParserService) GetHeroTemplates() ([]model.HeroTemplate, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + path := filepath.Join(homeDir, ".equipment-analyzer", "herodata.json") + body, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var raw map[string]heroDataEntry + if err := json.Unmarshal(body, &raw); err != nil { + return nil, err + } + list := make([]model.HeroTemplate, 0, len(raw)) + for _, entry := range raw { + if entry.Code == "" { + continue + } + stats := entry.CalculatedStatus.Lv60SixStarFullyAwakened + list = append(list, model.HeroTemplate{ + Code: entry.Code, + Name: entry.Name, + BaseAtk: stats.Atk, + BaseDef: stats.Def, + BaseHp: stats.Hp, + BaseSpd: stats.Spd, + BaseCr: stats.Chc * 100, + BaseCd: stats.Chd * 100, + BaseEff: stats.Eff * 100, + BaseRes: stats.Efr * 100, + }) + } + sort.Slice(list, func(i, j int) bool { + return list[i].Name < list[j].Name + }) + return list, nil +} + +func (ps *ParserService) applyHeroBase(unit map[string]interface{}, base heroBaseStats) { + if _, ok := unit["baseAtk"]; !ok { + unit["baseAtk"] = base.Atk + } + if _, ok := unit["baseDef"]; !ok { + unit["baseDef"] = base.Def + } + if _, ok := unit["baseHp"]; !ok { + unit["baseHp"] = base.Hp + } + if _, ok := unit["baseSpd"]; !ok { + unit["baseSpd"] = base.Spd + } + if _, ok := unit["baseCr"]; !ok { + unit["baseCr"] = base.Cr + } + if _, ok := unit["baseCd"]; !ok { + unit["baseCd"] = base.Cd + } + if _, ok := unit["baseEff"]; !ok { + unit["baseEff"] = base.Eff + } + if _, ok := unit["baseRes"]; !ok { + unit["baseRes"] = base.Res + } + + if _, ok := unit["atk"]; !ok { + unit["atk"] = base.Atk + } + if _, ok := unit["def"]; !ok { + unit["def"] = base.Def + } + if _, ok := unit["hp"]; !ok { + unit["hp"] = base.Hp + } + if _, ok := unit["spd"]; !ok { + unit["spd"] = base.Spd + } + if _, ok := unit["cr"]; !ok { + unit["cr"] = base.Cr + } + if _, ok := unit["cd"]; !ok { + unit["cd"] = base.Cd + } + if _, ok := unit["eff"]; !ok { + unit["eff"] = base.Eff + } + if _, ok := unit["res"]; !ok { + unit["res"] = base.Res + } +}