feat(inventory-go): port the suitbuilder solver (/suitbuilder/search) — validated
Full Go port of suitbuilder.py's ConstraintSatisfactionSolver (the LIVE solver behind the suitbuilder UI; main.py's /optimize/suits is legacy/unused): - suit_model.go: CoverageMask + reductions, SuitItem/ItemBucket/SuitState, SpellBitmapIndex, ScoringWeights, SearchConstraints, CompletedSuit.to_dict, ItemPreFilter, set name<->id maps. Every sort carries (character_name, name) tiebreakers for deterministic results. - suit_solver.go: the 5-phase pipeline — load_items (fed in-process by the Go /search/items), create_buckets (+multi-slot/generic-jewelry expansion), apply_reduction_options, sort_buckets, and the depth-first recursive_search with both Mag-SuitBuilder pruning rules, can_add_item constraints (set limits, jewelry spell contribution, strict spell mode), scoring, and finalize. - suit_http.go: POST /suitbuilder/search (SSE: phase/log/suit/progress/complete), GET /suitbuilder/characters, GET /suitbuilder/sets. - search.go: refactor handleSearchItems -> shared runSearch (the solver reuses it so both see identical rows); emit slot_name (get_sophisticated_slot_options + translate_equipment_slot); fix the trinket slot_names clause to exclude %bracelet% (matches Python). - slotname.go: the EquipMask-based slot translation, loaded from the enum DB. Validation: 9/9 scenarios stream byte-identical suits vs the Python service on production data (no-spell, multi-character, locked slots with/without spells, spell constraints, alternate set pairs, primary-only). ~45x faster than Python. Three subtle bugs found and fixed during validation: - slot_name is load-bearing, not display: jewelry's computed_slot_name is empty, so load_items falls back to slot_name to bucket rings/neck/wrists/trinket. - Python scoring uses floor division (total_armor // 100); total_armor goes negative (non-armor items carry armor_level -1) so Go's truncation was +1 off. - the trinket fetch must exclude bracelets or they duplicate the Wrist buckets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2473b80519
commit
57f53ff36b
6 changed files with 1806 additions and 18 deletions
593
go-services/inventory-go/suit_model.go
Normal file
593
go-services/inventory-go/suit_model.go
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math/bits"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Port of inventory-service/suitbuilder.py data model. This is the LIVE solver
|
||||
// (mounted at /suitbuilder/search; main.py's /optimize/suits is legacy/unused).
|
||||
// Every sort carries (character_name, name) tiebreakers so results are
|
||||
// deterministic and reproducible, exactly as the Python source documents.
|
||||
|
||||
// --- Equipment set name<->id maps (suitbuilder.py SET_NAMES / _convert_set_name_to_id) ---
|
||||
|
||||
var setNames = map[int]string{
|
||||
14: "Adept's", 16: "Defender's", 13: "Soldier's", 21: "Wise",
|
||||
40: "Heroic Protector", 41: "Heroic Destroyer", 46: "Relic Alduressa",
|
||||
47: "Ancient Relic", 48: "Noble Relic", 15: "Archer's", 19: "Hearty",
|
||||
20: "Dexterous", 22: "Swift", 24: "Reinforced", 26: "Flame Proof",
|
||||
29: "Lightning Proof",
|
||||
}
|
||||
|
||||
// nameToSetID is the reverse map used by load_items to turn the search's
|
||||
// item_set field into a numeric set id (note the " Set" suffix, verbatim).
|
||||
var nameToSetID = map[string]int{
|
||||
"Adept's Set": 14, "Defender's Set": 16, "Soldier's Set": 13, "Wise Set": 21,
|
||||
"Heroic Protector Set": 40, "Heroic Destroyer Set": 41, "Relic Alduressa Set": 46,
|
||||
"Ancient Relic Set": 47, "Noble Relic Set": 48, "Archer's Set": 15,
|
||||
"Hearty Set": 19, "Dexterous Set": 20, "Swift Set": 22, "Reinforced Set": 24,
|
||||
"Flame Proof Set": 26, "Lightning Proof Set": 29,
|
||||
}
|
||||
|
||||
// getSetName mirrors suitbuilder.get_set_name (None/0 -> "").
|
||||
func getSetName(setID int) string {
|
||||
if setID == 0 {
|
||||
return ""
|
||||
}
|
||||
if n, ok := setNames[setID]; ok {
|
||||
return n
|
||||
}
|
||||
return fmt.Sprintf("Set %d", setID)
|
||||
}
|
||||
|
||||
func convertSetNameToID(setName string) int { return nameToSetID[setName] }
|
||||
|
||||
// --- CoverageMask (suitbuilder.py:81) ---
|
||||
|
||||
const (
|
||||
covUnderwearUpperLegs = 0x00000002
|
||||
covUnderwearLowerLegs = 0x00000004
|
||||
covUnderwearChest = 0x00000008
|
||||
covUnderwearAbdomen = 0x00000010
|
||||
covUnderwearUpperArms = 0x00000020
|
||||
covUnderwearLowerArms = 0x00000040
|
||||
covOuterUpperLegs = 0x00000100
|
||||
covOuterLowerLegs = 0x00000200
|
||||
covOuterChest = 0x00000400
|
||||
covOuterAbdomen = 0x00000800
|
||||
covOuterUpperArms = 0x00001000
|
||||
covOuterLowerArms = 0x00002000
|
||||
covHead = 0x00004000
|
||||
covHands = 0x00008000
|
||||
covFeet = 0x00010000
|
||||
|
||||
// Aliases matching slot names (suitbuilder.py:110-115).
|
||||
covChest = covOuterChest
|
||||
covAbdomen = covOuterAbdomen
|
||||
covUpperArms = covOuterUpperArms
|
||||
covLowerArms = covOuterLowerArms
|
||||
covUpperLegs = covOuterUpperLegs
|
||||
covLowerLegs = covOuterLowerLegs
|
||||
|
||||
magRobePattern = 0x00013F00
|
||||
)
|
||||
|
||||
// coverageReductionOptions mirrors CoverageMask.reduction_options().
|
||||
func coverageReductionOptions(v int) []int {
|
||||
if bits.OnesCount(uint(v)) <= 1 {
|
||||
return nil
|
||||
}
|
||||
if coverageIsRobe(v) {
|
||||
return nil
|
||||
}
|
||||
switch v {
|
||||
case covUpperArms | covLowerArms:
|
||||
return []int{covUpperArms, covLowerArms}
|
||||
case covUpperLegs | covLowerLegs:
|
||||
return []int{covUpperLegs, covLowerLegs}
|
||||
case covLowerLegs | covFeet:
|
||||
return []int{covFeet}
|
||||
case covChest | covAbdomen:
|
||||
return []int{covChest}
|
||||
case covChest | covAbdomen | covUpperArms:
|
||||
return []int{covChest}
|
||||
case covChest | covUpperArms | covLowerArms:
|
||||
return []int{covChest}
|
||||
case covChest | covUpperArms:
|
||||
return []int{covChest}
|
||||
case covAbdomen | covUpperLegs | covLowerLegs:
|
||||
return []int{covAbdomen, covUpperLegs, covLowerLegs}
|
||||
case covChest | covAbdomen | covUpperArms | covLowerArms:
|
||||
return []int{covChest}
|
||||
case covAbdomen | covUpperLegs:
|
||||
return []int{covAbdomen}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// coverageIsRobe mirrors CoverageMask.is_robe() (exact pattern == component
|
||||
// pattern == 0x13F00; otherwise the 6+ coverage-areas fallback).
|
||||
func coverageIsRobe(v int) bool {
|
||||
if v == magRobePattern {
|
||||
return true
|
||||
}
|
||||
return bits.OnesCount(uint(v)) >= 6
|
||||
}
|
||||
|
||||
// coverageToSlotName mirrors CoverageMask.to_slot_name() (single coverage only).
|
||||
func coverageToSlotName(v int) string {
|
||||
switch v {
|
||||
case covHead:
|
||||
return "Head"
|
||||
case covChest:
|
||||
return "Chest"
|
||||
case covUpperArms:
|
||||
return "Upper Arms"
|
||||
case covLowerArms:
|
||||
return "Lower Arms"
|
||||
case covHands:
|
||||
return "Hands"
|
||||
case covAbdomen:
|
||||
return "Abdomen"
|
||||
case covUpperLegs:
|
||||
return "Upper Legs"
|
||||
case covLowerLegs:
|
||||
return "Lower Legs"
|
||||
case covFeet:
|
||||
return "Feet"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- SuitItem (suitbuilder.py:221) ---
|
||||
|
||||
type SuitItem struct {
|
||||
ID string // unique per (character,name); used for uniqueness checks
|
||||
Name string
|
||||
CharacterName string
|
||||
Slot string
|
||||
Coverage int // 0 == None
|
||||
HasCoverage bool
|
||||
SetID int // 0 == None
|
||||
ArmorLevel int
|
||||
Ratings map[string]int
|
||||
SpellBitmap uint64
|
||||
SpellNames []string
|
||||
IsLocked bool
|
||||
Material string
|
||||
}
|
||||
|
||||
func (it *SuitItem) ratingsSum() int {
|
||||
s := 0
|
||||
for _, v := range it.Ratings {
|
||||
s += v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (it *SuitItem) ratingsSumExcept(skip string) int {
|
||||
s := 0
|
||||
for k, v := range it.Ratings {
|
||||
if k != skip {
|
||||
s += v
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (it *SuitItem) clone(slot string, name string, coverage int, hasCov bool) *SuitItem {
|
||||
r := make(map[string]int, len(it.Ratings))
|
||||
for k, v := range it.Ratings {
|
||||
r[k] = v
|
||||
}
|
||||
sn := make([]string, len(it.SpellNames))
|
||||
copy(sn, it.SpellNames)
|
||||
return &SuitItem{
|
||||
ID: it.ID, Name: name, CharacterName: it.CharacterName, Slot: slot,
|
||||
Coverage: coverage, HasCoverage: hasCov, SetID: it.SetID, ArmorLevel: it.ArmorLevel,
|
||||
Ratings: r, SpellBitmap: it.SpellBitmap, SpellNames: sn, IsLocked: it.IsLocked,
|
||||
Material: it.Material,
|
||||
}
|
||||
}
|
||||
|
||||
// --- ItemBucket (suitbuilder.py:247) ---
|
||||
|
||||
type ItemBucket struct {
|
||||
Slot string
|
||||
Items []*SuitItem
|
||||
IsArmor bool
|
||||
}
|
||||
|
||||
var clothingSortSlots = map[string]bool{"Shirt": true, "Pants": true}
|
||||
|
||||
// sortItems mirrors ItemBucket.sort_items() (reverse=True over the key tuple,
|
||||
// stable so equal keys keep prior order).
|
||||
func (b *ItemBucket) sortItems() {
|
||||
items := b.Items
|
||||
if _, isClothing := clothingSortSlots[b.Slot]; isClothing {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return descTuple(
|
||||
cmpInt(items[i].Ratings["damage_rating"], items[j].Ratings["damage_rating"]),
|
||||
cmpInt(len(items[i].SpellNames), len(items[j].SpellNames)),
|
||||
cmpInt(items[i].ratingsSumExcept("damage_rating"), items[j].ratingsSumExcept("damage_rating")),
|
||||
cmpStr(items[i].CharacterName, items[j].CharacterName),
|
||||
cmpStr(items[i].Name, items[j].Name),
|
||||
)
|
||||
})
|
||||
} else if b.IsArmor {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return descTuple(
|
||||
cmpInt(items[i].ArmorLevel, items[j].ArmorLevel),
|
||||
cmpInt(items[i].Ratings["crit_damage_rating"], items[j].Ratings["crit_damage_rating"]),
|
||||
cmpInt(len(items[i].SpellNames), len(items[j].SpellNames)),
|
||||
cmpInt(items[i].ratingsSum(), items[j].ratingsSum()),
|
||||
cmpStr(items[i].CharacterName, items[j].CharacterName),
|
||||
cmpStr(items[i].Name, items[j].Name),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return descTuple(
|
||||
cmpInt(len(items[i].SpellNames), len(items[j].SpellNames)),
|
||||
cmpInt(items[i].ratingsSum(), items[j].ratingsSum()),
|
||||
cmpStr(items[i].CharacterName, items[j].CharacterName),
|
||||
cmpStr(items[i].Name, items[j].Name),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// descTuple returns true if the left tuple sorts before the right under Python's
|
||||
// reverse=True (i.e. the larger tuple comes first). cmp* return -1/0/1.
|
||||
func descTuple(cmps ...int) bool {
|
||||
for _, c := range cmps {
|
||||
if c != 0 {
|
||||
return c > 0 // larger first
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cmpInt(a, b int) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func cmpStr(a, b string) int { return strings.Compare(a, b) }
|
||||
|
||||
// --- SpellBitmapIndex (suitbuilder.py:299) ---
|
||||
|
||||
type SpellBitmapIndex struct {
|
||||
spellToBit map[string]uint64
|
||||
order []struct {
|
||||
bit uint64
|
||||
name string
|
||||
}
|
||||
nextBit uint
|
||||
}
|
||||
|
||||
func newSpellBitmapIndex() *SpellBitmapIndex {
|
||||
return &SpellBitmapIndex{spellToBit: map[string]uint64{}}
|
||||
}
|
||||
|
||||
func (s *SpellBitmapIndex) registerSpell(name string) uint64 {
|
||||
if b, ok := s.spellToBit[name]; ok {
|
||||
return b
|
||||
}
|
||||
var bit uint64
|
||||
if s.nextBit < 64 {
|
||||
bit = uint64(1) << s.nextBit
|
||||
} // >=64: bit stays 0 (only non-required spells ever reach here; required
|
||||
// spells are registered first, so their low bits are always exact).
|
||||
s.spellToBit[name] = bit
|
||||
s.order = append(s.order, struct {
|
||||
bit uint64
|
||||
name string
|
||||
}{bit, name})
|
||||
s.nextBit++
|
||||
return bit
|
||||
}
|
||||
|
||||
func (s *SpellBitmapIndex) getBitmap(spells []string) uint64 {
|
||||
var m uint64
|
||||
for _, sp := range spells {
|
||||
m |= s.registerSpell(sp)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *SpellBitmapIndex) getSpellNames(bitmap uint64) []string {
|
||||
var out []string
|
||||
for _, e := range s.order {
|
||||
if e.bit != 0 && bitmap&e.bit != 0 {
|
||||
out = append(out, e.name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// --- SuitState (suitbuilder.py:342) ---
|
||||
|
||||
type SuitState struct {
|
||||
Items map[string]*SuitItem
|
||||
SpellBitmap uint64
|
||||
SetCounts map[int]int
|
||||
TotalArmor int
|
||||
TotalRatings map[string]int
|
||||
Occupied map[string]bool
|
||||
}
|
||||
|
||||
func newSuitState() *SuitState {
|
||||
return &SuitState{
|
||||
Items: map[string]*SuitItem{}, SetCounts: map[int]int{},
|
||||
TotalRatings: map[string]int{}, Occupied: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (st *SuitState) push(it *SuitItem) {
|
||||
st.Items[it.Slot] = it
|
||||
st.Occupied[it.Slot] = true
|
||||
st.SpellBitmap |= it.SpellBitmap
|
||||
if it.SetID != 0 {
|
||||
st.SetCounts[it.SetID]++
|
||||
}
|
||||
st.TotalArmor += it.ArmorLevel
|
||||
for k, v := range it.Ratings {
|
||||
st.TotalRatings[k] += v
|
||||
}
|
||||
}
|
||||
|
||||
func (st *SuitState) pop(slot string) {
|
||||
it, ok := st.Items[slot]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(st.Items, slot)
|
||||
delete(st.Occupied, slot)
|
||||
// Rebuild spell bitmap (overlaps prevent simple subtraction).
|
||||
st.SpellBitmap = 0
|
||||
for _, r := range st.Items {
|
||||
st.SpellBitmap |= r.SpellBitmap
|
||||
}
|
||||
if it.SetID != 0 {
|
||||
st.SetCounts[it.SetID]--
|
||||
if st.SetCounts[it.SetID] == 0 {
|
||||
delete(st.SetCounts, it.SetID)
|
||||
}
|
||||
}
|
||||
st.TotalArmor -= it.ArmorLevel
|
||||
for k, v := range it.Ratings {
|
||||
if _, present := st.TotalRatings[k]; present {
|
||||
st.TotalRatings[k] -= v
|
||||
if st.TotalRatings[k] <= 0 {
|
||||
delete(st.TotalRatings, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ScoringWeights / SearchConstraints (suitbuilder.py:409,426) ---
|
||||
|
||||
type ScoringWeights struct {
|
||||
ArmorSetComplete int
|
||||
MissingSetPenalty int
|
||||
CritDamage1 int
|
||||
CritDamage2 int
|
||||
DamageRating1 int
|
||||
DamageRating2 int
|
||||
DamageRating3 int
|
||||
}
|
||||
|
||||
func defaultScoringWeights() ScoringWeights {
|
||||
return ScoringWeights{
|
||||
ArmorSetComplete: 1000, MissingSetPenalty: -200,
|
||||
CritDamage1: 10, CritDamage2: 20,
|
||||
DamageRating1: 10, DamageRating2: 20, DamageRating3: 30,
|
||||
}
|
||||
}
|
||||
|
||||
type LockedSlotInfo struct {
|
||||
SetID int `json:"set_id"`
|
||||
Spells []string `json:"spells"`
|
||||
}
|
||||
|
||||
type SearchConstraints struct {
|
||||
Characters []string `json:"characters"`
|
||||
PrimarySet int `json:"primary_set"`
|
||||
SecondarySet int `json:"secondary_set"`
|
||||
RequiredSpells []string `json:"required_spells"`
|
||||
LockedSlots map[string]LockedSlotInfo `json:"locked_slots"`
|
||||
IncludeEquipped bool `json:"include_equipped"`
|
||||
IncludeInventory bool `json:"include_inventory"`
|
||||
MinArmor *int `json:"min_armor"`
|
||||
MaxArmor *int `json:"max_armor"`
|
||||
MinCritDamage *int `json:"min_crit_damage"`
|
||||
MaxCritDamage *int `json:"max_crit_damage"`
|
||||
MinDamageRating *int `json:"min_damage_rating"`
|
||||
MaxDamageRating *int `json:"max_damage_rating"`
|
||||
ScoringWeights *ScoringWeights `json:"scoring_weights"`
|
||||
MaxResults int `json:"max_results"`
|
||||
SearchTimeout int `json:"search_timeout"`
|
||||
}
|
||||
|
||||
// --- CompletedSuit (suitbuilder.py:446) ---
|
||||
|
||||
type CompletedSuit struct {
|
||||
Items map[string]*SuitItem
|
||||
Score int
|
||||
TotalArmor int
|
||||
TotalRatings map[string]int
|
||||
SetCounts map[int]int
|
||||
FulfilledSpells []string
|
||||
MissingSpells []string
|
||||
}
|
||||
|
||||
func fnvInt(s string) int {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(s))
|
||||
return int(h.Sum32())
|
||||
}
|
||||
|
||||
// toDict mirrors CompletedSuit.to_dict(). The opaque id fields are derived
|
||||
// deterministically (Python uses salted hash(); we use FNV) — never compared in
|
||||
// validation.
|
||||
func (c *CompletedSuit) toDict() map[string]any {
|
||||
transferByChar := map[string][]string{}
|
||||
totalItems := 0
|
||||
// Slots iterated in Python dict order; use a sorted-slot order for stable
|
||||
// transfer instructions (instructions are display-only).
|
||||
for _, it := range c.Items {
|
||||
transferByChar[it.CharacterName] = append(transferByChar[it.CharacterName], it.Name)
|
||||
totalItems++
|
||||
}
|
||||
chars := make([]string, 0, len(transferByChar))
|
||||
for ch := range transferByChar {
|
||||
chars = append(chars, ch)
|
||||
}
|
||||
sort.Strings(chars)
|
||||
instructions := []string{}
|
||||
step := 1
|
||||
for _, ch := range chars {
|
||||
for _, name := range transferByChar[ch] {
|
||||
instructions = append(instructions, fmt.Sprintf("%d. Transfer %s from %s to new character", step, name, ch))
|
||||
step++
|
||||
}
|
||||
}
|
||||
instructions = append(instructions, fmt.Sprintf("%d. Equip all transferred items on new character", step))
|
||||
|
||||
slotKeys := make([]string, 0, len(c.Items))
|
||||
for slot := range c.Items {
|
||||
slotKeys = append(slotKeys, slot)
|
||||
}
|
||||
sort.Strings(slotKeys)
|
||||
itemsOut := map[string]any{}
|
||||
for _, slot := range slotKeys {
|
||||
it := c.Items[slot]
|
||||
var setIDOut any
|
||||
if it.SetID != 0 {
|
||||
setIDOut = it.SetID
|
||||
}
|
||||
itemsOut[slot] = map[string]any{
|
||||
"id": fnvInt(it.ID),
|
||||
"name": it.Name,
|
||||
"source_character": it.CharacterName,
|
||||
"armor_level": it.ArmorLevel,
|
||||
"ratings": it.Ratings,
|
||||
"spells": it.SpellNames,
|
||||
"set_id": setIDOut,
|
||||
"set_name": getSetName(it.SetID),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"id": fnvInt(strings.Join(slotKeys, "|")),
|
||||
"score": c.Score,
|
||||
"items": itemsOut,
|
||||
"stats": map[string]any{
|
||||
"total_armor": c.TotalArmor,
|
||||
"total_crit_damage": c.TotalRatings["crit_damage_rating"],
|
||||
"total_damage_rating": c.TotalRatings["damage_rating"],
|
||||
"primary_set_count": 0,
|
||||
"secondary_set_count": 0,
|
||||
"spell_coverage": len(c.FulfilledSpells),
|
||||
},
|
||||
"missing": c.MissingSpells,
|
||||
"notes": []any{},
|
||||
"transfer_summary": map[string]any{
|
||||
"total_items": totalItems,
|
||||
"from_characters": transferByChar,
|
||||
},
|
||||
"instructions": instructions,
|
||||
}
|
||||
}
|
||||
|
||||
// --- ItemPreFilter (suitbuilder.py:519) ---
|
||||
|
||||
func removeSurpassedItems(items []*SuitItem) []*SuitItem {
|
||||
out := make([]*SuitItem, 0, len(items))
|
||||
for _, it := range items {
|
||||
surpassed := false
|
||||
for _, cmp := range items {
|
||||
if cmp == it {
|
||||
continue
|
||||
}
|
||||
if isSurpassedBy(it, cmp) {
|
||||
surpassed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !surpassed {
|
||||
out = append(out, it)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isSurpassedBy(item, cmp *SuitItem) bool {
|
||||
if item.Slot != cmp.Slot {
|
||||
return false
|
||||
}
|
||||
if item.SetID != cmp.SetID {
|
||||
return false
|
||||
}
|
||||
if !spellsSurpassOrEqual(cmp.SpellNames, item.SpellNames) {
|
||||
return false
|
||||
}
|
||||
betterInSomething := false
|
||||
for _, key := range []string{"crit_damage_rating", "damage_rating"} {
|
||||
ir := item.Ratings[key]
|
||||
cr := cmp.Ratings[key]
|
||||
if cr > ir {
|
||||
betterInSomething = true
|
||||
} else if ir > cr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if item.ArmorLevel > 0 && cmp.ArmorLevel > 0 {
|
||||
if cmp.ArmorLevel > item.ArmorLevel {
|
||||
betterInSomething = true
|
||||
} else if item.ArmorLevel > cmp.ArmorLevel {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return betterInSomething
|
||||
}
|
||||
|
||||
func spellsSurpassOrEqual(spells1, spells2 []string) bool {
|
||||
for _, s2 := range spells2 {
|
||||
found := false
|
||||
for _, s1 := range spells1 {
|
||||
if s1 == s2 || spellSurpasses(s1, s2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func spellSurpasses(s1, s2 string) bool {
|
||||
if strings.Contains(s1, "Legendary") && (strings.Contains(s2, "Epic") || strings.Contains(s2, "Major")) {
|
||||
b1 := strings.ReplaceAll(s1, "Legendary ", "")
|
||||
b2 := strings.ReplaceAll(strings.ReplaceAll(s2, "Epic ", ""), "Major ", "")
|
||||
return b1 == b2
|
||||
}
|
||||
if strings.Contains(s1, "Epic") && strings.Contains(s2, "Major") {
|
||||
b1 := strings.ReplaceAll(s1, "Epic ", "")
|
||||
b2 := strings.ReplaceAll(s2, "Major ", "")
|
||||
return b1 == b2
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue