feat(database): add gearTxt field to parsed results and update related functions
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -186,19 +186,6 @@
|
||||
color: #9fb3e5;
|
||||
}
|
||||
|
||||
.optimizer-weight-row {
|
||||
display: grid;
|
||||
grid-template-columns: 38px 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.optimizer-weight-label {
|
||||
color: #c9d7f7;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.optimizer-left-panels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -206,83 +193,139 @@
|
||||
}
|
||||
|
||||
.optimizer-side-panel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(124px, 0.82fr) minmax(176px, 1.18fr);
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.optimizer-left-panels {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.optimizer-weight-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.optimizer-weight-panel {
|
||||
background: linear-gradient(180deg, #1f2c49 0%, #1a233b 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #18243c 0%, #11192d 100%);
|
||||
border: 1px solid rgba(145, 174, 226, 0.18);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
align-self: flex-start;
|
||||
height: fit-content;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.optimizer-weight-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 34px 1fr;
|
||||
.optimizer-weight-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
background: rgba(6, 10, 20, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
justify-content: space-between;
|
||||
height: 20px;
|
||||
color: #e4ecff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.optimizer-weight-row-no-label {
|
||||
grid-template-columns: 20px 1fr;
|
||||
}
|
||||
|
||||
.optimizer-weight-row-no-label .optimizer-weight-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.optimizer-weight-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.optimizer-weight-row .ant-slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.optimizer-weight-icon {
|
||||
width: 18px;
|
||||
.optimizer-weight-scale {
|
||||
min-width: 36px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #c9d7f7;
|
||||
border-radius: 999px;
|
||||
background: rgba(123, 180, 255, 0.14);
|
||||
color: #9fc4ff;
|
||||
font-size: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.optimizer-weight-list {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.optimizer-weight-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 34px minmax(72px, 1fr) 26px;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
line-height: 1;
|
||||
background: rgba(7, 12, 24, 0.62);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 8px;
|
||||
padding: 2px 6px;
|
||||
transition: border-color 0.16s ease, background 0.16s ease;
|
||||
}
|
||||
|
||||
.optimizer-weight-row:hover {
|
||||
background: rgba(10, 18, 34, 0.82);
|
||||
border-color: rgba(123, 180, 255, 0.28);
|
||||
}
|
||||
|
||||
.optimizer-weight-row .ant-slider {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.optimizer-weight-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(180deg, rgba(123, 180, 255, 0.26), rgba(123, 180, 255, 0.1));
|
||||
color: #dbe8ff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.optimizer-weight-label {
|
||||
color: #c9d7f7;
|
||||
color: #d2def7;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.optimizer-weight-slider {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.optimizer-weight-value {
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.optimizer-weight-slider .ant-slider-rail {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.optimizer-weight-slider .ant-slider-track {
|
||||
background-color: #7bb4ff;
|
||||
background-color: #83bcff;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
@@ -290,7 +333,7 @@
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: -1px;
|
||||
border-color: #7bb4ff;
|
||||
border-color: #83bcff;
|
||||
box-shadow: none;
|
||||
border-radius: 50%;
|
||||
background-color: #ffffff;
|
||||
@@ -299,15 +342,151 @@
|
||||
.optimizer-weight-slider .ant-slider-handle::after {
|
||||
display: none;
|
||||
}
|
||||
.optimizer-weight-row .ant-slider-rail {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
|
||||
.optimizer-progress-modal {
|
||||
padding: 6px 2px 2px;
|
||||
}
|
||||
|
||||
.optimizer-weight-row .ant-slider-track {
|
||||
background-color: #6aa9ff;
|
||||
.optimizer-progress-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.optimizer-weight-row .ant-slider-handle {
|
||||
border-color: #6aa9ff;
|
||||
background-color: #ffffff;
|
||||
.optimizer-progress-bar .ant-progress-outer {
|
||||
width: 100%;
|
||||
padding-inline-end: 0;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.optimizer-progress-bar .ant-progress-inner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.optimizer-progress-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
min-height: 22px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.optimizer-progress-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.optimizer-progress-text b {
|
||||
display: inline-block;
|
||||
min-width: 86px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.optimizer-progress-percent {
|
||||
flex: 0 0 54px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.optimizer-result-tools {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.optimizer-result-panel {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.optimizer-result-tool-title {
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.optimizer-result-tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.optimizer-result-filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(150px, 1fr));
|
||||
gap: 8px 10px;
|
||||
}
|
||||
|
||||
.optimizer-result-filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) 10px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #4b5563;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.optimizer-result-filter-row .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.optimizer-result-range-separator {
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.optimizer-result-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.optimizer-result-table .ant-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.optimizer-result-table .ant-table-thead > tr > th,
|
||||
.optimizer-result-table .ant-table-tbody > tr > td {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.optimizer-result-table .ant-table-column-sorters {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 16px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.optimizer-result-table .ant-table-column-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.optimizer-result-table .ant-table-column-sorter {
|
||||
width: 16px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.optimizer-result-set-tag {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.optimizer-result-filter-grid {
|
||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
429
frontend/src/utils/optimizer.ts
Normal file
429
frontend/src/utils/optimizer.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
export type RawItem = {
|
||||
id?: string | number;
|
||||
level?: number;
|
||||
enhance?: number;
|
||||
rank?: string;
|
||||
gear?: string;
|
||||
set?: string;
|
||||
f?: string;
|
||||
main?: { type?: string; value?: number };
|
||||
substats?: Array<{ type?: string; value?: number }>;
|
||||
};
|
||||
|
||||
export type RawHero = {
|
||||
id?: string | number;
|
||||
code?: string;
|
||||
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;
|
||||
};
|
||||
|
||||
export type StatKey = 'atk' | 'def' | 'hp' | 'spd' | 'cr' | 'cd' | 'acc' | 'res';
|
||||
export type MainStatKey =
|
||||
| 'Attack'
|
||||
| 'Defense'
|
||||
| 'Health'
|
||||
| 'AttackPercent'
|
||||
| 'DefensePercent'
|
||||
| 'HealthPercent'
|
||||
| 'CriticalHitChancePercent'
|
||||
| 'CriticalHitDamagePercent'
|
||||
| 'EffectivenessPercent'
|
||||
| 'EffectResistancePercent'
|
||||
| 'Speed';
|
||||
export type StatRange = { min?: number; max?: number };
|
||||
export type Filters = Record<StatKey, StatRange>;
|
||||
|
||||
export type ItemStats = {
|
||||
atk: number;
|
||||
def: number;
|
||||
hp: number;
|
||||
spd: number;
|
||||
cr: number;
|
||||
cd: number;
|
||||
acc: number;
|
||||
res: number;
|
||||
atkPct: number;
|
||||
defPct: number;
|
||||
hpPct: number;
|
||||
};
|
||||
|
||||
export type 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;
|
||||
};
|
||||
|
||||
export type SetFilters = {
|
||||
set1: string[];
|
||||
set2: string[];
|
||||
set3: string[];
|
||||
};
|
||||
|
||||
export const emptyFilters: Filters = {
|
||||
atk: {},
|
||||
def: {},
|
||||
hp: {},
|
||||
spd: {},
|
||||
cr: {},
|
||||
cd: {},
|
||||
acc: {},
|
||||
res: {},
|
||||
};
|
||||
|
||||
export const emptySetFilters: SetFilters = {
|
||||
set1: [],
|
||||
set2: [],
|
||||
set3: [],
|
||||
};
|
||||
|
||||
export const emptyBonusStats: BonusStats = {
|
||||
atk: 0,
|
||||
def: 0,
|
||||
hp: 0,
|
||||
atkPct: 0,
|
||||
defPct: 0,
|
||||
hpPct: 0,
|
||||
spd: 0,
|
||||
cr: 0,
|
||||
cd: 0,
|
||||
eff: 0,
|
||||
res: 0,
|
||||
finalAtkMultiplier: 0,
|
||||
finalDefMultiplier: 0,
|
||||
finalHpMultiplier: 0,
|
||||
};
|
||||
|
||||
export const FOUR_PIECE_SETS = new Set([
|
||||
'AttackSet',
|
||||
'SpeedSet',
|
||||
'DestructionSet',
|
||||
'LifestealSet',
|
||||
'ProtectionSet',
|
||||
'CounterSet',
|
||||
'RageSet',
|
||||
'RevengeSet',
|
||||
'InjurySet',
|
||||
'ReversalSet',
|
||||
'RiposteSet',
|
||||
'WarfareSet',
|
||||
]);
|
||||
|
||||
export const TWO_PIECE_SETS = new Set([
|
||||
'HealthSet',
|
||||
'DefenseSet',
|
||||
'CriticalSet',
|
||||
'HitSet',
|
||||
'ResistSet',
|
||||
'UnitySet',
|
||||
'ImmunitySet',
|
||||
'PenetrationSet',
|
||||
'TorrentSet',
|
||||
'PursuitSet',
|
||||
]);
|
||||
|
||||
export const toNumber = (value: any) => {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return 0;
|
||||
return value;
|
||||
};
|
||||
|
||||
export 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;
|
||||
};
|
||||
|
||||
export 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),
|
||||
};
|
||||
};
|
||||
|
||||
export const scoreStats = (stats: ItemStats, baseStats: ReturnType<typeof buildBaseStats>) => {
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
export const buildTotalStats = (
|
||||
statsList: ItemStats[],
|
||||
baseStats: ReturnType<typeof buildBaseStats>,
|
||||
bonusStats: BonusStats,
|
||||
items: RawItem[]
|
||||
) => {
|
||||
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;
|
||||
});
|
||||
|
||||
const bonusBaseAtk = baseStats.atk + baseStats.atk * (bonusStats.atkPct / 100) + bonusStats.atk;
|
||||
const bonusBaseDef = baseStats.def + baseStats.def * (bonusStats.defPct / 100) + bonusStats.def;
|
||||
const bonusBaseHp = baseStats.hp + baseStats.hp * (bonusStats.hpPct / 100) + bonusStats.hp;
|
||||
|
||||
const bonusMaxAtk = 1 + bonusStats.finalAtkMultiplier / 100;
|
||||
const bonusMaxDef = 1 + bonusStats.finalDefMultiplier / 100;
|
||||
const bonusMaxHp = 1 + bonusStats.finalHpMultiplier / 100;
|
||||
|
||||
const counts = new Map<string, number>();
|
||||
items.forEach(item => {
|
||||
if (!item.set) return;
|
||||
counts.set(item.set, (counts.get(item.set) || 0) + 1);
|
||||
});
|
||||
|
||||
const attackSetBonus = (counts.get('AttackSet') || 0) >= 4 ? 0.45 * baseStats.atk : 0;
|
||||
const healthSetBonus = Math.floor((counts.get('HealthSet') || 0) / 2) * 0.20 * baseStats.hp;
|
||||
const defenseSetBonus = Math.floor((counts.get('DefenseSet') || 0) / 2) * 0.20 * baseStats.def;
|
||||
const speedSetBonus = (counts.get('SpeedSet') || 0) >= 4 ? 0.25 * baseStats.spd : 0;
|
||||
const revengeSetBonus = (counts.get('RevengeSet') || 0) >= 4 ? 0.12 * baseStats.spd : 0;
|
||||
const reversalSetBonus = (counts.get('ReversalSet') || 0) >= 4 ? 0.15 * baseStats.spd : 0;
|
||||
const criticalSetBonus = Math.floor((counts.get('CriticalSet') || 0) / 2) * 12;
|
||||
const hitSetBonus = Math.floor((counts.get('HitSet') || 0) / 2) * 20;
|
||||
const resistSetBonus = Math.floor((counts.get('ResistSet') || 0) / 2) * 20;
|
||||
const destructionSetBonus = (counts.get('DestructionSet') || 0) >= 4 ? 60 : 0;
|
||||
const warfareSetBonus = (counts.get('WarfareSet') || 0) >= 4 ? 0.20 * baseStats.hp : 0;
|
||||
const torrentSetPenalty = Math.floor((counts.get('TorrentSet') || 0) / 2) * (-0.10 * baseStats.hp);
|
||||
|
||||
const atkFlat = totals.atk + baseStats.atk * (totals.atkPct / 100);
|
||||
const defFlat = totals.def + baseStats.def * (totals.defPct / 100);
|
||||
const hpFlat = totals.hp + baseStats.hp * (totals.hpPct / 100);
|
||||
|
||||
const atk = (bonusBaseAtk + atkFlat + attackSetBonus) * bonusMaxAtk;
|
||||
const def = (bonusBaseDef + defFlat + defenseSetBonus) * bonusMaxDef;
|
||||
const hp = (bonusBaseHp + hpFlat + healthSetBonus + warfareSetBonus + torrentSetPenalty) * bonusMaxHp;
|
||||
const spd = baseStats.spd + totals.spd + speedSetBonus + revengeSetBonus + reversalSetBonus + bonusStats.spd;
|
||||
const cr = Math.min(100, baseStats.cr + totals.cr + criticalSetBonus + bonusStats.cr);
|
||||
const cd = Math.min(350, baseStats.cd + totals.cd + destructionSetBonus + bonusStats.cd);
|
||||
const acc = baseStats.acc + totals.acc + hitSetBonus + bonusStats.eff;
|
||||
const res = baseStats.res + totals.res + resistSetBonus + bonusStats.res;
|
||||
|
||||
return {atk, def, hp, spd, cr, cd, acc, res};
|
||||
};
|
||||
|
||||
export const passesFilters = (totals: ReturnType<typeof buildTotalStats>, filters: Filters) => {
|
||||
return Object.keys(filters).every(key => {
|
||||
const statKey = key as StatKey;
|
||||
const value = (totals as any)[statKey];
|
||||
const range = filters[statKey];
|
||||
if (range.min !== undefined && value < range.min) return false;
|
||||
if (range.max !== undefined && value > range.max) return false;
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export const isFourPieceSet = (setName: string) => FOUR_PIECE_SETS.has(setName);
|
||||
export const isTwoPieceSet = (setName: string) => TWO_PIECE_SETS.has(setName);
|
||||
|
||||
export const getSetPieceCount = (setName: string) => {
|
||||
if (!setName || setName === 'All') return 0;
|
||||
if (isFourPieceSet(setName)) return 4;
|
||||
if (isTwoPieceSet(setName)) return 2;
|
||||
return -1;
|
||||
};
|
||||
|
||||
export const getSelectedSetNames = (setFilters: SetFilters) => {
|
||||
return [setFilters.set1, setFilters.set2, setFilters.set3]
|
||||
.flat()
|
||||
.filter(setName => setName && setName !== 'All');
|
||||
};
|
||||
|
||||
export const getSetFilterPieceTotal = (setFilters: SetFilters) => {
|
||||
return getSelectedSetNames(setFilters).reduce((total, setName) => {
|
||||
const pieces = getSetPieceCount(setName);
|
||||
return pieces > 0 ? total + pieces : total;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const isValidSetFilterSelection = (setFilters: SetFilters) => {
|
||||
const selectedSets = getSelectedSetNames(setFilters);
|
||||
if (selectedSets.length === 0) return true;
|
||||
|
||||
const totalPieces = selectedSets.reduce((total, setName) => {
|
||||
const pieces = getSetPieceCount(setName);
|
||||
if (pieces <= 0) return Number.POSITIVE_INFINITY;
|
||||
return total + pieces;
|
||||
}, 0);
|
||||
|
||||
return totalPieces <= 6;
|
||||
};
|
||||
|
||||
const buildRequiredSetCounts = (setFilters: SetFilters) => {
|
||||
const required = new Map<string, number>();
|
||||
for (const setName of getSelectedSetNames(setFilters)) {
|
||||
const pieces = getSetPieceCount(setName);
|
||||
if (pieces <= 0) return null;
|
||||
required.set(setName, (required.get(setName) || 0) + pieces);
|
||||
}
|
||||
return required;
|
||||
};
|
||||
|
||||
export const getCompletedSets = (items: RawItem[]) => {
|
||||
const counts = new Map<string, number>();
|
||||
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;
|
||||
};
|
||||
|
||||
export const passesSetFilter = (items: RawItem[], setFilters: SetFilters) => {
|
||||
const requiredCounts = buildRequiredSetCounts(setFilters);
|
||||
if (!requiredCounts) return false;
|
||||
if (requiredCounts.size === 0) {
|
||||
return true;
|
||||
}
|
||||
if (!isValidSetFilterSelection(setFilters)) return false;
|
||||
|
||||
const counts = new Map<string, number>();
|
||||
items.forEach(item => {
|
||||
if (!item.set) return;
|
||||
counts.set(item.set, (counts.get(item.set) || 0) + 1);
|
||||
});
|
||||
|
||||
for (const [setName, requiredPieces] of requiredCounts) {
|
||||
if ((counts.get(setName) || 0) < requiredPieces) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
8
frontend/wailsjs/go/service/App.d.ts
vendored
8
frontend/wailsjs/go/service/App.d.ts
vendored
@@ -2,6 +2,8 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {model} from '../models';
|
||||
|
||||
export function CancelOptimizeBuilds():Promise<void>;
|
||||
|
||||
export function DeleteParsedSession(arg1:number):Promise<void>;
|
||||
|
||||
export function ExportCurrentData(arg1:string):Promise<void>;
|
||||
@@ -24,6 +26,8 @@ export function GetLatestParsedDataFromDatabase():Promise<model.ParsedResult>;
|
||||
|
||||
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
||||
|
||||
export function GetOptimizerPreference(arg1:string):Promise<string>;
|
||||
|
||||
export function GetParsedDataByID(arg1:number):Promise<model.ParsedResult>;
|
||||
|
||||
export function GetParsedSessions():Promise<Array<model.ParsedSession>>;
|
||||
@@ -38,12 +42,16 @@ export function ReadRawJsonFile():Promise<model.ParsedResult>;
|
||||
|
||||
export function SaveAppSetting(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function SaveOptimizerPreference(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string,arg4:string):Promise<void>;
|
||||
|
||||
export function StartCapture(arg1:string):Promise<void>;
|
||||
|
||||
export function StartCaptureWithFilter(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function StartOptimizeBuilds(arg1:model.OptimizeRequest):Promise<void>;
|
||||
|
||||
export function StopAndParseCapture():Promise<model.ParsedResult>;
|
||||
|
||||
export function StopCapture():Promise<void>;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CancelOptimizeBuilds() {
|
||||
return window['go']['service']['App']['CancelOptimizeBuilds']();
|
||||
}
|
||||
|
||||
export function DeleteParsedSession(arg1) {
|
||||
return window['go']['service']['App']['DeleteParsedSession'](arg1);
|
||||
}
|
||||
@@ -46,6 +50,10 @@ export function GetNetworkInterfaces() {
|
||||
return window['go']['service']['App']['GetNetworkInterfaces']();
|
||||
}
|
||||
|
||||
export function GetOptimizerPreference(arg1) {
|
||||
return window['go']['service']['App']['GetOptimizerPreference'](arg1);
|
||||
}
|
||||
|
||||
export function GetParsedDataByID(arg1) {
|
||||
return window['go']['service']['App']['GetParsedDataByID'](arg1);
|
||||
}
|
||||
@@ -74,6 +82,10 @@ export function SaveAppSetting(arg1, arg2) {
|
||||
return window['go']['service']['App']['SaveAppSetting'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SaveOptimizerPreference(arg1, arg2) {
|
||||
return window['go']['service']['App']['SaveOptimizerPreference'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SaveParsedDataToDatabase(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
@@ -86,6 +98,10 @@ export function StartCaptureWithFilter(arg1, arg2) {
|
||||
return window['go']['service']['App']['StartCaptureWithFilter'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function StartOptimizeBuilds(arg1) {
|
||||
return window['go']['service']['App']['StartOptimizeBuilds'](arg1);
|
||||
}
|
||||
|
||||
export function StopAndParseCapture() {
|
||||
return window['go']['service']['App']['StopAndParseCapture']();
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -6,7 +6,7 @@ toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
go.uber.org/zap v1.26.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
modernc.org/sqlite v1.45.0
|
||||
@@ -18,6 +18,7 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -14,6 +14,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
@@ -67,8 +69,8 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
|
||||
@@ -87,9 +87,17 @@ func (d *Database) initTables() error {
|
||||
updated_at INTEGER NOT NULL
|
||||
);`
|
||||
|
||||
optimizerPreferencesTable := `
|
||||
CREATE TABLE IF NOT EXISTS optimizer_preferences (
|
||||
hero_id TEXT PRIMARY KEY,
|
||||
options_json TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);`
|
||||
|
||||
tables := []string{
|
||||
parsedDataTable,
|
||||
settingsTable,
|
||||
optimizerPreferencesTable,
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
@@ -221,6 +229,9 @@ func (d *Database) GetSetting(key string) (string, error) {
|
||||
var value string
|
||||
err := d.db.QueryRow(stmt, key).Scan(&value)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
@@ -246,3 +257,26 @@ func (d *Database) GetAllSettings() (map[string]string, error) {
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// SaveOptimizerPreference saves the latest optimizer options for a hero.
|
||||
func (d *Database) SaveOptimizerPreference(heroID string, optionsJSON string) error {
|
||||
stmt := `
|
||||
INSERT OR REPLACE INTO optimizer_preferences (hero_id, options_json, updated_at)
|
||||
VALUES (?, ?, ?)`
|
||||
_, err := d.db.Exec(stmt, heroID, optionsJSON, time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOptimizerPreference returns saved optimizer options for a hero.
|
||||
func (d *Database) GetOptimizerPreference(heroID string) (string, error) {
|
||||
stmt := "SELECT options_json FROM optimizer_preferences WHERE hero_id = ?"
|
||||
var optionsJSON string
|
||||
err := d.db.QueryRow(stmt, heroID).Scan(&optionsJSON)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return optionsJSON, nil
|
||||
}
|
||||
|
||||
2115
internal/optimizer/optimizer.go
Normal file
2115
internal/optimizer/optimizer.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -116,3 +116,22 @@ func (s *DatabaseService) GetAllAppSettings() (map[string]string, error) {
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// SaveOptimizerPreference saves the latest optimizer options for a hero.
|
||||
func (s *DatabaseService) SaveOptimizerPreference(heroID string, optionsJSON string) error {
|
||||
if err := s.db.SaveOptimizerPreference(heroID, optionsJSON); err != nil {
|
||||
s.logger.Error("保存配装偏好失败", "error", err, "hero_id", heroID)
|
||||
return fmt.Errorf("保存配装偏好失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOptimizerPreference gets saved optimizer options for a hero.
|
||||
func (s *DatabaseService) GetOptimizerPreference(heroID string) (string, error) {
|
||||
optionsJSON, err := s.db.GetOptimizerPreference(heroID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取配装偏好失败", "error", err, "hero_id", heroID)
|
||||
return "", fmt.Errorf("获取配装偏好失败: %w", err)
|
||||
}
|
||||
return optionsJSON, nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"equipment-analyzer/internal/capture"
|
||||
@@ -23,6 +25,8 @@ type App struct {
|
||||
parserService *ParserService
|
||||
database *model.Database
|
||||
databaseService *DatabaseService
|
||||
optimizeMu sync.Mutex
|
||||
optimizeCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
||||
@@ -84,16 +88,97 @@ func (a *App) Shutdown(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// OptimizeBuilds runs the optimizer on latest parsed data.
|
||||
// OptimizeBuilds runs the optimizer on latest parsed data and returns when done.
|
||||
func (a *App) OptimizeBuilds(req model.OptimizeRequest) (*model.OptimizeResponse, error) {
|
||||
log.Printf("[service] OptimizeBuilds entry heroId=%s", req.HeroID)
|
||||
log.Printf("[service] OptimizeBuilds sync entry heroId=%s", req.HeroID)
|
||||
optCtx, cancel := context.WithCancel(context.Background())
|
||||
a.optimizeMu.Lock()
|
||||
if a.optimizeCancel != nil {
|
||||
a.optimizeMu.Unlock()
|
||||
cancel()
|
||||
return nil, fmt.Errorf("已有配装计算正在进行")
|
||||
}
|
||||
a.optimizeCancel = cancel
|
||||
a.optimizeMu.Unlock()
|
||||
defer func() {
|
||||
cancel()
|
||||
a.optimizeMu.Lock()
|
||||
a.optimizeCancel = nil
|
||||
a.optimizeMu.Unlock()
|
||||
}()
|
||||
|
||||
resp, err := a.runOptimizeBuilds(optCtx, req, a.emitOptimizeProgress)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
log.Printf("[service] OptimizeBuilds canceled")
|
||||
return nil, fmt.Errorf("配装计算已中断")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// StartOptimizeBuilds starts the optimizer in the background and reports progress through Wails events.
|
||||
func (a *App) StartOptimizeBuilds(req model.OptimizeRequest) error {
|
||||
log.Printf("[service] StartOptimizeBuilds entry heroId=%s", req.HeroID)
|
||||
optCtx, cancel := context.WithCancel(context.Background())
|
||||
a.optimizeMu.Lock()
|
||||
if a.optimizeCancel != nil {
|
||||
a.optimizeMu.Unlock()
|
||||
cancel()
|
||||
return fmt.Errorf("已有配装计算正在进行")
|
||||
}
|
||||
a.optimizeCancel = cancel
|
||||
a.optimizeMu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
cancel()
|
||||
a.optimizeMu.Lock()
|
||||
a.optimizeCancel = nil
|
||||
a.optimizeMu.Unlock()
|
||||
}()
|
||||
|
||||
resp, err := a.runOptimizeBuilds(optCtx, req, a.emitOptimizeProgress)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
log.Printf("[service] StartOptimizeBuilds canceled")
|
||||
if a.ctx != nil {
|
||||
runtime.EventsEmit(a.ctx, "optimize:canceled", "配装计算已中断")
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("[service] StartOptimizeBuilds optimize failed: %v", err)
|
||||
if a.ctx != nil {
|
||||
runtime.EventsEmit(a.ctx, "optimize:error", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("[service] StartOptimizeBuilds done results=%d totalCombos=%d", len(resp.Results), resp.TotalCombos)
|
||||
if a.ctx != nil {
|
||||
runtime.EventsEmit(a.ctx, "optimize:done", resp)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) runOptimizeBuilds(ctx context.Context, req model.OptimizeRequest, progress optimizer.ProgressCallback) (*model.OptimizeResponse, error) {
|
||||
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))
|
||||
itemCount := 0
|
||||
heroCount := 0
|
||||
if parsedResult != nil {
|
||||
itemCount = len(parsedResult.Items)
|
||||
heroCount = len(parsedResult.Heroes)
|
||||
}
|
||||
if parsedResult == nil || (itemCount == 0 && heroCount == 0) {
|
||||
log.Printf("[service] OptimizeBuilds no data items=%d heroes=%d", itemCount, heroCount)
|
||||
return &model.OptimizeResponse{TotalCombos: 0, Results: []model.OptimizeResult{}}, nil
|
||||
}
|
||||
|
||||
@@ -140,13 +225,37 @@ func (a *App) OptimizeBuilds(req model.OptimizeRequest) (*model.OptimizeResponse
|
||||
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
|
||||
return optimizer.OptimizeWithContext(ctx, items, heroes, req, progress)
|
||||
}
|
||||
|
||||
func (a *App) emitOptimizeProgress(checked int, total int, matched int) {
|
||||
if a.ctx == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("[service] OptimizeBuilds done results=%d totalCombos=%d", len(resp.Results), resp.TotalCombos)
|
||||
return resp, nil
|
||||
percent := 0.0
|
||||
if total > 0 {
|
||||
percent = float64(checked) * 100 / float64(total)
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "optimize:progress", map[string]interface{}{
|
||||
"checked": checked,
|
||||
"matched": matched,
|
||||
"total": total,
|
||||
"percent": percent,
|
||||
})
|
||||
}
|
||||
|
||||
// CancelOptimizeBuilds stops the currently running optimizer, if any.
|
||||
func (a *App) CancelOptimizeBuilds() error {
|
||||
a.optimizeMu.Lock()
|
||||
cancel := a.optimizeCancel
|
||||
a.optimizeMu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNetworkInterfaces returns available network interfaces.
|
||||
@@ -529,6 +638,28 @@ func (a *App) GetAllAppSettings() (map[string]string, error) {
|
||||
return a.databaseService.GetAllAppSettings()
|
||||
}
|
||||
|
||||
// SaveOptimizerPreference saves optimizer options for a hero.
|
||||
func (a *App) SaveOptimizerPreference(heroID string, optionsJSON string) error {
|
||||
if a.databaseService == nil {
|
||||
return fmt.Errorf("database service not initialized")
|
||||
}
|
||||
if heroID == "" {
|
||||
return fmt.Errorf("hero id is required")
|
||||
}
|
||||
return a.databaseService.SaveOptimizerPreference(heroID, optionsJSON)
|
||||
}
|
||||
|
||||
// GetOptimizerPreference gets optimizer options for a hero.
|
||||
func (a *App) GetOptimizerPreference(heroID string) (string, error) {
|
||||
if a.databaseService == nil {
|
||||
return "", fmt.Errorf("database service not initialized")
|
||||
}
|
||||
if heroID == "" {
|
||||
return "", nil
|
||||
}
|
||||
return a.databaseService.GetOptimizerPreference(heroID)
|
||||
}
|
||||
|
||||
func logMissingSetStats(items []interface{}) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user