feat(i18n): integrate i18next for internationalization support and add initial translation setup
This commit is contained in:
@@ -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<StatKey, StatRange>;
|
||||
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<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
|
||||
);
|
||||
};
|
||||
|
||||
const buildTotalStats = (statsList: ItemStats[], baseStats: ReturnType<typeof buildBaseStats>) => {
|
||||
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<typeof buildTotalStats>, 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<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;
|
||||
};
|
||||
|
||||
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<string, number>();
|
||||
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<BuildResult | null>(null);
|
||||
const [totalCombos, setTotalCombos] = useState(0);
|
||||
const {success, error, info} = useMessage();
|
||||
const [bonusByHeroId, setBonusByHeroId] = useState<Record<string, BonusStats>>({});
|
||||
const [bonusEditorOpen, setBonusEditorOpen] = useState(false);
|
||||
const [bonusDraft, setBonusDraft] = useState<BonusStats>(emptyBonusStats);
|
||||
|
||||
const leftPanelsRef = useRef<HTMLDivElement | null>(null);
|
||||
const [leftPanelsHeight, setLeftPanelsHeight] = useState<number | null>(null);
|
||||
@@ -423,24 +135,75 @@ export default function OptimizerPage() {
|
||||
const filterCardRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightPanelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const setPickerOptions = [
|
||||
const INGAME_SET_MAP: Record<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',
|
||||
};
|
||||
|
||||
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<string, number> = {};
|
||||
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<HTMLElement>) => {
|
||||
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<string, RawItem[]>();
|
||||
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<RawItem, ItemStats>();
|
||||
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="请选择英雄"
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (!selectedHeroKey) return;
|
||||
setBonusDraft({...selectedHeroBonus});
|
||||
setBonusEditorOpen(true);
|
||||
}}
|
||||
>
|
||||
{'\u7f16\u8f91\u989d\u5916\u52a0\u6210'}
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
@@ -900,8 +642,9 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat
|
||||
</div>
|
||||
</div>
|
||||
<div className="optimizer-weight-panel" style={leftPanelsHeight ? {height: leftPanelsHeight} : undefined}>
|
||||
<div className="optimizer-weight-row optimizer-weight-row-no-label">
|
||||
<div className="optimizer-weight-row">
|
||||
<span className="optimizer-weight-icon">攻</span>
|
||||
<span className="optimizer-weight-label">攻击</span>
|
||||
<Slider
|
||||
min={0}
|
||||
max={5}
|
||||
@@ -1077,6 +820,75 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat
|
||||
</Card>
|
||||
</Content>
|
||||
</Layout>
|
||||
<Modal
|
||||
open={bonusEditorOpen}
|
||||
title={'\u989d\u5916\u52a0\u6210'}
|
||||
onCancel={() => setBonusEditorOpen(false)}
|
||||
onOk={() => {
|
||||
if (!selectedHeroKey) return;
|
||||
setBonusByHeroId(prev => ({...prev, [selectedHeroKey]: bonusDraft}));
|
||||
setBonusEditorOpen(false);
|
||||
}}
|
||||
>
|
||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12}}>
|
||||
<div>
|
||||
<div>{'\u653b\u51fb'}</div>
|
||||
<InputNumber value={bonusDraft.atk} onChange={v => setBonusDraft(prev => ({...prev, atk: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u653b\u51fb%'}</div>
|
||||
<InputNumber value={bonusDraft.atkPct} onChange={v => setBonusDraft(prev => ({...prev, atkPct: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u9632\u5fa1'}</div>
|
||||
<InputNumber value={bonusDraft.def} onChange={v => setBonusDraft(prev => ({...prev, def: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u9632\u5fa1%'}</div>
|
||||
<InputNumber value={bonusDraft.defPct} onChange={v => setBonusDraft(prev => ({...prev, defPct: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u751f\u547d'}</div>
|
||||
<InputNumber value={bonusDraft.hp} onChange={v => setBonusDraft(prev => ({...prev, hp: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u751f\u547d%'}</div>
|
||||
<InputNumber value={bonusDraft.hpPct} onChange={v => setBonusDraft(prev => ({...prev, hpPct: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u901f\u5ea6'}</div>
|
||||
<InputNumber value={bonusDraft.spd} onChange={v => setBonusDraft(prev => ({...prev, spd: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u66b4\u51fb\u7387'}</div>
|
||||
<InputNumber value={bonusDraft.cr} onChange={v => setBonusDraft(prev => ({...prev, cr: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u66b4\u51fb\u4f24\u5bb3'}</div>
|
||||
<InputNumber value={bonusDraft.cd} onChange={v => setBonusDraft(prev => ({...prev, cd: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u6548\u679c\u547d\u4e2d'}</div>
|
||||
<InputNumber value={bonusDraft.eff} onChange={v => setBonusDraft(prev => ({...prev, eff: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u6548\u679c\u6297\u6027'}</div>
|
||||
<InputNumber value={bonusDraft.res} onChange={v => setBonusDraft(prev => ({...prev, res: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u6700\u7ec8\u653b\u51fb\u500d\u7387%'}</div>
|
||||
<InputNumber value={bonusDraft.finalAtkMultiplier} onChange={v => setBonusDraft(prev => ({...prev, finalAtkMultiplier: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u6700\u7ec8\u9632\u5fa1\u500d\u7387%'}</div>
|
||||
<InputNumber value={bonusDraft.finalDefMultiplier} onChange={v => setBonusDraft(prev => ({...prev, finalDefMultiplier: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{'\u6700\u7ec8\u751f\u547d\u500d\u7387%'}</div>
|
||||
<InputNumber value={bonusDraft.finalHpMultiplier} onChange={v => setBonusDraft(prev => ({...prev, finalHpMultiplier: Number(v || 0)}))} style={{width: '100%'}} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{setPickerOpen &&
|
||||
createPortal(
|
||||
<div
|
||||
@@ -1088,7 +900,7 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat
|
||||
style={{top: setPickerPos.top, left: setPickerPos.left}}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="optimizer-set-modal-list">
|
||||
<div className="optimizer-set-modal-list" style={{maxHeight: setPickerListHeight}}>
|
||||
{setPickerOptions.map(option => (
|
||||
<button
|
||||
key={option.key}
|
||||
@@ -1121,7 +933,7 @@ const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStat
|
||||
>
|
||||
<span className="optimizer-set-modal-icon">◎</span>
|
||||
<span className="optimizer-set-modal-label">{option.label}</span>
|
||||
<span className="optimizer-set-modal-count">{Math.floor(Math.random() * 200)}</span>
|
||||
<span className="optimizer-set-modal-count">{setCounts[option.setKey] || 0}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user