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 }