From 8a1d9b558b458ecfbf74234f9ad24496627181c5 Mon Sep 17 00:00:00 2001 From: CarlPoppa Date: Tue, 4 Mar 2025 15:27:39 +0000 Subject: [PATCH 1/5] Leveling runeword maker initial commit --- internal/action/runeword_recipes.go | 385 ++++++++++++++++++ internal/config/config.go | 6 +- internal/config/rw_recipes.go | 18 + internal/server/http_server.go | 19 +- internal/server/template_parameters.go | 17 +- .../templates/run_settings_components.gohtml | 9 + 6 files changed, 437 insertions(+), 17 deletions(-) create mode 100644 internal/action/runeword_recipes.go create mode 100644 internal/config/rw_recipes.go diff --git a/internal/action/runeword_recipes.go b/internal/action/runeword_recipes.go new file mode 100644 index 000000000..a6c42806d --- /dev/null +++ b/internal/action/runeword_recipes.go @@ -0,0 +1,385 @@ +package action + +import ( + "fmt" + "slices" + + "github.com/hectorgimenez/d2go/pkg/data" + "github.com/hectorgimenez/d2go/pkg/data/item" + "github.com/hectorgimenez/d2go/pkg/data/stat" + "github.com/hectorgimenez/koolo/internal/action/step" + "github.com/hectorgimenez/koolo/internal/context" + "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/ui" + "github.com/hectorgimenez/koolo/internal/utils" +) + +type RunewordRecipe struct { + Name string + Inserts []string + BaseItemTypes []string + BaseSortOrder []stat.ID + AllowEth bool +} + +var ( + RunewordRecipes = []RunewordRecipe{ + // Add recipes in order of priority. If we have inserts for two recipes, it will process the first one in the list first. + // Add the inserts in order required for the runeword + { + Name: "Stealth", + Inserts: []string{"TalRune", "EthRune"}, + BaseItemTypes: []string{"Armor"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Lore", + Inserts: []string{"OrtRune", "SolRune"}, + BaseItemTypes: []string{"Helm"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Ancients Pledge", + Inserts: []string{"RalRune", "OrtRune", "TalRune"}, + BaseItemTypes: []string{"Shield"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Rhyme", + Inserts: []string{"ShaelRune", "EthRune"}, + BaseItemTypes: []string{"Shield"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Smoke", + Inserts: []string{"NefRune", "LumRune"}, + BaseItemTypes: []string{"Armor"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Spirit", + Inserts: []string{"TalRune", "ThulRune", "OrtRune", "AmnRune"}, + BaseItemTypes: []string{"Sword", "Shield"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Insight", + Inserts: []string{"RalRune", "TirRune", "TalRune", "SolRune"}, + BaseItemTypes: []string{"Polearm"}, + BaseSortOrder: []stat.ID{stat.TwoHandedMaxDamage}, + AllowEth: true, + }, + { + Name: "Leaf", + Inserts: []string{"TirRune", "RalRune"}, + BaseItemTypes: []string{"Staff"}, + BaseSortOrder: []stat.ID{}, + AllowEth: true, + }, + { + Name: "Flickering Flame", + Inserts: []string{"NefRune", "PulRune", "VexRune"}, + BaseItemTypes: []string{"Helm"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, + { + Name: "Call to Arms", + Inserts: []string{"AmnRune", "RalRune", "MalRune", "IstRune", "OhmRune"}, + BaseItemTypes: []string{"Mace"}, + BaseSortOrder: []stat.ID{}, + AllowEth: true, + }, + { + Name: "Heart of the Oak", + Inserts: []string{"KoRune", "VexRune", "PulRune", "ThulRune"}, + BaseItemTypes: []string{"Mace"}, + BaseSortOrder: []stat.ID{}, + AllowEth: true, + }, + { + Name: "Fortitude", + Inserts: []string{"ElRune", "SolRune", "DolRune", "LoRune"}, + BaseItemTypes: []string{"Armor"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: true, + }, + { + Name: "Chains of Honor", + Inserts: []string{"DolRune", "UmRune", "BerRune", "IstRune"}, + BaseItemTypes: []string{"Armor"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: true, + }, + } +) + +func MakeRunewords() error { + ctx := context.Get() + ctx.SetLastAction("SocketAddItems") + + insertItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) + baseItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) + + for _, recipe := range RunewordRecipes { + if !slices.Contains(ctx.CharacterCfg.Game.Leveling.EnabledRunewordRecipes, recipe.Name) { + continue + } + + ctx.Logger.Debug("Socket recipe is enabled, processing", "recipe", recipe.Name) + + continueProcessing := true + for continueProcessing { + + if baseItem, hasBase := hasBaseForRunewordRecipe(baseItems, recipe); hasBase { + + existingTier, hasExisting := currentRunewordBaseTier(ctx, recipe, baseItem.Type().Name) + // Prevent creating runeword multiple times if we don't care about damage / def + if hasExisting && (len(recipe.BaseSortOrder) == 0 || baseItem.Desc().Tier() <= existingTier) { + ctx.Logger.Debug("Skipping recipe - existing runeword has equal or better tier in same base type", + "recipe", recipe.Name, + "baseType", baseItem.Type().Name, + "existingTier", existingTier, + "newBaseTier", baseItem.Desc().Tier()) + continueProcessing = false + continue + } + if inserts, hasInserts := hasItemsForRunewordRecipe(insertItems, recipe); hasInserts { + err := SocketItems(ctx, recipe, baseItem, inserts...) + if err != nil { + return err + } + + insertItems = removeUsedItems(insertItems, inserts) + } else { + continueProcessing = false + } + baseItems = removeUsedItems(baseItems, []data.Item{baseItem}) + } else { + continueProcessing = false + } + } + } + return nil +} +func SocketItems(ctx *context.Status, recipe RunewordRecipe, base data.Item, items ...data.Item) error { + + ctx.SetLastAction("SocketItem") + + ins := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) + + for _, itm := range items { + if itm.Location.LocationType == item.LocationStash || itm.Location.LocationType == item.LocationSharedStash { + OpenStash() + break + } + } + if !ctx.Data.OpenMenus.Stash && (base.Location.LocationType == item.LocationStash || base.Location.LocationType == item.LocationSharedStash) { + err := OpenStash() + if err != nil { + return err + } + } + + if base.Location.LocationType == item.LocationSharedStash { + ctx.Logger.Debug("Base in shared - checking it fits") + if !itemFitsInventory(base) { + ctx.Logger.Error("Base item does not fit in inventory", "item", base.Name) + return step.CloseAllMenus() + } else { + ctx.Logger.Debug("Base in shared stash but fits in inv, switching to correct tab") + SwitchStashTab(base.Location.Page + 1) + ctx.Logger.Debug("Switched to correct tab") + utils.Sleep(500) + screenPos := ui.GetScreenCoordsForItem(base) + ctx.Logger.Debug(fmt.Sprintf("Clicking after 5s at %d:%d", screenPos.X, screenPos.Y)) + ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) + } + } + + requiredCounts := make(map[string]int) + for _, insert := range recipe.Inserts { + requiredCounts[insert]++ + } + + usedItems := make(map[*data.Item]bool) + orderedItems := make([]data.Item, 0) + + // Process each required insert in order + for _, requiredInsert := range recipe.Inserts { + for i := range ins { + item := &ins[i] + if string(item.Name) == requiredInsert && !usedItems[item] { + orderedItems = append(orderedItems, *item) + usedItems[item] = true + break + } + } + } + previousPage := -1 // Initialize to invalid page number + for _, itm := range orderedItems { + if itm.Location.LocationType == item.LocationSharedStash || itm.Location.LocationType == item.LocationStash { + currentPage := itm.Location.Page + 1 + if previousPage != currentPage || currentPage != base.Location.Page { + SwitchStashTab(currentPage) + } + previousPage = currentPage + } + + screenPos := ui.GetScreenCoordsForItem(itm) + ctx.HID.Click(game.LeftButton, screenPos.X, screenPos.Y) + utils.Sleep(300) + + for _, movedBase := range ctx.Data.Inventory.AllItems { + if base.UnitID == movedBase.UnitID { + if (base.Location.LocationType == item.LocationStash) && base.Location.Page != itm.Location.Page { + SwitchStashTab(base.Location.Page + 1) + } + + basescreenPos := ui.GetScreenCoordsForItem(movedBase) + ctx.HID.Click(game.LeftButton, basescreenPos.X, basescreenPos.Y) + utils.Sleep(300) + if itm.Location.LocationType == item.LocationCursor { + DropMouseItem() + return fmt.Errorf("failed to insert item %s into base %s", itm.Name, base.Name) + } + } + } + utils.Sleep(300) + } + return step.CloseAllMenus() +} + +func currentRunewordBaseTier(ctx *context.Status, recipe RunewordRecipe, baseType string) (item.Tier, bool) { + + items := ctx.Data.Inventory.ByLocation( + item.LocationInventory, + item.LocationEquipped, + item.LocationStash, + item.LocationSharedStash, + ) + + for _, itm := range items { + if string(itm.RunewordName) == recipe.Name && itm.Type().Name == baseType { + return itm.Desc().Tier(), true + } + } + return 0, false +} + +func hasBaseForRunewordRecipe(items []data.Item, rwrecipe RunewordRecipe) (data.Item, bool) { + var validBases []data.Item + for _, itm := range items { + itemType := itm.Type().Name + + isValidType := false + for _, baseType := range rwrecipe.BaseItemTypes { + if itemType == baseType { + isValidType = true + break + } + } + if !isValidType { + continue + } + + sockets, found := itm.FindStat(stat.NumSockets, 0) + if !found || sockets.Value != len(rwrecipe.Inserts) { + continue + } + + if itm.Ethereal && !rwrecipe.AllowEth { + continue + } + + if itm.HasSocketedItems() { + continue + } + + if itm.Quality > item.QualitySuperior { + continue + } + + validBases = append(validBases, itm) + } + + if len(validBases) == 0 { + return data.Item{}, false + } + + // Sort bases by BaseSortOrder if provided + if len(rwrecipe.BaseSortOrder) > 0 { + slices.SortFunc(validBases, func(a, b data.Item) int { + for _, statID := range rwrecipe.BaseSortOrder { + statA, foundA := a.FindStat(statID, 0) + statB, foundB := b.FindStat(statID, 0) + + if !foundA && !foundB { + continue + } + if !foundA { + return -1 + } + if !foundB { + return 1 + } + + if statA.Value != statB.Value { + return statB.Value - statA.Value // Higher values first + } + } + return 0 + }) + } else { + // When no BaseSortOrder specified, sort by lowest combined requirements + slices.SortFunc(validBases, func(a, b data.Item) int { + aStr := a.Desc().RequiredStrength + aDex := a.Desc().RequiredDexterity + bStr := b.Desc().RequiredStrength + bDex := b.Desc().RequiredDexterity + + aTotal := aStr + aDex + bTotal := bStr + bDex + + return aTotal - bTotal // Lower requirements first + }) + } + + return validBases[0], true +} + +func hasItemsForRunewordRecipe(items []data.Item, rwrecipe RunewordRecipe) ([]data.Item, bool) { + + RunewordRecipeItems := make(map[string]int) + for _, item := range rwrecipe.Inserts { + RunewordRecipeItems[item]++ + } + + itemsForRecipe := []data.Item{} + + for _, item := range items { + if count, ok := RunewordRecipeItems[string(item.Name)]; ok { + + itemsForRecipe = append(itemsForRecipe, item) + + // Check if we now have exactly the needed count before decrementing + count -= 1 + if count == 0 { + delete(RunewordRecipeItems, string(item.Name)) + if len(RunewordRecipeItems) == 0 { + return itemsForRecipe, true + } + } else { + RunewordRecipeItems[string(item.Name)] = count + } + } + } + + return nil, false +} diff --git a/internal/config/config.go b/internal/config/config.go index 7afa7b5ab..0c9ce33a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -213,8 +213,10 @@ type CharacterCfg struct { OpenChests bool `yaml:"openChests"` } `yaml:"terror_zone"` Leveling struct { - EnsurePointsAllocation bool `yaml:"ensurePointsAllocation"` - EnsureKeyBinding bool `yaml:"ensureKeyBinding"` + EnsurePointsAllocation bool `yaml:"ensurePointsAllocation"` + EnsureKeyBinding bool `yaml:"ensureKeyBinding"` + EnableRunewordMaker bool `yaml:"enableRunewordMaker"` + EnabledRunewordRecipes []string `yaml:"enabledRunewordRecipes"` } `yaml:"leveling"` Quests struct { ClearDen bool `yaml:"clearDen"` diff --git a/internal/config/rw_recipes.go b/internal/config/rw_recipes.go new file mode 100644 index 000000000..38c09f9bd --- /dev/null +++ b/internal/config/rw_recipes.go @@ -0,0 +1,18 @@ +package config + +var AvailableRunewordRecipes = []string{ + // List out all the recipe names from runeword_recipes.go + "Stealth", + "Lore", + "Ancients Pledge", + "Rhyme", + "Smoke", + "Spirit", + "Insight", + "Leaf", + "Flickering Flame", + "Call to Arms", + "Heart of the Oak", + "Fortitude", + "Chains of Honor", +} diff --git a/internal/server/http_server.go b/internal/server/http_server.go index 7ec51428e..9cf6d0c94 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -925,6 +925,10 @@ func (s *HttpServer) characterSettings(w http.ResponseWriter, r *http.Request) { } cfg.Game.Leveling.EnsurePointsAllocation = r.Form.Has("gameLevelingEnsurePointsAllocation") cfg.Game.Leveling.EnsureKeyBinding = r.Form.Has("gameLevelingEnsureKeyBinding") + // Socket Recipes + cfg.Game.Leveling.EnableRunewordMaker = r.Form.Has("gameLevelingEnableRunewordMaker") + enabledRunewordRecipes := r.Form["gameLevelingEnabledRunewordRecipes"] + cfg.Game.Leveling.EnabledRunewordRecipes = enabledRunewordRecipes // Quests options for Act 1 cfg.Game.Quests.ClearDen = r.Form.Has("gameQuestsClearDen") @@ -1022,12 +1026,13 @@ func (s *HttpServer) characterSettings(w http.ResponseWriter, r *http.Request) { dayNames := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} s.templates.ExecuteTemplate(w, "character_settings.gohtml", CharacterSettings{ - Supervisor: supervisor, - Config: cfg, - DayNames: dayNames, - EnabledRuns: enabledRuns, - DisabledRuns: disabledRuns, - AvailableTZs: availableTZs, - RecipeList: config.AvailableRecipes, + Supervisor: supervisor, + Config: cfg, + DayNames: dayNames, + EnabledRuns: enabledRuns, + DisabledRuns: disabledRuns, + AvailableTZs: availableTZs, + RecipeList: config.AvailableRecipes, + RunewordRecipeList: config.AvailableRunewordRecipes, }) } diff --git a/internal/server/template_parameters.go b/internal/server/template_parameters.go index 9202b5e85..a136125d0 100644 --- a/internal/server/template_parameters.go +++ b/internal/server/template_parameters.go @@ -20,14 +20,15 @@ type DropData struct { } type CharacterSettings struct { - ErrorMessage string - Supervisor string - Config *config.CharacterCfg - DayNames []string - EnabledRuns []string - DisabledRuns []string - AvailableTZs map[int]string - RecipeList []string + ErrorMessage string + Supervisor string + Config *config.CharacterCfg + DayNames []string + EnabledRuns []string + DisabledRuns []string + AvailableTZs map[int]string + RecipeList []string + RunewordRecipeList []string } type ConfigData struct { diff --git a/internal/server/templates/run_settings_components.gohtml b/internal/server/templates/run_settings_components.gohtml index 639dce0c1..8c21b3b98 100644 --- a/internal/server/templates/run_settings_components.gohtml +++ b/internal/server/templates/run_settings_components.gohtml @@ -138,6 +138,15 @@
+ +
+ {{ range $index, $recipe := .RunewordRecipeList }} + + {{ end }} +
{{ end }} From 51ea36d37de0f4ea3b48a54083a40b88c80da7de Mon Sep 17 00:00:00 2001 From: CarlPoppa Date: Tue, 4 Mar 2025 15:28:20 +0000 Subject: [PATCH 2/5] Fix typo affecting stash opening logic --- internal/action/stash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/action/stash.go b/internal/action/stash.go index 4a1eff294..726b3b2e5 100644 --- a/internal/action/stash.go +++ b/internal/action/stash.go @@ -408,7 +408,7 @@ func TakeItemsFromStash(stashedItems []data.Item) error { ctx := context.Get() ctx.SetLastAction("TakeItemsFromStash") - if ctx.Data.OpenMenus.Stash { + if !ctx.Data.OpenMenus.Stash { err := OpenStash() if err != nil { return err From d39ff0661086c0f2a901d065fc06ae3eff1f958e Mon Sep 17 00:00:00 2001 From: CarlPoppa Date: Tue, 4 Mar 2025 15:30:08 +0000 Subject: [PATCH 3/5] Added runeword maker to town routine --- internal/action/town.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/action/town.go b/internal/action/town.go index c6f9cb2bb..ffb913ff9 100644 --- a/internal/action/town.go +++ b/internal/action/town.go @@ -46,6 +46,7 @@ func PreRun(firstRun bool) error { Stash(false) CubeRecipes() + MakeRunewords() // Leveling related checks if ctx.CharacterCfg.Game.Leveling.EnsurePointsAllocation { @@ -93,6 +94,7 @@ func InRunReturnTownRoutine() error { Gamble() Stash(false) CubeRecipes() + MakeRunewords() if ctx.CharacterCfg.Game.Leveling.EnsurePointsAllocation { EnsureStatPoints() From aaad959ffff44c428b94d2a5240310e4a8e11852 Mon Sep 17 00:00:00 2001 From: CarlPoppa Date: Wed, 5 Mar 2025 09:38:41 +0000 Subject: [PATCH 4/5] Added cure & re-ordered list based on req lvl --- internal/action/runeword_recipes.go | 85 ++++++++++++++++------------- internal/config/rw_recipes.go | 19 ++++--- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/internal/action/runeword_recipes.go b/internal/action/runeword_recipes.go index a6c42806d..00274d58f 100644 --- a/internal/action/runeword_recipes.go +++ b/internal/action/runeword_recipes.go @@ -27,30 +27,37 @@ var ( // Add recipes in order of priority. If we have inserts for two recipes, it will process the first one in the list first. // Add the inserts in order required for the runeword { - Name: "Stealth", - Inserts: []string{"TalRune", "EthRune"}, + Name: "Chains of Honor", + Inserts: []string{"DolRune", "UmRune", "BerRune", "IstRune"}, BaseItemTypes: []string{"Armor"}, BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, + AllowEth: true, }, { - Name: "Lore", - Inserts: []string{"OrtRune", "SolRune"}, - BaseItemTypes: []string{"Helm"}, + Name: "Fortitude", + Inserts: []string{"ElRune", "SolRune", "DolRune", "LoRune"}, + BaseItemTypes: []string{"Armor"}, BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, + AllowEth: true, }, { - Name: "Ancients Pledge", - Inserts: []string{"RalRune", "OrtRune", "TalRune"}, - BaseItemTypes: []string{"Shield"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, + Name: "Call to Arms", + Inserts: []string{"AmnRune", "RalRune", "MalRune", "IstRune", "OhmRune"}, + BaseItemTypes: []string{"Mace"}, + BaseSortOrder: []stat.ID{}, + AllowEth: true, }, { - Name: "Rhyme", - Inserts: []string{"ShaelRune", "EthRune"}, - BaseItemTypes: []string{"Shield"}, + Name: "Heart of the Oak", + Inserts: []string{"KoRune", "VexRune", "PulRune", "ThulRune"}, + BaseItemTypes: []string{"Mace"}, + BaseSortOrder: []stat.ID{}, + AllowEth: true, + }, + { + Name: "Flickering Flame", + Inserts: []string{"NefRune", "PulRune", "VexRune"}, + BaseItemTypes: []string{"Helm"}, BaseSortOrder: []stat.ID{stat.Defense}, AllowEth: false, }, @@ -61,6 +68,13 @@ var ( BaseSortOrder: []stat.ID{stat.Defense}, AllowEth: false, }, + { + Name: "Rhyme", + Inserts: []string{"ShaelRune", "EthRune"}, + BaseItemTypes: []string{"Shield"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, + }, { Name: "Spirit", Inserts: []string{"TalRune", "ThulRune", "OrtRune", "AmnRune"}, @@ -76,46 +90,39 @@ var ( AllowEth: true, }, { - Name: "Leaf", - Inserts: []string{"TirRune", "RalRune"}, - BaseItemTypes: []string{"Staff"}, - BaseSortOrder: []stat.ID{}, + Name: "Cure", + Inserts: []string{"ShaelRune", "IoRune", "TalRune"}, + BaseItemTypes: []string{"Helm"}, + BaseSortOrder: []stat.ID{stat.Defense}, AllowEth: true, }, { - Name: "Flickering Flame", - Inserts: []string{"NefRune", "PulRune", "VexRune"}, + Name: "Lore", + Inserts: []string{"OrtRune", "SolRune"}, BaseItemTypes: []string{"Helm"}, BaseSortOrder: []stat.ID{stat.Defense}, AllowEth: false, }, { - Name: "Call to Arms", - Inserts: []string{"AmnRune", "RalRune", "MalRune", "IstRune", "OhmRune"}, - BaseItemTypes: []string{"Mace"}, - BaseSortOrder: []stat.ID{}, - AllowEth: true, + Name: "Ancients Pledge", + Inserts: []string{"RalRune", "OrtRune", "TalRune"}, + BaseItemTypes: []string{"Shield"}, + BaseSortOrder: []stat.ID{stat.Defense}, + AllowEth: false, }, { - Name: "Heart of the Oak", - Inserts: []string{"KoRune", "VexRune", "PulRune", "ThulRune"}, - BaseItemTypes: []string{"Mace"}, + Name: "Leaf", + Inserts: []string{"TirRune", "RalRune"}, + BaseItemTypes: []string{"Staff"}, BaseSortOrder: []stat.ID{}, AllowEth: true, }, { - Name: "Fortitude", - Inserts: []string{"ElRune", "SolRune", "DolRune", "LoRune"}, - BaseItemTypes: []string{"Armor"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: true, - }, - { - Name: "Chains of Honor", - Inserts: []string{"DolRune", "UmRune", "BerRune", "IstRune"}, + Name: "Stealth", + Inserts: []string{"TalRune", "EthRune"}, BaseItemTypes: []string{"Armor"}, BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: true, + AllowEth: false, }, } ) diff --git a/internal/config/rw_recipes.go b/internal/config/rw_recipes.go index 38c09f9bd..f8c438dc0 100644 --- a/internal/config/rw_recipes.go +++ b/internal/config/rw_recipes.go @@ -2,17 +2,18 @@ package config var AvailableRunewordRecipes = []string{ // List out all the recipe names from runeword_recipes.go - "Stealth", - "Lore", - "Ancients Pledge", - "Rhyme", + "Chains of Honor", + "Fortitude", + "Call to Arms", + "Heart of the Oak", + "Flickering Flame", "Smoke", + "Rhyme", "Spirit", "Insight", + "Cure", + "Lore", + "Ancients Pledge", "Leaf", - "Flickering Flame", - "Call to Arms", - "Heart of the Oak", - "Fortitude", - "Chains of Honor", + "Stealth", } From d7c266ede3bcd9993ec62a0fbf1307a350c97ae9 Mon Sep 17 00:00:00 2001 From: CarlPoppa Date: Sat, 8 Mar 2025 21:55:19 +0000 Subject: [PATCH 5/5] Improved runeword recipes struct, add reroll stats Added every runeword recipe, improved base selection --- internal/action/runeword_maker.go | 297 +++++++ internal/action/runeword_recipes.go | 1250 +++++++++++++++++++-------- internal/config/rw_recipes.go | 30 +- 3 files changed, 1187 insertions(+), 390 deletions(-) create mode 100644 internal/action/runeword_maker.go diff --git a/internal/action/runeword_maker.go b/internal/action/runeword_maker.go new file mode 100644 index 000000000..a9fbaaa54 --- /dev/null +++ b/internal/action/runeword_maker.go @@ -0,0 +1,297 @@ +package action + +import ( + "fmt" + "slices" + + "github.com/hectorgimenez/d2go/pkg/data" + "github.com/hectorgimenez/d2go/pkg/data/item" + "github.com/hectorgimenez/d2go/pkg/data/stat" + "github.com/hectorgimenez/koolo/internal/action/step" + "github.com/hectorgimenez/koolo/internal/context" + "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/ui" + "github.com/hectorgimenez/koolo/internal/utils" +) + +func MakeRunewords() error { + ctx := context.Get() + ctx.SetLastAction("SocketAddItems") + + insertItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) + baseItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) + + for _, recipe := range Runewords { + if !slices.Contains(ctx.CharacterCfg.Game.Leveling.EnabledRunewordRecipes, string(recipe.Name)) { + continue + } + + ctx.Logger.Debug("Socket recipe is enabled, processing", "recipe", recipe.Name) + + continueProcessing := true + for continueProcessing { + + if baseItem, hasBase := hasBaseForRunewordRecipe(baseItems, recipe); hasBase { + existingTier, hasExisting := currentRunewordBaseTier(ctx, recipe, baseItem.Type().Name) + // Prevent creating runeword multiple times if we don't care about damage / def + if hasExisting && (len(recipe.BaseSortOrder) == 0 || baseItem.Desc().Tier() <= existingTier) { + ctx.Logger.Debug("Skipping recipe - existing runeword has equal or better tier in same base type", + "recipe", recipe.Name, + "baseType", baseItem.Type().Name, + "existingTier", existingTier, + "newBaseTier", baseItem.Desc().Tier()) + continueProcessing = false + continue + } + if inserts, hasInserts := hasItemsForRunewordRecipe(insertItems, recipe); hasInserts { + err := SocketItems(ctx, recipe, baseItem, inserts...) + if err != nil { + return err + } + + insertItems = removeUsedItems(insertItems, inserts) + } else { + continueProcessing = false + } + baseItems = removeUsedItems(baseItems, []data.Item{baseItem}) + } else { + continueProcessing = false + } + } + } + return nil +} +func SocketItems(ctx *context.Status, recipe Runeword, base data.Item, items ...data.Item) error { + + ctx.SetLastAction("SocketItem") + + ins := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) + + for _, itm := range items { + if itm.Location.LocationType == item.LocationStash || itm.Location.LocationType == item.LocationSharedStash { + OpenStash() + break + } + } + if !ctx.Data.OpenMenus.Stash && (base.Location.LocationType == item.LocationStash || base.Location.LocationType == item.LocationSharedStash) { + err := OpenStash() + if err != nil { + return err + } + } + + if base.Location.LocationType == item.LocationSharedStash { + ctx.Logger.Debug("Base in shared - checking it fits") + if !itemFitsInventory(base) { + ctx.Logger.Error("Base item does not fit in inventory", "item", base.Name) + return step.CloseAllMenus() + } else { + ctx.Logger.Debug("Base in shared stash but fits in inv, switching to correct tab") + SwitchStashTab(base.Location.Page + 1) + ctx.Logger.Debug("Switched to correct tab") + utils.Sleep(500) + screenPos := ui.GetScreenCoordsForItem(base) + ctx.Logger.Debug(fmt.Sprintf("Clicking after 5s at %d:%d", screenPos.X, screenPos.Y)) + ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) + } + } + + requiredCounts := make(map[string]int) + for _, insert := range recipe.Runes { + requiredCounts[insert]++ + } + + usedItems := make(map[*data.Item]bool) + orderedItems := make([]data.Item, 0) + + // Process each required insert in order + for _, requiredInsert := range recipe.Runes { + for i := range ins { + item := &ins[i] + if string(item.Name) == requiredInsert && !usedItems[item] { + orderedItems = append(orderedItems, *item) + usedItems[item] = true + break + } + } + } + previousPage := -1 // Initialize to invalid page number + for _, itm := range orderedItems { + if itm.Location.LocationType == item.LocationSharedStash || itm.Location.LocationType == item.LocationStash { + currentPage := itm.Location.Page + 1 + if previousPage != currentPage || currentPage != base.Location.Page { + SwitchStashTab(currentPage) + } + previousPage = currentPage + } + + screenPos := ui.GetScreenCoordsForItem(itm) + ctx.HID.Click(game.LeftButton, screenPos.X, screenPos.Y) + utils.Sleep(300) + + for _, movedBase := range ctx.Data.Inventory.AllItems { + if base.UnitID == movedBase.UnitID { + if (base.Location.LocationType == item.LocationStash) && base.Location.Page != itm.Location.Page { + SwitchStashTab(base.Location.Page + 1) + } + + basescreenPos := ui.GetScreenCoordsForItem(movedBase) + ctx.HID.Click(game.LeftButton, basescreenPos.X, basescreenPos.Y) + utils.Sleep(300) + if itm.Location.LocationType == item.LocationCursor { + DropMouseItem() + return fmt.Errorf("failed to insert item %s into base %s", itm.Name, base.Name) + } + } + } + utils.Sleep(300) + } + return step.CloseAllMenus() +} + +func currentRunewordBaseTier(ctx *context.Status, recipe Runeword, baseType string) (item.Tier, bool) { + + items := ctx.Data.Inventory.ByLocation( + item.LocationInventory, + item.LocationEquipped, + item.LocationStash, + item.LocationSharedStash, + ) + + for _, itm := range items { + if itm.RunewordName == recipe.Name && itm.Type().Name == baseType { + return itm.Desc().Tier(), true + } + } + return 0, false +} + +func hasBaseForRunewordRecipe(items []data.Item, recipe Runeword) (data.Item, bool) { + var validBases []data.Item + for _, itm := range items { + itemType := itm.Type().Code + + isValidType := false + for _, baseType := range recipe.BaseItemTypes { + if itemType == baseType { + isValidType = true + break + } + } + if !isValidType { + continue + } + + sockets, found := itm.FindStat(stat.NumSockets, 0) + if !found || sockets.Value != len(recipe.Runes) { + continue + } + + if itm.Ethereal && !recipe.AllowEth { + continue + } + + if itm.HasSocketedItems() { + continue + } + + if itm.Quality > item.QualitySuperior { + continue + } + + validBases = append(validBases, itm) + } + + if len(validBases) == 0 { + return data.Item{}, false + } + + sortBases := func() { + // Try stat-based sorting first if BaseSortOrder is provided + if len(recipe.BaseSortOrder) > 0 { + // Find which stats actually exist on at least one base + var validSortStats []stat.ID + for _, statID := range recipe.BaseSortOrder { + for _, base := range validBases { + if _, found := base.FindStat(statID, 0); found { + validSortStats = append(validSortStats, statID) + break + } + } + } + + // If we have valid stats to sort by, use them + if len(validSortStats) > 0 { + + slices.SortFunc(validBases, func(a, b data.Item) int { + for _, statID := range validSortStats { + statA, foundA := a.FindStat(statID, 0) + statB, foundB := b.FindStat(statID, 0) + + // Skip if neither has this stat + if !foundA && !foundB { + continue + } + + if !foundA { + return 1 // b comes first + } + if !foundB { + return -1 // a comes first + } + if statA.Value != statB.Value { + return statB.Value - statA.Value // Higher values first + } + } + return 0 + }) + return + } + } + + // Fall back to requirement-based sorting + slices.SortFunc(validBases, func(a, b data.Item) int { + aTotal := a.Desc().RequiredStrength + a.Desc().RequiredDexterity + bTotal := b.Desc().RequiredStrength + b.Desc().RequiredDexterity + return aTotal - bTotal // Lower requirements first + }) + } + + // Sort the bases + sortBases() + + // Get the best base + bestBase := validBases[0] + + return bestBase, true +} + +func hasItemsForRunewordRecipe(items []data.Item, recipe Runeword) ([]data.Item, bool) { + + RunewordRecipeItems := make(map[string]int) + for _, item := range recipe.Runes { + RunewordRecipeItems[item]++ + } + + itemsForRecipe := []data.Item{} + + for _, item := range items { + if count, ok := RunewordRecipeItems[string(item.Name)]; ok { + + itemsForRecipe = append(itemsForRecipe, item) + + // Check if we now have exactly the needed count before decrementing + count -= 1 + if count == 0 { + delete(RunewordRecipeItems, string(item.Name)) + if len(RunewordRecipeItems) == 0 { + return itemsForRecipe, true + } + } else { + RunewordRecipeItems[string(item.Name)] = count + } + } + } + + return nil, false +} diff --git a/internal/action/runeword_recipes.go b/internal/action/runeword_recipes.go index 00274d58f..c7a5dbbeb 100644 --- a/internal/action/runeword_recipes.go +++ b/internal/action/runeword_recipes.go @@ -1,392 +1,890 @@ package action import ( - "fmt" - "slices" - - "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/item" "github.com/hectorgimenez/d2go/pkg/data/stat" - "github.com/hectorgimenez/koolo/internal/action/step" - "github.com/hectorgimenez/koolo/internal/context" - "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/ui" - "github.com/hectorgimenez/koolo/internal/utils" -) - -type RunewordRecipe struct { - Name string - Inserts []string - BaseItemTypes []string - BaseSortOrder []stat.ID - AllowEth bool -} - -var ( - RunewordRecipes = []RunewordRecipe{ - // Add recipes in order of priority. If we have inserts for two recipes, it will process the first one in the list first. - // Add the inserts in order required for the runeword - { - Name: "Chains of Honor", - Inserts: []string{"DolRune", "UmRune", "BerRune", "IstRune"}, - BaseItemTypes: []string{"Armor"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: true, - }, - { - Name: "Fortitude", - Inserts: []string{"ElRune", "SolRune", "DolRune", "LoRune"}, - BaseItemTypes: []string{"Armor"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: true, - }, - { - Name: "Call to Arms", - Inserts: []string{"AmnRune", "RalRune", "MalRune", "IstRune", "OhmRune"}, - BaseItemTypes: []string{"Mace"}, - BaseSortOrder: []stat.ID{}, - AllowEth: true, - }, - { - Name: "Heart of the Oak", - Inserts: []string{"KoRune", "VexRune", "PulRune", "ThulRune"}, - BaseItemTypes: []string{"Mace"}, - BaseSortOrder: []stat.ID{}, - AllowEth: true, - }, - { - Name: "Flickering Flame", - Inserts: []string{"NefRune", "PulRune", "VexRune"}, - BaseItemTypes: []string{"Helm"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - { - Name: "Smoke", - Inserts: []string{"NefRune", "LumRune"}, - BaseItemTypes: []string{"Armor"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - { - Name: "Rhyme", - Inserts: []string{"ShaelRune", "EthRune"}, - BaseItemTypes: []string{"Shield"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - { - Name: "Spirit", - Inserts: []string{"TalRune", "ThulRune", "OrtRune", "AmnRune"}, - BaseItemTypes: []string{"Sword", "Shield"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - { - Name: "Insight", - Inserts: []string{"RalRune", "TirRune", "TalRune", "SolRune"}, - BaseItemTypes: []string{"Polearm"}, - BaseSortOrder: []stat.ID{stat.TwoHandedMaxDamage}, - AllowEth: true, - }, - { - Name: "Cure", - Inserts: []string{"ShaelRune", "IoRune", "TalRune"}, - BaseItemTypes: []string{"Helm"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: true, - }, - { - Name: "Lore", - Inserts: []string{"OrtRune", "SolRune"}, - BaseItemTypes: []string{"Helm"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - { - Name: "Ancients Pledge", - Inserts: []string{"RalRune", "OrtRune", "TalRune"}, - BaseItemTypes: []string{"Shield"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - { - Name: "Leaf", - Inserts: []string{"TirRune", "RalRune"}, - BaseItemTypes: []string{"Staff"}, - BaseSortOrder: []stat.ID{}, - AllowEth: true, - }, - { - Name: "Stealth", - Inserts: []string{"TalRune", "EthRune"}, - BaseItemTypes: []string{"Armor"}, - BaseSortOrder: []stat.ID{stat.Defense}, - AllowEth: false, - }, - } ) -func MakeRunewords() error { - ctx := context.Get() - ctx.SetLastAction("SocketAddItems") - - insertItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) - baseItems := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) - - for _, recipe := range RunewordRecipes { - if !slices.Contains(ctx.CharacterCfg.Game.Leveling.EnabledRunewordRecipes, recipe.Name) { - continue - } - - ctx.Logger.Debug("Socket recipe is enabled, processing", "recipe", recipe.Name) - - continueProcessing := true - for continueProcessing { - - if baseItem, hasBase := hasBaseForRunewordRecipe(baseItems, recipe); hasBase { - - existingTier, hasExisting := currentRunewordBaseTier(ctx, recipe, baseItem.Type().Name) - // Prevent creating runeword multiple times if we don't care about damage / def - if hasExisting && (len(recipe.BaseSortOrder) == 0 || baseItem.Desc().Tier() <= existingTier) { - ctx.Logger.Debug("Skipping recipe - existing runeword has equal or better tier in same base type", - "recipe", recipe.Name, - "baseType", baseItem.Type().Name, - "existingTier", existingTier, - "newBaseTier", baseItem.Desc().Tier()) - continueProcessing = false - continue - } - if inserts, hasInserts := hasItemsForRunewordRecipe(insertItems, recipe); hasInserts { - err := SocketItems(ctx, recipe, baseItem, inserts...) - if err != nil { - return err - } - - insertItems = removeUsedItems(insertItems, inserts) - } else { - continueProcessing = false - } - baseItems = removeUsedItems(baseItems, []data.Item{baseItem}) - } else { - continueProcessing = false - } - } - } - return nil -} -func SocketItems(ctx *context.Status, recipe RunewordRecipe, base data.Item, items ...data.Item) error { - - ctx.SetLastAction("SocketItem") - - ins := ctx.Data.Inventory.ByLocation(item.LocationStash, item.LocationSharedStash, item.LocationInventory) - - for _, itm := range items { - if itm.Location.LocationType == item.LocationStash || itm.Location.LocationType == item.LocationSharedStash { - OpenStash() - break - } - } - if !ctx.Data.OpenMenus.Stash && (base.Location.LocationType == item.LocationStash || base.Location.LocationType == item.LocationSharedStash) { - err := OpenStash() - if err != nil { - return err - } - } - - if base.Location.LocationType == item.LocationSharedStash { - ctx.Logger.Debug("Base in shared - checking it fits") - if !itemFitsInventory(base) { - ctx.Logger.Error("Base item does not fit in inventory", "item", base.Name) - return step.CloseAllMenus() - } else { - ctx.Logger.Debug("Base in shared stash but fits in inv, switching to correct tab") - SwitchStashTab(base.Location.Page + 1) - ctx.Logger.Debug("Switched to correct tab") - utils.Sleep(500) - screenPos := ui.GetScreenCoordsForItem(base) - ctx.Logger.Debug(fmt.Sprintf("Clicking after 5s at %d:%d", screenPos.X, screenPos.Y)) - ctx.HID.ClickWithModifier(game.LeftButton, screenPos.X, screenPos.Y, game.CtrlKey) - } - } - - requiredCounts := make(map[string]int) - for _, insert := range recipe.Inserts { - requiredCounts[insert]++ - } - - usedItems := make(map[*data.Item]bool) - orderedItems := make([]data.Item, 0) - - // Process each required insert in order - for _, requiredInsert := range recipe.Inserts { - for i := range ins { - item := &ins[i] - if string(item.Name) == requiredInsert && !usedItems[item] { - orderedItems = append(orderedItems, *item) - usedItems[item] = true - break - } - } - } - previousPage := -1 // Initialize to invalid page number - for _, itm := range orderedItems { - if itm.Location.LocationType == item.LocationSharedStash || itm.Location.LocationType == item.LocationStash { - currentPage := itm.Location.Page + 1 - if previousPage != currentPage || currentPage != base.Location.Page { - SwitchStashTab(currentPage) - } - previousPage = currentPage - } - - screenPos := ui.GetScreenCoordsForItem(itm) - ctx.HID.Click(game.LeftButton, screenPos.X, screenPos.Y) - utils.Sleep(300) - - for _, movedBase := range ctx.Data.Inventory.AllItems { - if base.UnitID == movedBase.UnitID { - if (base.Location.LocationType == item.LocationStash) && base.Location.Page != itm.Location.Page { - SwitchStashTab(base.Location.Page + 1) - } - - basescreenPos := ui.GetScreenCoordsForItem(movedBase) - ctx.HID.Click(game.LeftButton, basescreenPos.X, basescreenPos.Y) - utils.Sleep(300) - if itm.Location.LocationType == item.LocationCursor { - DropMouseItem() - return fmt.Errorf("failed to insert item %s into base %s", itm.Name, base.Name) - } - } - } - utils.Sleep(300) - } - return step.CloseAllMenus() +type ItemBase struct { + Name string + NumSockets int } -func currentRunewordBaseTier(ctx *context.Status, recipe RunewordRecipe, baseType string) (item.Tier, bool) { - - items := ctx.Data.Inventory.ByLocation( - item.LocationInventory, - item.LocationEquipped, - item.LocationStash, - item.LocationSharedStash, - ) - - for _, itm := range items { - if string(itm.RunewordName) == recipe.Name && itm.Type().Name == baseType { - return itm.Desc().Tier(), true - } - } - return 0, false +type RunewordStatRolls struct { + Min float64 + Max float64 + StatID stat.ID + Layer int } -func hasBaseForRunewordRecipe(items []data.Item, rwrecipe RunewordRecipe) (data.Item, bool) { - var validBases []data.Item - for _, itm := range items { - itemType := itm.Type().Name +type Runeword struct { + Enabled bool - isValidType := false - for _, baseType := range rwrecipe.BaseItemTypes { - if itemType == baseType { - isValidType = true - break - } - } - if !isValidType { - continue - } - - sockets, found := itm.FindStat(stat.NumSockets, 0) - if !found || sockets.Value != len(rwrecipe.Inserts) { - continue - } - - if itm.Ethereal && !rwrecipe.AllowEth { - continue - } - - if itm.HasSocketedItems() { - continue - } - - if itm.Quality > item.QualitySuperior { - continue - } - - validBases = append(validBases, itm) - } - - if len(validBases) == 0 { - return data.Item{}, false - } - - // Sort bases by BaseSortOrder if provided - if len(rwrecipe.BaseSortOrder) > 0 { - slices.SortFunc(validBases, func(a, b data.Item) int { - for _, statID := range rwrecipe.BaseSortOrder { - statA, foundA := a.FindStat(statID, 0) - statB, foundB := b.FindStat(statID, 0) - - if !foundA && !foundB { - continue - } - if !foundA { - return -1 - } - if !foundB { - return 1 - } - - if statA.Value != statB.Value { - return statB.Value - statA.Value // Higher values first - } - } - return 0 - }) - } else { - // When no BaseSortOrder specified, sort by lowest combined requirements - slices.SortFunc(validBases, func(a, b data.Item) int { - aStr := a.Desc().RequiredStrength - aDex := a.Desc().RequiredDexterity - bStr := b.Desc().RequiredStrength - bDex := b.Desc().RequiredDexterity - - aTotal := aStr + aDex - bTotal := bStr + bDex - - return aTotal - bTotal // Lower requirements first - }) - } + // Required + Name item.RunewordName + Runes []string // rune order is important! Always has to be the same + BaseItemTypes []string + Rolls []RunewordStatRolls - return validBases[0], true + // Options + AllowEth bool + AllowReroll bool + BaseSortOrder []stat.ID + // BaseItems will override BaseItemTypes + BaseItems []item.Name + PickNoSocketBase bool } -func hasItemsForRunewordRecipe(items []data.Item, rwrecipe RunewordRecipe) ([]data.Item, bool) { - - RunewordRecipeItems := make(map[string]int) - for _, item := range rwrecipe.Inserts { - RunewordRecipeItems[item]++ - } - - itemsForRecipe := []data.Item{} - - for _, item := range items { - if count, ok := RunewordRecipeItems[string(item.Name)]; ok { - - itemsForRecipe = append(itemsForRecipe, item) - - // Check if we now have exactly the needed count before decrementing - count -= 1 - if count == 0 { - delete(RunewordRecipeItems, string(item.Name)) - if len(RunewordRecipeItems) == 0 { - return itemsForRecipe, true - } - } else { - RunewordRecipeItems[string(item.Name)] = count - } - } - } - - return nil, false +var Runewords = []Runeword{ + { + Name: item.RunewordAncientsPledge, + Runes: []string{"RalRune", "OrtRune", "TalRune"}, + BaseItemTypes: []string{item.TypeShield, item.TypeAuricShields}, + AllowEth: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordBeast, + Runes: []string{"BerRune", "TirRune", "UmRune", "MalRune", "LumRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeHammer, item.TypeScepter}, + Rolls: []RunewordStatRolls{ + {Min: 240, Max: 270, StatID: stat.EnhancedDamageMin}, + {Min: 25, Max: 40, StatID: stat.Strength}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordBlack, + Runes: []string{"ThulRune", "IoRune", "NefRune"}, + BaseItemTypes: []string{item.TypeClub, item.TypeHammer, item.TypeMace}, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordBone, + Runes: []string{"SolRune", "UmRune", "UmRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 100, Max: 150, StatID: stat.MaxMana}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordBramble, + Runes: []string{"RalRune", "OhmRune", "SurRune", "EthRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 15, Max: 21, StatID: stat.Aura}, // 103 + {Min: 25, Max: 50, StatID: stat.PoisonSkillDamage}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordBrand, + Runes: []string{"JahRune", "LoRune", "MalRune", "GulRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 260, Max: 340, StatID: stat.EnhancedDamageMin}, + {Min: 280, Max: 330, StatID: stat.DemonDamagePercent}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordBreathOfTheDying, + Runes: []string{"VexRune", "HelRune", "ElRune", "EldRune", "ZodRune", "EthRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 350, Max: 400, StatID: stat.EnhancedDamageMin}, + {Min: 12, Max: 15, StatID: stat.LifeSteal}, + }, + AllowEth: true, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordBulwark, + Runes: []string{"ShaelRune", "IoRune", "SolRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 4, Max: 6, StatID: stat.LifeSteal}, + {Min: 75, Max: 100, StatID: stat.EnhancedDefense}, + {Min: 10, Max: 15, StatID: stat.DamageReduced}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordCallToArms, + Runes: []string{"AmnRune", "RalRune", "MalRune", "IstRune", "OhmRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + // {Min: 240, Max: 290, StatID: stat.EnhancedDamageMin}, // Excluding temporarily + {Min: 2, Max: 6, StatID: stat.NonClassSkill, Layer: 155}, // 155 + {Min: 1, Max: 6, StatID: stat.NonClassSkill, Layer: 149}, // 149 + {Min: 1, Max: 4, StatID: stat.NonClassSkill, Layer: 146}, // 146 + }, + AllowReroll: true, + AllowEth: true, + }, + { + Name: item.RunewordChainsOfHonor, + Runes: []string{"DolRune", "UmRune", "BerRune", "IstRune"}, + BaseItemTypes: []string{item.TypeArmor}, + AllowEth: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordChaos, + Runes: []string{"FalRune", "OhmRune", "UmRune"}, + BaseItemTypes: []string{item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 240, Max: 290, StatID: stat.EnhancedDamageMin}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.SingleSkill, stat.MinDamage}, + }, + { + Name: item.RunewordCrescentMoon, + Runes: []string{"ShaelRune", "UmRune", "TirRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeSword, item.TypePolearm}, + Rolls: []RunewordStatRolls{ + {Min: 180, Max: 220, StatID: stat.EnhancedDamageMin}, + {Min: 9, Max: 11, StatID: stat.AbsorbMagic}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordCure, + Runes: []string{"ShaelRune", "IoRune", "TalRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 75, Max: 100, StatID: stat.EnhancedDefense}, + {Min: 40, Max: 60, StatID: stat.PoisonResist}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordDeath, + Runes: []string{"HelRune", "ElRune", "VexRune", "OrtRune", "GulRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeAxe}, + Rolls: []RunewordStatRolls{ + {Min: 300, Max: 385, StatID: stat.EnhancedDamageMin}, + }, + AllowEth: true, + AllowReroll: false, + }, + { + Name: item.RunewordDelerium, + Runes: []string{"LemRune", "IstRune", "IoRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordDestruction, + Runes: []string{"VexRune", "LoRune", "BerRune", "JahRune", "KoRune"}, + BaseItemTypes: []string{item.TypePolearm, item.TypeSword}, + BaseSortOrder: []stat.ID{stat.SingleSkill, stat.MinDamage}, + }, + { + Name: item.RunewordDoom, + Runes: []string{"HelRune", "OhmRune", "UmRune", "LoRune", "ChamRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypePolearm, item.TypeHammer}, + Rolls: []RunewordStatRolls{ + {Min: 330, Max: 370, StatID: stat.EnhancedDamageMin}, + {Min: 40, Max: 60, StatID: stat.PierceCold}, + }, + AllowEth: true, + AllowReroll: false, + }, + { + Name: item.RunewordDragon, + Runes: []string{"SurRune", "LoRune", "SolRune"}, + BaseItemTypes: []string{item.TypeArmor, item.TypeShield, item.TypeAuricShields}, + Rolls: []RunewordStatRolls{ + // {Min: 3, Max: 5, StatID: "To All Attributes"}, + }, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordDream, + Runes: []string{"IoRune", "JahRune", "PulRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet, item.TypeShield, item.TypeAuricShields}, + Rolls: []RunewordStatRolls{ + {Min: 20, Max: 30, StatID: stat.FasterHitRecovery}, + {Min: 150, Max: 220, StatID: stat.Defense}, + // {Min: 5, Max: 20, StatID: "All Resistances"}, + {Min: 12, Max: 25, StatID: stat.MagicFind}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordDuress, + Runes: []string{"ShaelRune", "UmRune", "ThulRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 10, Max: 20, StatID: stat.EnhancedDamageMin}, + {Min: 150, Max: 200, StatID: stat.EnhancedDefense}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordEdge, + Runes: []string{"TirRune", "TalRune", "AmnRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 320, Max: 380, StatID: stat.DemonDamagePercent}, + // {Min: 5, Max: 10, StatID: "To All Attributes"}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordEnigma, + Runes: []string{"JahRune", "IthRune", "BerRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 750, Max: 775, StatID: stat.EnhancedDefense}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + + { + Name: item.RunewordEnlightenment, + Runes: []string{"PulRune", "RalRune", "SolRune"}, + BaseItemTypes: []string{item.TypeArmor}, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordEternity, + Runes: []string{"AmnRune", "BerRune", "IstRune", "SolRune", "SurRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 260, Max: 310, StatID: stat.EnhancedDamageMin}, + }, + AllowEth: true, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordExile, + Runes: []string{"VexRune", "OhmRune", "IstRune", "DolRune"}, + BaseItemTypes: []string{item.TypeAuricShields}, + Rolls: []RunewordStatRolls{ + {Min: 13, Max: 16, StatID: stat.Aura, Layer: 104}, // 104 + {Min: 220, Max: 260, StatID: stat.EnhancedDefense}, + }, + AllowEth: true, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.PoisonResist}, + }, + { + Name: item.RunewordFaith, + Runes: []string{"OhmRune", "JahRune", "LemRune", "EldRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 12, Max: 15, StatID: stat.Aura, Layer: 122}, // 122 + {Min: 1, Max: 2, StatID: stat.AllSkills}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordFamine, + Runes: []string{"FalRune", "OhmRune", "OrtRune", "JahRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeHammer}, + Rolls: []RunewordStatRolls{ + {Min: 320, Max: 370, StatID: stat.EnhancedDamageMin}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordFlickeringFlame, + Runes: []string{"NefRune", "PulRune", "VexRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 4, Max: 8, StatID: stat.Aura}, // Resist fire unmapped + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordFortitude, + Runes: []string{"ElRune", "SolRune", "DolRune", "LoRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 8, Max: 12, StatID: stat.LifePerLevel}, + // {Min: 25, Max: 30, StatID: "All Resistances"}, + }, + AllowReroll: false, + AllowEth: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordFury, + Runes: []string{"JahRune", "GulRune", "EthRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordGloom, + Runes: []string{"FalRune", "UmRune", "PulRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 200, Max: 260, StatID: stat.EnhancedDefense}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordGrief, + Runes: []string{"EthRune", "TirRune", "LoRune", "MalRune", "RalRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeAxe}, + Rolls: []RunewordStatRolls{ + {Min: 30, Max: 40, StatID: stat.IncreasedAttackSpeed}, + {Min: 340, Max: 400, StatID: stat.MinDamage}, // 0 + {Min: 20, Max: 25, StatID: stat.EnemyPoisonResist}, + {Min: 10, Max: 15, StatID: stat.LifeAfterEachKill}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordGround, + Runes: []string{"ShaelRune", "IoRune", "OrtRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 75, Max: 100, StatID: stat.EnhancedDefense}, + {Min: 40, Max: 60, StatID: stat.LightningResist}, + {Min: 10, Max: 15, StatID: stat.AbsorbLightning}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordHandOfJustice, + Runes: []string{"SurRune", "ChamRune", "AmnRune", "LoRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 280, Max: 330, StatID: stat.EnhancedDamageMin}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordHarmony, + Runes: []string{"TirRune", "IthRune", "SolRune", "KoRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 200, Max: 275, StatID: stat.EnhancedDamageMin}, + {Min: 2, Max: 6, StatID: stat.SingleSkill}, // 107 + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordHeartOfTheOak, + Runes: []string{"KoRune", "VexRune", "PulRune", "ThulRune"}, + BaseItemTypes: []string{item.TypeMace}, + Rolls: []RunewordStatRolls{ + // {Min: 30, Max: 40, StatID: "All Resistances"}, + }, + AllowReroll: true, + AllowEth: true, + }, + { + Name: item.RunewordHearth, + Runes: []string{"ShaelRune", "IoRune", "ThulRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 75, Max: 100, StatID: stat.EnhancedDefense}, + {Min: 40, Max: 60, StatID: stat.ColdResist}, + {Min: 10, Max: 15, StatID: stat.AbsorbCold}, + }, + AllowReroll: true, + AllowEth: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordHolyThunder, + Runes: []string{"EthRune", "RalRune", "OrtRune", "TalRune"}, + BaseItemTypes: []string{item.TypeScepter}, + BaseSortOrder: []stat.ID{stat.SingleSkill}, + }, + { + Name: item.RunewordHonor, + Runes: []string{"AmnRune", "ElRune", "IthRune", "TirRune", "SolRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordHustle, + Runes: []string{"ShaelRune", "KoRune", "EldRune"}, + BaseItemTypes: []string{item.TypeArmor, item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 180, Max: 200, StatID: stat.EnhancedDamage}, + }, + }, + { + Name: item.RunewordIce, + Runes: []string{"AmnRune", "ShaelRune", "JahRune", "LoRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 140, Max: 210, StatID: stat.EnhancedDamageMin}, + {Min: 25, Max: 30, StatID: stat.ColdSkillDamage}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage}, + }, + { + Name: item.RunewordInfinity, + Runes: []string{"BerRune", "MalRune", "BerRune", "IstRune"}, + BaseItemTypes: []string{item.TypePolearm, item.TypeSpear}, + Rolls: []RunewordStatRolls{ + {Min: 255, Max: 325, StatID: stat.EnhancedDamageMin}, + {Min: 45, Max: 55, StatID: stat.EnemyLightningResist}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordInsight, + Runes: []string{"RalRune", "TirRune", "TalRune", "SolRune"}, + BaseItemTypes: []string{item.TypePolearm, item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 12, Max: 17, StatID: stat.Aura, Layer: 120}, // 120 + // {Min: 200, Max: 260, StatID: stat.EnhancedDamageMin}, + {Min: 180, Max: 250, StatID: stat.AttackRatingPercent}, + {Min: 1, Max: 6, StatID: stat.NonClassSkill, Layer: 120}, // 9 + }, + AllowReroll: true, + AllowEth: true, + BaseSortOrder: []stat.ID{stat.TwoHandedMaxDamage}, + }, + { + Name: item.RunewordKingslayer, + Runes: []string{"MalRune", "UmRune", "GulRune", "FalRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeAxe}, + Rolls: []RunewordStatRolls{ + {Min: 230, Max: 270, StatID: stat.EnhancedDamage}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordKingsGrace, + Runes: []string{"AmnRune", "RalRune", "ThulRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeScepter}, + Rolls: []RunewordStatRolls{ + {Min: 100, Max: 150, StatID: stat.EnhancedDamageMin}, + {Min: 50, Max: 100, StatID: stat.DemonDamagePercent}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordLastWish, + Runes: []string{"JahRune", "MalRune", "JahRune", "SurRune", "JahRune", "BerRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeHammer, item.TypeAxe}, + Rolls: []RunewordStatRolls{ + {Min: 330, Max: 375, StatID: stat.EnhancedDamage}, + {Min: 60, Max: 70, StatID: stat.CrushingBlow}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.MinDamage, stat.TwoHandedMaxDamage}, + }, + { + Name: item.RunewordLawbringer, + Runes: []string{"AmnRune", "LemRune", "KoRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeHammer, item.TypeScepter}, + Rolls: []RunewordStatRolls{ + {Min: 16, Max: 18, StatID: stat.Aura, Layer: 119}, // 119 + {Min: 200, Max: 250, StatID: stat.DefenseVsMissiles}, + }, + AllowReroll: true, + }, + { + Name: item.RunewordLeaf, + Runes: []string{"TirRune", "RalRune"}, + BaseItemTypes: []string{item.TypeStaff}, + Rolls: []RunewordStatRolls{ + {Min: 50, Max: 70, StatID: stat.FireSkillDamage}, + }, + AllowReroll: true, + AllowEth: true, + }, + { + Name: item.RunewordLionheart, + Runes: []string{"HelRune", "LumRune", "FalRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + // {Min: 10, Max: 15, StatID: "To All Attributes"}, + {Min: 15, Max: 20, StatID: stat.Vitality}, + {Min: 10, Max: 15, StatID: stat.Dexterity}, + {Min: 50, Max: 75, StatID: stat.MaxLife}, + }, + AllowReroll: true, + }, + { + Name: item.RunewordLore, + Runes: []string{"OrtRune", "SolRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + AllowEth: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordMalice, + Runes: []string{"IthRune", "ElRune", "EthRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + }, + { + Name: item.RunewordMelody, + Runes: []string{"ShaelRune", "KoRune", "NefRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + }, + { + Name: item.RunewordMemory, + Runes: []string{"LumRune", "IoRune", "SolRune", "EthRune"}, + BaseItemTypes: []string{item.TypeStaff}, + }, + { + Name: item.RunewordMetamorphosis, + Runes: []string{"IoRune", "ChamRune", "FalRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + }, + { + Name: item.RunewordMist, + Runes: []string{"ChamRune", "ShaelRune", "GulRune", "ThulRune", "IthRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 8, Max: 12, StatID: stat.Aura, Layer: 113}, // 113 + {Min: 325, Max: 375, StatID: stat.EnhancedDamage}, + }, + }, + { + Name: item.RunewordMosaic, + Runes: []string{"MalRune", "GulRune", "AmnRune"}, + BaseItemTypes: []string{item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 200, Max: 250, StatID: stat.EnhancedDamage}, + {Min: 8, Max: 15, StatID: stat.ColdSkillDamage}, + {Min: 8, Max: 15, StatID: stat.LightningSkillDamage}, + {Min: 8, Max: 15, StatID: stat.FireSkillDamage}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordMyth, + Runes: []string{"HelRune", "AmnRune", "NefRune"}, + BaseItemTypes: []string{item.TypeArmor}, + }, + { + Name: item.RunewordNadir, + Runes: []string{"NefRune", "TirRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + }, + { + Name: item.RunewordOath, + Runes: []string{"ShaelRune", "PulRune", "MalRune", "LumRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeAxe, item.TypeMace}, + Rolls: []RunewordStatRolls{ + {Min: 10, Max: 15, StatID: stat.AbsorbMagic}, + {Min: 210, Max: 340, StatID: stat.EnhancedDamageMin}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordObsession, + Runes: []string{"ZodRune", "IstRune", "LemRune", "LumRune", "IoRune", "NefRune"}, + BaseItemTypes: []string{item.TypeStaff}, + Rolls: []RunewordStatRolls{ + {Min: 15, Max: 25, StatID: stat.MaxLife}, + {Min: 15, Max: 30, StatID: stat.ManaRecoveryBonus}, + // {Min: 60, Max: 70, StatID: "All Resistances"}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordObedience, + Runes: []string{"HelRune", "KoRune", "ThulRune", "EthRune", "FalRune"}, + BaseItemTypes: []string{item.TypePolearm, item.TypeSpear}, + Rolls: []RunewordStatRolls{ + {Min: 200, Max: 300, StatID: stat.Defense}, + // {Min: 20, Max: 30, StatID: "All Resistances"}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.TwoHandedMaxDamage}, + }, + { + Name: item.RunewordPassion, + Runes: []string{"DolRune", "OrtRune", "EldRune", "LemRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 160, Max: 210, StatID: stat.EnhancedDamage}, + {Min: 50, Max: 80, StatID: stat.AttackRatingPercent}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordPattern, + Runes: []string{"TalRune", "OrtRune", "ThulRune"}, + BaseItemTypes: []string{item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 40, Max: 80, StatID: stat.EnhancedDamage}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordPeace, + Runes: []string{"ShaelRune", "ThulRune", "AmnRune"}, + BaseItemTypes: []string{item.TypeArmor}, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordPhoenix, + Runes: []string{"VexRune", "VexRune", "LoRune", "JahRune"}, + BaseItemTypes: []string{item.TypeShield, item.TypeAuricShields, item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 10, Max: 15, StatID: stat.Aura, Layer: 124}, // 124 + {Min: 350, Max: 400, StatID: stat.EnhancedDamage}, + {Min: 350, Max: 400, StatID: stat.DefenseVsMissiles}, + {Min: 15, Max: 21, StatID: stat.AbsorbFire}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.PoisonResist}, + }, + { + Name: item.RunewordPlague, + Runes: []string{"ChamRune", "ShaelRune", "UmRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeKnife, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 260, Max: 320, StatID: stat.Aura}, // Cleansing unmapped + {Min: 1, Max: 2, StatID: stat.AllSkills}, + {Min: 220, Max: 320, StatID: stat.EnhancedDamage}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordPride, + Runes: []string{"ChamRune", "SurRune", "IoRune", "LoRune"}, + BaseItemTypes: []string{item.TypePolearm, item.TypeSpear}, + Rolls: []RunewordStatRolls{ + {Min: 16, Max: 20, StatID: stat.Aura, Layer: 113}, // 113 + {Min: 260, Max: 300, StatID: stat.AttackRatingPercent}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.TwoHandedMaxDamage}, + }, + { + Name: item.RunewordPrinciple, + Runes: []string{"RalRune", "GulRune", "EldRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 100, Max: 150, StatID: stat.MaxLife}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordPrudence, + Runes: []string{"MalRune", "TirRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 140, Max: 170, StatID: stat.EnhancedDefense}, + // {Min: 25, Max: 35, StatID: "All Resistances"}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordRadiance, + Runes: []string{"NefRune", "SolRune", "IthRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordRain, + Runes: []string{"OrtRune", "MalRune", "IthRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 100, Max: 150, StatID: stat.MaxMana}, + }, + AllowReroll: true, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordRhyme, + Runes: []string{"ShaelRune", "EthRune"}, + BaseItemTypes: []string{item.TypeShield, item.TypeAuricShields}, + AllowEth: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordRift, + Runes: []string{"HelRune", "KoRune", "LemRune", "GulRune"}, + BaseItemTypes: []string{item.TypePolearm, item.TypeScepter}, + Rolls: []RunewordStatRolls{ + // {Min: 5, Max: 10, StatID: "To All Attributes"}, + }, + AllowReroll: true, + }, + { + Name: item.RunewordSanctuary, + Runes: []string{"KoRune", "KoRune", "MalRune"}, + BaseItemTypes: []string{item.TypeShield, item.TypeAuricShields}, + Rolls: []RunewordStatRolls{ + {Min: 130, Max: 160, StatID: stat.EnhancedDefense}, + // {Min: 50, Max: 70, StatID: "All Resistances"}, + }, + AllowReroll: true, + }, + { + Name: item.RunewordSilence, + Runes: []string{"DolRune", "EldRune", "HelRune", "IstRune", "TirRune", "VexRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + }, + { + Name: item.RunewordSmoke, + Runes: []string{"NefRune", "LumRune"}, + BaseItemTypes: []string{item.TypeArmor}, + AllowEth: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordSpirit, + Runes: []string{"TalRune", "ThulRune", "OrtRune", "AmnRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeShield, item.TypeAuricShields}, + Rolls: []RunewordStatRolls{ + {Min: 25, Max: 35, StatID: stat.FasterCastRate}, + {Min: 89, Max: 112, StatID: stat.MaxMana}, + {Min: 3, Max: 8, StatID: stat.AbsorbMagic}, + }, + AllowReroll: true, + AllowEth: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordSplendor, + Runes: []string{"EthRune", "LumRune"}, + BaseItemTypes: []string{item.TypeShield, item.TypeAuricShields}, + Rolls: []RunewordStatRolls{ + {Min: 60, Max: 100, StatID: stat.EnhancedDefense}, + }, + AllowReroll: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordStealth, + Runes: []string{"TalRune", "EthRune"}, + BaseItemTypes: []string{item.TypeArmor}, + AllowEth: false, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordSteel, + BaseItemTypes: []string{item.TypeSword, item.TypeAxe, item.TypeMace}, + Runes: []string{"TirRune", "ElRune"}, + }, + { + Name: item.RunewordStone, + Runes: []string{"ShaelRune", "UmRune", "PulRune", "LumRune"}, + BaseItemTypes: []string{item.TypeArmor}, + Rolls: []RunewordStatRolls{ + {Min: 250, Max: 290, StatID: stat.EnhancedDefense}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordStrength, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Runes: []string{"AmnRune", "TirRune"}, + }, + { + Name: item.RunewordTemper, + Runes: []string{"ShaelRune", "IoRune", "RalRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 75, Max: 100, StatID: stat.EnhancedDefense}, + {Min: 40, Max: 60, StatID: stat.FireResist}, + {Min: 10, Max: 15, StatID: stat.AbsorbFire}, + }, + }, + { + Name: item.RunewordTreachery, + Runes: []string{"ShaelRune", "ThulRune", "LemRune"}, + BaseItemTypes: []string{item.TypeArmor}, + BaseSortOrder: []stat.ID{stat.Defense}, + }, + { + Name: item.RunewordUnbendingWill, + Runes: []string{"FalRune", "IoRune", "IthRune", "EldRune", "ElRune", "HelRune"}, + BaseItemTypes: []string{item.TypeSword}, + Rolls: []RunewordStatRolls{ + {Min: 20, Max: 30, StatID: stat.IncreasedAttackSpeed}, + {Min: 300, Max: 350, StatID: stat.EnhancedDamage}, + {Min: 8, Max: 10, StatID: stat.LifeSteal}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordVenom, + Runes: []string{"TalRune", "DolRune", "MalRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + }, + { + Name: item.RunewordVoiceOfReason, + Runes: []string{"LemRune", "KoRune", "ElRune", "EldRune"}, + BaseItemTypes: []string{item.TypeSword, item.TypeMace}, + Rolls: []RunewordStatRolls{ + {Min: 220, Max: 350, StatID: stat.DemonDamagePercent}, + {Min: 355, Max: 375, StatID: stat.UndeadDamagePercent}, + }, + AllowReroll: true, + }, + { + Name: item.RunewordWealth, + Runes: []string{"LemRune", "KoRune", "TirRune"}, + BaseItemTypes: []string{item.TypeArmor}, + }, + { + Name: item.RunewordWhite, + Runes: []string{"DolRune", "IoRune"}, + BaseItemTypes: []string{item.TypeWand}, + }, + { + Name: item.RunewordWind, + Runes: []string{"SurRune", "ElRune"}, + BaseItemTypes: []string{item.TypeAxe, item.TypeWand, item.TypeClub, item.TypeScepter, item.TypeMace, item.TypeHammer, item.TypeSword, item.TypeKnife, item.TypeSpear, item.TypePolearm, item.TypeStaff, item.TypeHandtoHand, item.TypeHandtoHand2}, + Rolls: []RunewordStatRolls{ + {Min: 120, Max: 160, StatID: stat.EnhancedDamageMin}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordWisdom, + Runes: []string{"PulRune", "IthRune", "EldRune"}, + BaseItemTypes: []string{item.TypeHelm, item.TypePelt, item.TypePrimalHelm, item.TypeCirclet}, + Rolls: []RunewordStatRolls{ + {Min: 15, Max: 25, StatID: stat.AttackRatingPercent}, + {Min: 4, Max: 8, StatID: stat.ManaSteal}, + }, + AllowReroll: false, + }, + { + Name: item.RunewordWrath, + Runes: []string{"PulRune", "LumRune", "BerRune", "MalRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + Rolls: []RunewordStatRolls{ + {Min: 250, Max: 300, StatID: stat.UndeadDamagePercent}, + }, + AllowReroll: true, + }, + { + Name: item.RunewordZephyr, + Runes: []string{"OrtRune", "EthRune"}, + BaseItemTypes: []string{item.TypeAmazonBow, item.TypeBow, item.TypeCrossbow}, + }, } diff --git a/internal/config/rw_recipes.go b/internal/config/rw_recipes.go index f8c438dc0..f831d1a2d 100644 --- a/internal/config/rw_recipes.go +++ b/internal/config/rw_recipes.go @@ -1,19 +1,21 @@ package config +import "github.com/hectorgimenez/d2go/pkg/data/item" + var AvailableRunewordRecipes = []string{ // List out all the recipe names from runeword_recipes.go - "Chains of Honor", - "Fortitude", - "Call to Arms", - "Heart of the Oak", - "Flickering Flame", - "Smoke", - "Rhyme", - "Spirit", - "Insight", - "Cure", - "Lore", - "Ancients Pledge", - "Leaf", - "Stealth", + string(item.RunewordChainsOfHonor), + string(item.RunewordFortitude), + string(item.RunewordCallToArms), + string(item.RunewordHeartOfTheOak), + string(item.RunewordFlickeringFlame), + string(item.RunewordSmoke), + string(item.RunewordRhyme), + string(item.RunewordSpirit), + string(item.RunewordInsight), + string(item.RunewordCure), + string(item.RunewordLore), + string(item.RunewordAncientsPledge), + string(item.RunewordLeaf), + string(item.RunewordStealth), }