Files
wails-epic/internal/optimizer/optimizer.go

2116 lines
50 KiB
Go

package optimizer
import (
"container/heap"
"context"
"encoding/json"
"fmt"
"log"
"math"
"runtime"
"sort"
"sync"
"sync/atomic"
"equipment-analyzer/internal/model"
)
const (
defaultMaxItemsPerSlot = 300
defaultMaxCombos = 2000000
defaultMaxResults = 5000
maxCollectedPerPass = 5000
maxCandidateItemsPerSlot = 5000
maxParallelSearchPasses = 4
progressReportInterval = 10000
setCoverageSearchBonus = 1000000
)
var gearSlots = []string{"Weapon", "Helmet", "Armor", "Necklace", "Ring", "Boots"}
var statKeys = []string{"atk", "def", "hp", "spd", "cr", "cd", "acc", "res"}
var fourPieceSets = map[string]bool{
"AttackSet": true,
"SpeedSet": true,
"DestructionSet": true,
"LifestealSet": true,
"ProtectionSet": true,
"CounterSet": true,
"RageSet": true,
"RevengeSet": true,
"InjurySet": true,
"ReversalSet": true,
"RiposteSet": true,
"WarfareSet": true,
}
var twoPieceSets = map[string]bool{
"HealthSet": true,
"DefenseSet": true,
"CriticalSet": true,
"HitSet": true,
"ResistSet": true,
"UnitySet": true,
"ImmunitySet": true,
"PenetrationSet": true,
"TorrentSet": true,
"PursuitSet": true,
}
type itemStats struct {
atk float64
def float64
hp float64
spd float64
cr float64
cd float64
acc float64
res float64
atkPct float64
defPct float64
hpPct float64
}
type baseStats struct {
atk float64
def float64
hp float64
spd float64
cr float64
cd float64
acc float64
res float64
}
// ParseItems converts raw interface data to optimizer items.
func ParseItems(items []interface{}) ([]model.OptimizeItem, error) {
if len(items) == 0 {
return []model.OptimizeItem{}, nil
}
data, err := json.Marshal(items)
if err != nil {
return nil, err
}
var out []model.OptimizeItem
if err := json.Unmarshal(data, &out); err != nil {
return nil, err
}
for i := range out {
if out[i].Set == "" && out[i].F != "" {
if mapped, ok := ingameSetMap[out[i].F]; ok {
out[i].Set = mapped
}
}
}
return out, nil
}
type ProgressCallback func(checked int, total int, matched int)
type scoredOptimizeItem struct {
item model.OptimizeItem
stats itemStats
score float64
}
type searchSlotItem struct {
item model.OptimizeItem
stats itemStats
score float64
requiredSetIndex int
}
type optimizeCandidateItem struct {
item model.OptimizeItem
stats itemStats
score float64
}
type optimizeSearchPass struct {
objective string
req model.OptimizeRequest
topItemsByGear [][]scoredOptimizeItem
maxCombos int
}
type optimizePassExecutionResult struct {
results []model.OptimizeResult
checked int
matched int
err error
}
// ParseHeroes converts raw interface data to optimizer heroes.
func ParseHeroes(heroes []interface{}) ([]model.OptimizeHero, error) {
if len(heroes) == 0 {
return []model.OptimizeHero{}, nil
}
data, err := json.Marshal(heroes)
if err != nil {
return nil, err
}
var out []model.OptimizeHero
if err := json.Unmarshal(data, &out); err != nil {
return nil, err
}
return out, nil
}
func Optimize(items []model.OptimizeItem, heroes []model.OptimizeHero, req model.OptimizeRequest) (*model.OptimizeResponse, error) {
return OptimizeWithContext(context.Background(), items, heroes, req, nil)
}
func OptimizeWithContext(ctx context.Context, items []model.OptimizeItem, heroes []model.OptimizeHero, req model.OptimizeRequest, progress ProgressCallback) (*model.OptimizeResponse, error) {
if ctx == nil {
ctx = context.Background()
}
if req.HeroID == "" {
return nil, fmt.Errorf("hero code is required")
}
hero := findHeroByCode(heroes, req.HeroID)
if hero == nil {
return nil, fmt.Errorf("hero not found")
}
maxItems := req.MaxItemsPerSlot
if maxItems <= 0 {
maxItems = defaultMaxItemsPerSlot
}
maxCombos := req.MaxCombos
if maxCombos <= 0 {
maxCombos = defaultMaxCombos
}
maxResults := req.MaxResults
if maxResults <= 0 {
maxResults = defaultMaxResults
}
base := buildBaseStats(hero)
bonus := req.BonusStats
setRequirements, requiredSetPieces, validSetFilters := buildSetRequirements(req.SetFilters)
if !validSetFilters {
return nil, fmt.Errorf("invalid set filter combination: selected set effects require more than 6 gear pieces")
}
if len(setRequirements) == 0 {
return nil, fmt.Errorf("set filter is required")
}
if !hasAnyWeightPreference(req.WeightValues) {
return nil, fmt.Errorf("attribute preference is required")
}
restrictToSelectedSets := len(setRequirements) > 0 && requiredSetPieces == len(gearSlots)
itemsByGear := map[string][]model.OptimizeItem{}
for _, slot := range gearSlots {
itemsByGear[slot] = []model.OptimizeItem{}
}
for _, item := range items {
if item.Gear == "" {
continue
}
if !itemPassesMainStatFilter(item, req.MainStatFilters) {
continue
}
if restrictToSelectedSets {
if item.Set == "" {
continue
}
if _, ok := setRequirements[item.Set]; !ok {
continue
}
}
if _, ok := itemsByGear[item.Gear]; ok {
itemsByGear[item.Gear] = append(itemsByGear[item.Gear], item)
}
}
statsCache := buildStatsCache(itemsByGear)
searchObjectives := buildSearchObjectives(req)
searchPasses := buildOptimizeSearchPasses(itemsByGear, statsCache, base, req, maxItems, maxCombos, setRequirements, !restrictToSelectedSets, searchObjectives)
totalExpectedCombos := 0
for _, pass := range searchPasses {
totalExpectedCombos += estimatePassCombos(pass.topItemsByGear, pass.maxCombos)
}
if progress != nil {
progress(0, totalExpectedCombos, 0)
}
passResults, checkedCount, matchedCount, uniqueMatchedCount, err := collectOptimizeSearchPasses(
ctx,
searchPasses,
statsCache,
base,
bonus,
req,
setRequirements,
totalExpectedCombos,
progress,
)
if err != nil {
return nil, err
}
resultByKey := map[string]model.OptimizeResult{}
for _, passResult := range passResults {
for _, result := range passResult.results {
resultByKey[result.Key] = result
}
}
results := make([]model.OptimizeResult, 0, len(resultByKey))
for _, result := range resultByKey {
results = append(results, result)
}
results = selectOptimizeResults(results, maxResults, searchObjectives)
log.Printf("[optimizer] checked=%d matched=%d unique=%d retained=%d returned=%d", checkedCount, matchedCount, uniqueMatchedCount, len(resultByKey), len(results))
if progress != nil {
progress(totalExpectedCombos, totalExpectedCombos, uniqueMatchedCount)
}
totalCombos := uniqueMatchedCount
return &model.OptimizeResponse{
TotalCombos: totalCombos,
Results: results,
}, nil
}
func collectOptimizeSearchPasses(
ctx context.Context,
searchPasses []optimizeSearchPass,
statsCache map[string]itemStats,
base baseStats,
bonus model.BonusStats,
req model.OptimizeRequest,
setRequirements map[string]int,
totalExpectedCombos int,
progress ProgressCallback,
) ([]optimizePassExecutionResult, int, int, int, error) {
if len(searchPasses) == 0 {
return nil, 0, 0, 0, nil
}
workerCtx, cancel := context.WithCancel(ctx)
defer cancel()
jobs := make(chan int, len(searchPasses))
results := make(chan optimizePassExecutionResult, len(searchPasses))
passChecked := make([]int64, len(searchPasses))
uniqueMatched := map[string]struct{}{}
var progressMu sync.Mutex
lastProgressChecked := 0
lastProgressMatched := 0
reportProgress := func() {
if progress == nil {
return
}
checked := sumAtomicInts(passChecked)
progressMu.Lock()
matched := len(uniqueMatched)
if checked < lastProgressChecked {
checked = lastProgressChecked
} else {
lastProgressChecked = checked
}
if matched < lastProgressMatched {
matched = lastProgressMatched
} else {
lastProgressMatched = matched
}
progress(checked, totalExpectedCombos, matched)
progressMu.Unlock()
}
recordMatched := func(key string) {
if key == "" {
return
}
progressMu.Lock()
uniqueMatched[key] = struct{}{}
progressMu.Unlock()
}
for index := range searchPasses {
jobs <- index
}
close(jobs)
workerCount := optimizeWorkerCount(len(searchPasses))
for worker := 0; worker < workerCount; worker++ {
go func() {
for index := range jobs {
pass := searchPasses[index]
passResults, checked, matched, err := collectOptimizeResults(
workerCtx,
pass.topItemsByGear,
statsCache,
base,
bonus,
pass.req,
pass.objective,
setRequirements,
req.WeightValues,
pass.maxCombos,
func(checked int, _ int) {
atomic.StoreInt64(&passChecked[index], int64(checked))
reportProgress()
},
recordMatched,
)
atomic.StoreInt64(&passChecked[index], int64(checked))
reportProgress()
if err != nil {
cancel()
}
results <- optimizePassExecutionResult{
results: passResults,
checked: checked,
matched: matched,
err: err,
}
}
}()
}
passResults := make([]optimizePassExecutionResult, 0, len(searchPasses))
var firstErr error
for range searchPasses {
result := <-results
passResults = append(passResults, result)
if result.err != nil && firstErr == nil {
firstErr = result.err
cancel()
}
}
checkedCount := 0
matchedCount := 0
for _, result := range passResults {
checkedCount += result.checked
matchedCount += result.matched
}
progressMu.Lock()
uniqueMatchedCount := len(uniqueMatched)
progressMu.Unlock()
return passResults, checkedCount, matchedCount, uniqueMatchedCount, firstErr
}
func optimizeWorkerCount(passCount int) int {
if passCount <= 1 {
return passCount
}
workers := runtime.NumCPU()
if workers > 1 {
workers--
}
if workers < 1 {
workers = 1
}
if workers > maxParallelSearchPasses {
workers = maxParallelSearchPasses
}
if workers > passCount {
workers = passCount
}
return workers
}
func sumAtomicInts(values []int64) int {
total := 0
for index := range values {
total += int(atomic.LoadInt64(&values[index]))
}
return total
}
func buildOptimizeSearchPasses(
itemsByGear map[string][]model.OptimizeItem,
statsCache map[string]itemStats,
base baseStats,
req model.OptimizeRequest,
maxItems int,
maxCombos int,
setRequirements map[string]int,
preferSelectedSets bool,
objectives []string,
) []optimizeSearchPass {
if len(objectives) == 0 {
objectives = []string{""}
}
passes := make([]optimizeSearchPass, len(objectives))
jobs := make(chan int, len(objectives))
for index := range objectives {
jobs <- index
}
close(jobs)
var wg sync.WaitGroup
workerCount := optimizeWorkerCount(len(objectives))
wg.Add(workerCount)
for worker := 0; worker < workerCount; worker++ {
go func() {
defer wg.Done()
for index := range jobs {
objective := objectives[index]
passReq := req
passReq.WeightValues = weightsForObjective(req.WeightValues, objective)
passes[index] = optimizeSearchPass{
objective: objective,
req: passReq,
topItemsByGear: buildTopItemsByGear(itemsByGear, statsCache, base, passReq, maxItems, setRequirements, preferSelectedSets),
maxCombos: maxCombos,
}
}
}()
}
wg.Wait()
return passes
}
func estimatePassCombos(topItemsByGear [][]scoredOptimizeItem, maxCombos int) int {
if len(topItemsByGear) != len(gearSlots) || maxCombos <= 0 {
return 0
}
total := 1
for _, slotItems := range topItemsByGear {
if len(slotItems) == 0 {
return 0
}
total *= len(slotItems)
if total >= maxCombos {
return maxCombos
}
}
return total
}
func buildStatsCache(itemsByGear map[string][]model.OptimizeItem) map[string]itemStats {
statsCache := map[string]itemStats{}
for _, items := range itemsByGear {
for _, item := range items {
statsCache[itemKey(item)] = buildItemStats(item)
}
}
return statsCache
}
func buildTopItemsByGear(
itemsByGear map[string][]model.OptimizeItem,
statsCache map[string]itemStats,
base baseStats,
req model.OptimizeRequest,
maxItems int,
setRequirements map[string]int,
preferSelectedSets bool,
) [][]scoredOptimizeItem {
focusKeys := buildCandidateFocusKeys(req)
topItemsByGear := make([][]scoredOptimizeItem, 0, len(gearSlots))
for _, slot := range gearSlots {
slotItems := itemsByGear[slot]
scored := make([]optimizeCandidateItem, 0, len(slotItems))
for _, item := range slotItems {
stats := statsCache[itemKey(item)]
score := scoreCandidateItem(stats, base, req)
scored = append(scored, optimizeCandidateItem{item: item, stats: stats, score: score})
}
sort.Slice(scored, func(i, j int) bool {
if scored[i].score == scored[j].score {
return itemKey(scored[i].item) < itemKey(scored[j].item)
}
return scored[i].score > scored[j].score
})
requestedLimit := maxItems
if requestedLimit > len(scored) {
requestedLimit = len(scored)
}
requiredSetKeys := sortedRequiredSetKeys(setRequirements)
candidateLimit := candidateLimitForSlot(len(scored), requestedLimit, focusKeys, requiredSetKeys, preferSelectedSets)
selected := buildCandidateUnion(scored, focusKeys, requiredSetKeys, base, requestedLimit, preferSelectedSets)
selected = paretoPruneCandidates(selected, focusKeys, base, setRequirements)
selected = capCandidatesByChannels(selected, focusKeys, requiredSetKeys, base, candidateLimit, preferSelectedSets)
sort.Slice(selected, func(i, j int) bool {
if selected[i].score == selected[j].score {
return itemKey(selected[i].item) < itemKey(selected[j].item)
}
return selected[i].score > selected[j].score
})
if len(selected) > candidateLimit {
selected = selected[:candidateLimit]
}
topItemsByGear = append(topItemsByGear, selected)
}
return topItemsByGear
}
func buildCandidateUnion(
scored []optimizeCandidateItem,
focusKeys []string,
requiredSetKeys []string,
base baseStats,
limit int,
preferSelectedSets bool,
) []scoredOptimizeItem {
if len(scored) == 0 || limit <= 0 {
return []scoredOptimizeItem{}
}
channelQuota := limit
if len(focusKeys) > 0 {
channelQuota = limit / 2
if channelQuota < 1 {
channelQuota = 1
}
}
if channelQuota > len(scored) {
channelQuota = len(scored)
}
selected := make([]scoredOptimizeItem, 0, minInt(len(scored), maxCandidateItemsPerSlot))
selectedKeys := map[string]struct{}{}
addCandidates := func(candidates []optimizeCandidateItem, quota int) {
for _, candidate := range candidates {
if len(selected) >= maxCandidateItemsPerSlot || quota <= 0 {
return
}
key := itemKey(candidate.item)
if _, ok := selectedKeys[key]; ok {
continue
}
selectedKeys[key] = struct{}{}
selected = append(selected, scoredOptimizeItem{
item: candidate.item,
stats: candidate.stats,
score: candidate.score,
})
quota--
}
}
addCandidates(scored, channelQuota)
for _, key := range focusKeys {
addCandidates(rankCandidatesByStat(scored, base, key), channelQuota)
}
if preferSelectedSets {
for _, setKey := range requiredSetKeys {
setCandidates := filterCandidatesBySet(scored, setKey)
addCandidates(setCandidates, channelQuota)
for _, key := range focusKeys {
addCandidates(rankCandidatesByStat(setCandidates, base, key), channelQuota)
}
}
}
return selected
}
func candidateLimitForSlot(
totalItems int,
requestedLimit int,
focusKeys []string,
requiredSetKeys []string,
preferSelectedSets bool,
) int {
if totalItems <= 0 || requestedLimit <= 0 {
return 0
}
channelCount := 1 + len(focusKeys)
if preferSelectedSets {
channelCount += len(requiredSetKeys) * (1 + len(focusKeys))
}
limit := requestedLimit
if channelCount > 1 {
limit = requestedLimit * channelCount
}
if limit > maxCandidateItemsPerSlot {
limit = maxCandidateItemsPerSlot
}
if limit > totalItems {
limit = totalItems
}
return limit
}
func paretoPruneCandidates(
selected []scoredOptimizeItem,
focusKeys []string,
base baseStats,
setRequirements map[string]int,
) []scoredOptimizeItem {
if len(selected) <= 1 || len(focusKeys) == 0 {
return selected
}
bySet := map[string][]scoredOptimizeItem{}
for _, candidate := range selected {
bySet[candidate.item.Set] = append(bySet[candidate.item.Set], candidate)
}
out := make([]scoredOptimizeItem, 0, len(selected))
for _, group := range bySet {
for i, candidate := range group {
dominated := false
for j, other := range group {
if i == j {
continue
}
if dominatesCandidate(other, candidate, focusKeys, base, setRequirements) {
dominated = true
break
}
}
if !dominated {
out = append(out, candidate)
}
}
}
return out
}
func capCandidatesByChannels(
selected []scoredOptimizeItem,
focusKeys []string,
requiredSetKeys []string,
base baseStats,
limit int,
preferSelectedSets bool,
) []scoredOptimizeItem {
if len(selected) <= limit || limit <= 0 {
return selected
}
candidates := optimizeCandidatesFromScored(selected)
sort.Slice(candidates, func(i, j int) bool {
return compareCandidateScore(candidates[i], candidates[j])
})
channelCount := 1 + len(focusKeys)
if preferSelectedSets {
channelCount += len(requiredSetKeys) * (1 + len(focusKeys))
}
channelQuota := limit / channelCount
if channelQuota < 1 {
channelQuota = 1
}
out := make([]scoredOptimizeItem, 0, limit)
outKeys := map[string]struct{}{}
addScored := func(items []scoredOptimizeItem, quota int) {
for _, item := range items {
if len(out) >= limit || quota <= 0 {
return
}
key := itemKey(item.item)
if _, ok := outKeys[key]; ok {
continue
}
outKeys[key] = struct{}{}
out = append(out, item)
quota--
}
}
addCandidates := func(items []optimizeCandidateItem, quota int) {
scoredItems := make([]scoredOptimizeItem, 0, len(items))
for _, item := range items {
scoredItems = append(scoredItems, scoredOptimizeItem{item: item.item, stats: item.stats, score: item.score})
}
addScored(scoredItems, quota)
}
addCandidates(candidates, channelQuota)
for _, key := range focusKeys {
addCandidates(rankCandidatesByStat(candidates, base, key), channelQuota)
}
if preferSelectedSets {
for _, setKey := range requiredSetKeys {
setCandidates := filterCandidatesBySet(candidates, setKey)
addCandidates(setCandidates, channelQuota)
for _, key := range focusKeys {
addCandidates(rankCandidatesByStat(setCandidates, base, key), channelQuota)
}
}
}
if len(out) < limit {
addCandidates(candidates, limit-len(out))
}
return out
}
func dominatesCandidate(
left scoredOptimizeItem,
right scoredOptimizeItem,
focusKeys []string,
base baseStats,
setRequirements map[string]int,
) bool {
if requiredSetRank(left.item.Set, setRequirements) < requiredSetRank(right.item.Set, setRequirements) {
return false
}
strictlyBetter := left.score > right.score
for _, key := range focusKeys {
leftValue := itemStatContribution(left.stats, base, key)
rightValue := itemStatContribution(right.stats, base, key)
if leftValue < rightValue {
return false
}
if leftValue > rightValue {
strictlyBetter = true
}
}
return strictlyBetter
}
func requiredSetRank(setName string, setRequirements map[string]int) int {
if setName == "" {
return 0
}
if _, ok := setRequirements[setName]; ok {
return 1
}
return 0
}
func rankCandidatesByStat(candidates []optimizeCandidateItem, base baseStats, key string) []optimizeCandidateItem {
ranked := append([]optimizeCandidateItem(nil), candidates...)
sort.Slice(ranked, func(i, j int) bool {
left := itemStatContribution(ranked[i].stats, base, key)
right := itemStatContribution(ranked[j].stats, base, key)
if left == right {
return compareCandidateScore(ranked[i], ranked[j])
}
return left > right
})
return ranked
}
func filterCandidatesBySet(candidates []optimizeCandidateItem, setKey string) []optimizeCandidateItem {
out := make([]optimizeCandidateItem, 0, len(candidates))
for _, candidate := range candidates {
if candidate.item.Set == setKey {
out = append(out, candidate)
}
}
return out
}
func optimizeCandidatesFromScored(items []scoredOptimizeItem) []optimizeCandidateItem {
out := make([]optimizeCandidateItem, 0, len(items))
for _, item := range items {
out = append(out, optimizeCandidateItem{
item: item.item,
stats: item.stats,
score: item.score,
})
}
return out
}
func compareCandidateScore(left optimizeCandidateItem, right optimizeCandidateItem) bool {
if left.score == right.score {
return itemKey(left.item) < itemKey(right.item)
}
return left.score > right.score
}
func minInt(left int, right int) int {
if left < right {
return left
}
return right
}
func buildCandidateFocusKeys(req model.OptimizeRequest) []string {
focusKeys := make([]string, 0, len(statKeys))
seen := map[string]struct{}{}
addKey := func(key string) {
if !isStatKey(key) {
return
}
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
focusKeys = append(focusKeys, key)
}
weightedKeys := make([]string, 0, len(statKeys))
for _, key := range statKeys {
if req.WeightValues[key] > 0 {
weightedKeys = append(weightedKeys, key)
}
}
sort.SliceStable(weightedKeys, func(i, j int) bool {
left := req.WeightValues[weightedKeys[i]]
right := req.WeightValues[weightedKeys[j]]
if left == right {
return weightedKeys[i] < weightedKeys[j]
}
return left > right
})
for _, key := range weightedKeys {
addKey(key)
}
for _, key := range statKeys {
if rangeVal, ok := req.StatFilters[key]; ok {
if rangeVal.Min != nil || rangeVal.Max != nil {
addKey(key)
}
}
}
return focusKeys
}
func buildSearchObjectives(req model.OptimizeRequest) []string {
objectives := []string{""}
weightedKeys := weightedStatKeys(req.WeightValues)
if len(weightedKeys) > 0 {
return append(objectives, weightedKeys...)
}
return append(objectives, statKeys...)
}
func weightedStatKeys(weights map[string]float64) []string {
keys := make([]string, 0, len(statKeys))
for _, key := range statKeys {
if weights[key] > 0 {
keys = append(keys, key)
}
}
sort.SliceStable(keys, func(i, j int) bool {
left := weights[keys[i]]
right := weights[keys[j]]
if left == right {
return keys[i] < keys[j]
}
return left > right
})
return keys
}
func sortedRequiredSetKeys(setRequirements map[string]int) []string {
keys := make([]string, 0, len(setRequirements))
for key := range setRequirements {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func cloneWeights(weights map[string]float64) map[string]float64 {
if len(weights) == 0 {
return nil
}
out := make(map[string]float64, len(weights))
for key, value := range weights {
out[key] = value
}
return out
}
func weightsForObjective(weights map[string]float64, objective string) map[string]float64 {
if objective == "" || !isStatKey(objective) {
return cloneWeights(weights)
}
out := make(map[string]float64, len(statKeys))
for _, key := range statKeys {
out[key] = 0
}
if weights[objective] > 0 {
out[objective] = weights[objective]
} else {
out[objective] = 1
}
return out
}
func hasAnyWeightPreference(weights map[string]float64) bool {
for _, key := range statKeys {
if weights[key] > 0 {
return true
}
}
return false
}
func isStatKey(key string) bool {
switch key {
case "atk", "def", "hp", "spd", "cr", "cd", "acc", "res":
return true
default:
return false
}
}
func collectOptimizeResults(
ctx context.Context,
topItemsByGear [][]scoredOptimizeItem,
statsCache map[string]itemStats,
base baseStats,
bonus model.BonusStats,
req model.OptimizeRequest,
objective string,
setRequirements map[string]int,
scoreWeights map[string]float64,
maxCombos int,
onProgress func(checked int, matched int),
onMatched func(key string),
) ([]model.OptimizeResult, int, int, error) {
if len(topItemsByGear) != 6 {
return []model.OptimizeResult{}, 0, 0, nil
}
searchItemsByGear, requiredPieces := buildSearchItemsByGear(topItemsByGear, setRequirements)
if len(searchItemsByGear) != 6 {
return []model.OptimizeResult{}, 0, 0, nil
}
checkedCount := 0
reportedCount := 0
matchedCount := 0
retained := &resultHeap{objective: objective}
heap.Init(retained)
startIndices := [6]int{}
startScore := 0.0
for slot := 0; slot < len(searchItemsByGear); slot++ {
if len(searchItemsByGear[slot]) == 0 {
return retained.items, checkedCount, matchedCount, nil
}
startScore += searchItemsByGear[slot][0].score
}
startScore += setCoverageScoreForSearch(searchItemsByGear, startIndices, requiredPieces)
startStates := []combinationState{{indices: startIndices, score: startScore}}
for _, setSeed := range buildSetCoverageSeedStates(searchItemsByGear, requiredPieces) {
if setSeed.indices == startIndices {
continue
}
startStates = append(startStates, setSeed)
}
states := &combinationHeap{}
heap.Init(states)
visited := map[combinationVisitKey]struct{}{}
for _, state := range startStates {
visitKey := packCombinationVisitKey(state.indices)
if _, ok := visited[visitKey]; ok {
continue
}
visited[visitKey] = struct{}{}
heap.Push(states, state)
}
for states.Len() > 0 && checkedCount < maxCombos {
state := heap.Pop(states).(combinationState)
checkedCount++
if checkedCount-reportedCount >= progressReportInterval {
if err := ctx.Err(); err != nil {
if onProgress != nil {
onProgress(checkedCount, matchedCount)
}
return retained.items, checkedCount, matchedCount, err
}
if onProgress != nil {
onProgress(checkedCount, matchedCount)
}
reportedCount = checkedCount
}
setCounts := setCountsArrayForSearch(searchItemsByGear, state.indices, len(requiredPieces))
currentCoverageScore := setCoverageScoreFromCountArray(setCounts, requiredPieces)
if passesRequiredSetCountsArray(setCounts, requiredPieces) {
totals := buildTotalStatsForSearch(searchItemsByGear, state.indices, base, bonus)
if passesFilters(totals, req.StatFilters) {
matchedCount++
itemsList := buildItemsListForSearch(searchItemsByGear, state.indices)
completed := getCompletedSets(itemsList)
setName := "无套装"
if len(completed) > 0 {
setName = joinSets(completed)
}
key := buildKey(itemsList)
if onMatched != nil {
onMatched(key)
}
result := model.OptimizeResult{
Key: key,
Sets: setName,
Atk: panelWholeStat(totals.atk),
Def: panelWholeStat(totals.def),
Hp: panelWholeStat(totals.hp),
Spd: panelRoundedStat(totals.spd),
Cr: math.Round(totals.cr),
Cd: math.Round(totals.cd),
Acc: math.Round(totals.acc),
Res: math.Round(totals.res),
Score: scoreTotals(totals, scoreWeights),
Items: itemsList,
}
retainResult(retained, result, maxCollectedPerPass)
}
}
for slot := 0; slot < len(searchItemsByGear); slot++ {
nextIndices := state.indices
nextIndices[slot]++
if nextIndices[slot] >= len(searchItemsByGear[slot]) {
continue
}
nextVisitKey := packCombinationVisitKey(nextIndices)
if _, ok := visited[nextVisitKey]; ok {
continue
}
visited[nextVisitKey] = struct{}{}
nextScore := state.score -
searchItemsByGear[slot][state.indices[slot]].score +
searchItemsByGear[slot][nextIndices[slot]].score
nextCounts := setCounts
currentSetIndex := searchItemsByGear[slot][state.indices[slot]].requiredSetIndex
if currentSetIndex >= 0 && currentSetIndex < len(requiredPieces) {
nextCounts[currentSetIndex]--
}
nextSetIndex := searchItemsByGear[slot][nextIndices[slot]].requiredSetIndex
if nextSetIndex >= 0 && nextSetIndex < len(requiredPieces) {
nextCounts[nextSetIndex]++
}
nextScore += setCoverageScoreFromCountArray(nextCounts, requiredPieces) - currentCoverageScore
heap.Push(states, combinationState{
indices: nextIndices,
score: nextScore,
})
}
}
if onProgress != nil {
onProgress(checkedCount, matchedCount)
}
return retained.items, checkedCount, matchedCount, nil
}
func buildSearchItemsByGear(topItemsByGear [][]scoredOptimizeItem, setRequirements map[string]int) ([][]searchSlotItem, []int) {
requiredSetKeys := sortedRequiredSetKeys(setRequirements)
requiredIndex := map[string]int{}
requiredPieces := make([]int, len(requiredSetKeys))
for index, setName := range requiredSetKeys {
requiredIndex[setName] = index
requiredPieces[index] = setRequirements[setName]
}
out := make([][]searchSlotItem, 0, len(topItemsByGear))
for _, slotItems := range topItemsByGear {
items := make([]searchSlotItem, 0, len(slotItems))
for _, candidate := range slotItems {
setIndex := -1
if index, ok := requiredIndex[candidate.item.Set]; ok {
setIndex = index
}
items = append(items, searchSlotItem{
item: candidate.item,
stats: candidate.stats,
score: candidate.score,
requiredSetIndex: setIndex,
})
}
out = append(out, items)
}
return out, requiredPieces
}
func buildSetCoverageSeedStates(searchItemsByGear [][]searchSlotItem, requiredPieces []int) []combinationState {
if len(requiredPieces) == 0 || len(searchItemsByGear) != len(gearSlots) {
return nil
}
assignments := buildRequiredSetSlotAssignments(requiredPieces)
seeds := make([]combinationState, 0, len(assignments))
for _, assignment := range assignments {
indices := [6]int{}
valid := true
for slot, setIndex := range assignment {
index := firstCandidateIndexForRequiredSet(searchItemsByGear[slot], setIndex)
if index < 0 {
valid = false
break
}
indices[slot] = index
}
if !valid {
continue
}
seeds = append(seeds, combinationState{
indices: indices,
score: combinationScoreForSearch(searchItemsByGear, indices) + setCoverageScoreForSearch(searchItemsByGear, indices, requiredPieces),
})
}
return seeds
}
func buildRequiredSetSlotAssignments(requiredPieces []int) []map[int]int {
assignments := []map[int]int{{}}
for setIndex, pieces := range requiredPieces {
if pieces <= 0 {
continue
}
nextAssignments := make([]map[int]int, 0, len(assignments))
for _, assignment := range assignments {
availableSlots := make([]int, 0, len(gearSlots))
for slot := range gearSlots {
if _, used := assignment[slot]; !used {
availableSlots = append(availableSlots, slot)
}
}
for _, slots := range chooseSlotCombos(availableSlots, pieces) {
next := cloneSlotAssignment(assignment)
for _, slot := range slots {
next[slot] = setIndex
}
nextAssignments = append(nextAssignments, next)
}
}
assignments = nextAssignments
}
return assignments
}
func chooseSlotCombos(slots []int, count int) [][]int {
if count <= 0 {
return [][]int{{}}
}
if count > len(slots) {
return nil
}
var out [][]int
var current []int
var walk func(start int)
walk = func(start int) {
if len(current) == count {
combo := append([]int(nil), current...)
out = append(out, combo)
return
}
needed := count - len(current)
for i := start; i <= len(slots)-needed; i++ {
current = append(current, slots[i])
walk(i + 1)
current = current[:len(current)-1]
}
}
walk(0)
return out
}
func cloneSlotAssignment(assignment map[int]int) map[int]int {
out := make(map[int]int, len(assignment))
for slot, setIndex := range assignment {
out[slot] = setIndex
}
return out
}
func firstCandidateIndexForRequiredSet(items []searchSlotItem, setIndex int) int {
for index, candidate := range items {
if candidate.requiredSetIndex == setIndex {
return index
}
}
return -1
}
func setCoverageScoreForSearch(searchItemsByGear [][]searchSlotItem, indices [6]int, requiredPieces []int) float64 {
if len(requiredPieces) == 0 || len(searchItemsByGear) != len(gearSlots) {
return 0
}
counts := setCountsArrayForSearch(searchItemsByGear, indices, len(requiredPieces))
return setCoverageScoreFromCountArray(counts, requiredPieces)
}
func setCoverageScoreFromCountArray(counts [6]int, requiredPieces []int) float64 {
score := 0.0
for setIndex, required := range requiredPieces {
if setIndex >= len(counts) {
break
}
if required <= 0 {
continue
}
covered := counts[setIndex]
if covered > required {
covered = required
}
score += float64(covered) * setCoverageSearchBonus
}
return score
}
func setCountsArrayForSearch(searchItemsByGear [][]searchSlotItem, indices [6]int, requiredSetCount int) [6]int {
var counts [6]int
if requiredSetCount <= 0 {
return counts
}
if requiredSetCount > len(counts) {
requiredSetCount = len(counts)
}
for slot := 0; slot < len(searchItemsByGear); slot++ {
index := indices[slot]
if index < 0 || index >= len(searchItemsByGear[slot]) {
continue
}
setIndex := searchItemsByGear[slot][index].requiredSetIndex
if setIndex >= 0 && setIndex < requiredSetCount {
counts[setIndex]++
}
}
return counts
}
func passesRequiredSetCountsArray(counts [6]int, requiredPieces []int) bool {
for index, required := range requiredPieces {
if required <= 0 {
continue
}
if index >= len(counts) || counts[index] < required {
return false
}
}
return true
}
func packCombinationVisitKey(indices [6]int) combinationVisitKey {
return combinationVisitKey{
lo: uint64(uint32(indices[0])) |
uint64(uint32(indices[1]))<<32,
hi: uint64(uint32(indices[2])) |
uint64(uint32(indices[3]))<<16 |
uint64(uint32(indices[4]))<<32 |
uint64(uint32(indices[5]))<<48,
}
}
func combinationScoreForSearch(searchItemsByGear [][]searchSlotItem, indices [6]int) float64 {
score := 0.0
for slot := 0; slot < len(searchItemsByGear); slot++ {
score += searchItemsByGear[slot][indices[slot]].score
}
return score
}
func resultObjectiveValue(result model.OptimizeResult, objective string) float64 {
switch objective {
case "atk":
return result.Atk
case "def":
return result.Def
case "hp":
return result.Hp
case "spd":
return result.Spd
case "cr":
return result.Cr
case "cd":
return result.Cd
case "acc":
return result.Acc
case "res":
return result.Res
default:
return result.Score
}
}
func compareResultsForObjective(a model.OptimizeResult, b model.OptimizeResult, objective string) int {
aPrimary := resultObjectiveValue(a, objective)
bPrimary := resultObjectiveValue(b, objective)
if aPrimary < bPrimary {
return -1
}
if aPrimary > bPrimary {
return 1
}
if a.Score < b.Score {
return -1
}
if a.Score > b.Score {
return 1
}
if a.Key < b.Key {
return -1
}
if a.Key > b.Key {
return 1
}
return 0
}
type resultHeap struct {
objective string
items []model.OptimizeResult
}
type combinationState struct {
indices [6]int
score float64
}
type combinationVisitKey struct {
lo uint64
hi uint64
}
type combinationHeap []combinationState
func (h combinationHeap) Len() int {
return len(h)
}
func (h combinationHeap) Less(i int, j int) bool {
return h[i].score > h[j].score
}
func (h combinationHeap) Swap(i int, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *combinationHeap) Push(x interface{}) {
*h = append(*h, x.(combinationState))
}
func (h *combinationHeap) Pop() interface{} {
old := *h
n := len(old)
item := old[n-1]
*h = old[:n-1]
return item
}
func (h resultHeap) Len() int {
return len(h.items)
}
func (h resultHeap) Less(i int, j int) bool {
return compareResultsForObjective(h.items[i], h.items[j], h.objective) < 0
}
func (h resultHeap) Swap(i int, j int) {
h.items[i], h.items[j] = h.items[j], h.items[i]
}
func (h *resultHeap) Push(x interface{}) {
h.items = append(h.items, x.(model.OptimizeResult))
}
func (h *resultHeap) Pop() interface{} {
old := h.items
n := len(old)
item := old[n-1]
h.items = old[:n-1]
return item
}
func retainResult(h *resultHeap, result model.OptimizeResult, limit int) {
if limit <= 0 {
return
}
if h.Len() < limit {
heap.Push(h, result)
return
}
if compareResultsForObjective(result, h.items[0], h.objective) <= 0 {
return
}
h.items[0] = result
heap.Fix(h, 0)
}
func selectOptimizeResults(results []model.OptimizeResult, maxResults int, objectives []string) []model.OptimizeResult {
if len(results) == 0 {
return []model.OptimizeResult{}
}
if maxResults <= 0 {
maxResults = defaultMaxResults
}
if len(objectives) <= 1 {
out := append([]model.OptimizeResult(nil), results...)
sort.Slice(out, func(i, j int) bool {
if out[i].Score == out[j].Score {
return out[i].Key < out[j].Key
}
return out[i].Score > out[j].Score
})
if len(out) > maxResults {
out = out[:maxResults]
}
return out
}
selected := map[string]struct{}{}
out := make([]model.OptimizeResult, 0, maxResults)
addQuota := maxResults / len(objectives)
if addQuota < 1 {
addQuota = 1
}
addTop := func(value func(model.OptimizeResult) float64) {
sorted := append([]model.OptimizeResult(nil), results...)
sort.Slice(sorted, func(i, j int) bool {
left := value(sorted[i])
right := value(sorted[j])
if left == right {
return sorted[i].Score > sorted[j].Score
}
return left > right
})
limit := addQuota
if limit > len(sorted) {
limit = len(sorted)
}
added := 0
for _, result := range sorted {
if len(out) >= maxResults || added >= limit {
return
}
if _, ok := selected[result.Key]; ok {
continue
}
selected[result.Key] = struct{}{}
out = append(out, result)
added++
}
}
for _, objective := range objectives {
currentObjective := objective
addTop(func(result model.OptimizeResult) float64 {
return resultObjectiveValue(result, currentObjective)
})
}
if len(selected) < maxResults {
sorted := append([]model.OptimizeResult(nil), results...)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Score == sorted[j].Score {
return sorted[i].Key < sorted[j].Key
}
return sorted[i].Score > sorted[j].Score
})
for _, result := range sorted {
if len(selected) >= maxResults {
break
}
if _, ok := selected[result.Key]; ok {
continue
}
selected[result.Key] = struct{}{}
out = append(out, result)
}
}
if len(out) > maxResults {
out = out[:maxResults]
}
return out
}
func findHeroByCode(heroes []model.OptimizeHero, code string) *model.OptimizeHero {
for i := range heroes {
if heroes[i].Code == code {
return &heroes[i]
}
}
return nil
}
func buildBaseStats(hero *model.OptimizeHero) baseStats {
atk := hero.BaseAtk
if atk == 0 {
atk = hero.Atk
}
def := hero.BaseDef
if def == 0 {
def = hero.Def
}
hp := hero.BaseHp
if hp == 0 {
hp = hero.Hp
}
spd := hero.BaseSpd
if spd == 0 {
spd = hero.Spd
}
cr := hero.BaseCr
if cr == 0 {
cr = hero.Cr
}
cd := hero.BaseCd
if cd == 0 {
cd = hero.Cd
}
acc := hero.BaseEff
if acc == 0 {
acc = hero.Eff
}
res := hero.BaseRes
if res == 0 {
res = hero.Res
}
return baseStats{
atk: atk,
def: def,
hp: hp,
spd: spd,
cr: cr,
cd: cd,
acc: acc,
res: res,
}
}
func buildItemStats(item model.OptimizeItem) itemStats {
stats := itemStats{}
apply := func(stat *model.OptimizeStat) {
if stat == nil {
return
}
switch stat.Type {
case "Attack":
stats.atk += stat.Value
case "Defense":
stats.def += stat.Value
case "Health":
stats.hp += stat.Value
case "Speed":
stats.spd += stat.Value
case "CriticalHitChancePercent":
stats.cr += stat.Value
case "CriticalHitDamagePercent":
stats.cd += stat.Value
case "EffectivenessPercent":
stats.acc += stat.Value
case "EffectResistancePercent":
stats.res += stat.Value
case "AttackPercent":
stats.atkPct += stat.Value
case "DefensePercent":
stats.defPct += stat.Value
case "HealthPercent":
stats.hpPct += stat.Value
}
}
if item.Main != nil {
apply(item.Main)
}
for i := range item.Substats {
stat := item.Substats[i]
apply(&stat)
}
return stats
}
func scoreCandidateItem(stats itemStats, base baseStats, req model.OptimizeRequest) float64 {
return scoreItemWeights(stats, base, req.WeightValues)
}
func itemStatContribution(stats itemStats, base baseStats, key string) float64 {
switch key {
case "atk":
return stats.atk + base.atk*(stats.atkPct/100)
case "def":
return stats.def + base.def*(stats.defPct/100)
case "hp":
return stats.hp + base.hp*(stats.hpPct/100)
case "spd":
return stats.spd
case "cr":
return stats.cr
case "cd":
return stats.cd
case "acc":
return stats.acc
case "res":
return stats.res
default:
return 0
}
}
func scoreItemWeights(stats itemStats, base baseStats, weights map[string]float64) float64 {
if len(weights) == 0 {
return 0
}
w := func(key string) float64 {
if val, ok := weights[key]; ok {
if val <= 0 {
return 0
}
return val
}
return 0
}
return (itemStatContribution(stats, base, "atk")/100)*w("atk") +
(itemStatContribution(stats, base, "def")/100)*w("def") +
(itemStatContribution(stats, base, "hp")/100)*w("hp") +
stats.spd*2*w("spd") +
stats.cr*w("cr") +
stats.cd*w("cd") +
stats.acc*w("acc") +
stats.res*w("res")
}
type totalsStats struct {
atk float64
def float64
hp float64
spd float64
cr float64
cd float64
acc float64
res float64
}
type setEffectCounts struct {
attack int
health int
defense int
speed int
revenge int
reversal int
critical int
hit int
resist int
destruction int
warfare int
torrent int
}
func buildTotalStats(statsList []itemStats, base baseStats, bonus model.BonusStats, items []model.OptimizeItem) totalsStats {
var totals itemStats
for _, stats := range statsList {
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
}
var counts setEffectCounts
for _, item := range items {
addSetEffectCount(&counts, item.Set)
}
return calculateTotalStats(totals, base, bonus, counts)
}
func buildTotalStatsForSearch(searchItemsByGear [][]searchSlotItem, indices [6]int, base baseStats, bonus model.BonusStats) totalsStats {
var totals itemStats
var counts setEffectCounts
for slot := 0; slot < len(searchItemsByGear); slot++ {
index := indices[slot]
if index < 0 || index >= len(searchItemsByGear[slot]) {
continue
}
candidate := searchItemsByGear[slot][index]
stats := candidate.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
addSetEffectCount(&counts, candidate.item.Set)
}
return calculateTotalStats(totals, base, bonus, counts)
}
func buildItemsListForSearch(searchItemsByGear [][]searchSlotItem, indices [6]int) []model.OptimizeItem {
itemsList := make([]model.OptimizeItem, 0, len(searchItemsByGear))
for slot := 0; slot < len(searchItemsByGear); slot++ {
itemsList = append(itemsList, searchItemsByGear[slot][indices[slot]].item)
}
return itemsList
}
func addSetEffectCount(counts *setEffectCounts, setName string) {
switch setName {
case "AttackSet":
counts.attack++
case "HealthSet":
counts.health++
case "DefenseSet":
counts.defense++
case "SpeedSet":
counts.speed++
case "RevengeSet":
counts.revenge++
case "ReversalSet":
counts.reversal++
case "CriticalSet":
counts.critical++
case "HitSet":
counts.hit++
case "ResistSet":
counts.resist++
case "DestructionSet":
counts.destruction++
case "WarfareSet":
counts.warfare++
case "TorrentSet":
counts.torrent++
}
}
func calculateTotalStats(totals itemStats, base baseStats, bonus model.BonusStats, counts setEffectCounts) totalsStats {
bonusBaseAtk := base.atk + base.atk*(bonus.AtkPct/100) + bonus.Atk
bonusBaseDef := base.def + base.def*(bonus.DefPct/100) + bonus.Def
bonusBaseHp := base.hp + base.hp*(bonus.HpPct/100) + bonus.Hp
bonusMaxAtk := 1 + bonus.FinalAtkMultiplier/100
bonusMaxDef := 1 + bonus.FinalDefMultiplier/100
bonusMaxHp := 1 + bonus.FinalHpMultiplier/100
attackSetBonus := 0.0
if counts.attack >= 4 {
attackSetBonus = 0.45 * base.atk
}
healthSetBonus := float64(counts.health/2) * 0.20 * base.hp
defenseSetBonus := float64(counts.defense/2) * 0.20 * base.def
speedSetBonus := 0.0
if counts.speed >= 4 {
speedSetBonus = 0.25 * base.spd
}
revengeSetBonus := 0.0
if counts.revenge >= 4 {
revengeSetBonus = 0.12 * base.spd
}
reversalSetBonus := 0.0
if counts.reversal >= 4 {
reversalSetBonus = 0.15 * base.spd
}
criticalSetBonus := float64(counts.critical/2) * 12
hitSetBonus := float64(counts.hit/2) * 20
resistSetBonus := float64(counts.resist/2) * 20
destructionSetBonus := 0.0
if counts.destruction >= 4 {
destructionSetBonus = 60
}
warfareSetBonus := 0.0
if counts.warfare >= 4 {
warfareSetBonus = 0.20 * base.hp
}
torrentSetPenalty := float64(counts.torrent/2) * (-0.10 * base.hp)
atkFlat := totals.atk + base.atk*(totals.atkPct/100)
defFlat := totals.def + base.def*(totals.defPct/100)
hpFlat := totals.hp + base.hp*(totals.hpPct/100)
atk := (bonusBaseAtk + atkFlat + attackSetBonus) * bonusMaxAtk
def := (bonusBaseDef + defFlat + defenseSetBonus) * bonusMaxDef
hp := (bonusBaseHp + hpFlat + healthSetBonus + warfareSetBonus + torrentSetPenalty) * bonusMaxHp
spd := base.spd + totals.spd + speedSetBonus + revengeSetBonus + reversalSetBonus + bonus.Spd
cr := math.Min(100, base.cr+totals.cr+criticalSetBonus+bonus.Cr)
cd := math.Min(350, base.cd+totals.cd+destructionSetBonus+bonus.Cd)
acc := base.acc + totals.acc + hitSetBonus + bonus.Eff
res := base.res + totals.res + resistSetBonus + bonus.Res
return totalsStats{
atk: atk,
def: def,
hp: hp,
spd: spd,
cr: cr,
cd: cd,
acc: acc,
res: res,
}
}
func panelWholeStat(value float64) float64 {
return math.Floor(value)
}
func panelRoundedStat(value float64) float64 {
return math.Round(value)
}
func passesFilters(t totalsStats, filters map[string]model.StatRange) bool {
if len(filters) == 0 {
return true
}
for key, rangeVal := range filters {
var value float64
switch key {
case "atk":
value = panelWholeStat(t.atk)
case "def":
value = panelWholeStat(t.def)
case "hp":
value = panelWholeStat(t.hp)
case "spd":
value = panelRoundedStat(t.spd)
case "cr":
value = panelRoundedStat(t.cr)
case "cd":
value = panelRoundedStat(t.cd)
case "acc":
value = panelRoundedStat(t.acc)
case "res":
value = panelRoundedStat(t.res)
default:
continue
}
if rangeVal.Min != nil && value < *rangeVal.Min {
return false
}
if rangeVal.Max != nil && value > *rangeVal.Max {
return false
}
}
return true
}
var 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",
}
func itemPassesMainStatFilter(item model.OptimizeItem, filters map[string]string) bool {
if len(filters) == 0 {
return true
}
var key string
switch item.Gear {
case "Necklace":
key = "necklace"
case "Ring":
key = "ring"
case "Boots":
key = "boots"
default:
return true
}
want, ok := filters[key]
if !ok || want == "" || want == "All" {
return true
}
return item.Main != nil && item.Main.Type == want
}
func passesMainStatFilter(items []model.OptimizeItem, filters map[string]string) bool {
if len(filters) == 0 {
return true
}
check := func(gear string, key string) bool {
want, ok := filters[key]
if !ok || want == "" || want == "All" {
return true
}
for _, item := range items {
if item.Gear != gear {
continue
}
if item.Main == nil || item.Main.Type == "" {
return false
}
return item.Main.Type == want
}
return true
}
if !check("Necklace", "necklace") {
return false
}
if !check("Ring", "ring") {
return false
}
if !check("Boots", "boots") {
return false
}
return true
}
func passesSetFilter(items []model.OptimizeItem, filters model.SetFilters) bool {
requiredCounts, _, ok := buildSetRequirements(filters)
if !ok {
return false
}
if len(requiredCounts) == 0 {
return true
}
counts := map[string]int{}
for _, item := range items {
if item.Set == "" {
continue
}
counts[item.Set]++
}
for setName, requiredPieces := range requiredCounts {
if counts[setName] < requiredPieces {
return false
}
}
return true
}
func buildSetRequirements(filters model.SetFilters) (map[string]int, int, bool) {
required := map[string]int{}
totalPieces := 0
for _, group := range [][]string{filters.Set1, filters.Set2, filters.Set3} {
for _, setName := range group {
if setName == "" || setName == "All" {
continue
}
pieces := setPieceCount(setName)
if pieces == 0 {
return required, totalPieces, false
}
totalPieces += pieces
if totalPieces > len(gearSlots) {
return required, totalPieces, false
}
required[setName] += pieces
}
}
return required, totalPieces, true
}
func setPieceCount(setName string) int {
if isFourPieceSet(setName) {
return 4
}
if isTwoPieceSet(setName) {
return 2
}
return 0
}
func scoreTotals(t totalsStats, weights map[string]float64) float64 {
if len(weights) == 0 {
return 0
}
w := func(key string) float64 {
if val, ok := weights[key]; ok {
if val <= 0 {
return 0
}
return val / 3
}
return 0
}
wAtk := w("atk")
wDef := w("def")
wHp := w("hp")
wSpd := w("spd")
wCr := w("cr")
wCd := w("cd")
wAcc := w("acc")
wRes := w("res")
return (t.atk/100)*wAtk +
(t.def/100)*wDef +
(t.hp/100)*wHp +
t.spd*2*wSpd +
t.cr*wCr +
t.cd*wCd +
t.acc*wAcc +
t.res*wRes
}
func getCompletedSets(items []model.OptimizeItem) []string {
counts := map[string]int{}
for _, item := range items {
if item.Set == "" {
continue
}
counts[item.Set]++
}
var completed []string
for setName, count := range counts {
if isFourPieceSet(setName) && count >= 4 {
completed = append(completed, setName)
continue
}
if isTwoPieceSet(setName) && count >= 2 {
stacks := count / 2
for i := 0; i < stacks; i++ {
completed = append(completed, setName)
}
}
}
return completed
}
func isFourPieceSet(setName string) bool {
return fourPieceSets[setName]
}
func isTwoPieceSet(setName string) bool {
return twoPieceSets[setName]
}
func joinSets(sets []string) string {
if len(sets) == 0 {
return ""
}
out := sets[0]
for i := 1; i < len(sets); i++ {
out += "/" + sets[i]
}
return out
}
func itemKey(item model.OptimizeItem) string {
if item.ID == nil {
return fmt.Sprintf("%s-%s", item.Gear, item.Set)
}
return fmt.Sprint(item.ID)
}
func buildKey(items []model.OptimizeItem) string {
key := ""
for i, item := range items {
if i > 0 {
key += "-"
}
key += itemKey(item)
}
return key
}