feat(database): add gearTxt field to parsed results and update related functions

This commit is contained in:
kever
2026-05-31 13:52:47 +08:00
parent 886c5fda08
commit bcf5d3d657
11 changed files with 3635 additions and 285 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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));
}
}

View 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;
};

View File

@@ -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>;

View File

@@ -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']();
}