diff --git a/cmd/raid-gear/main.go b/cmd/raid-gear/main.go index 1eddde2..fd9c43b 100644 --- a/cmd/raid-gear/main.go +++ b/cmd/raid-gear/main.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "log" - "math" "math/rand" "os" "strings" @@ -179,21 +178,21 @@ func getSimilarWeaponSubclasses(originalSubclass int) []int { // Weapon subclass mappings for fallback searches weaponGroups := map[int][]int{ // Two-handed weapons - 1: {1, 5, 8}, // 2H Axe -> 2H Axe, 2H Mace, 2H Sword - 5: {1, 5, 8}, // 2H Mace -> 2H Axe, 2H Mace, 2H Sword - 8: {1, 5, 8}, // 2H Sword -> 2H Axe, 2H Mace, 2H Sword - 6: {6, 17}, // Polearm -> Polearm, Spear - 17: {6, 17}, // Spear -> Polearm, Spear - 9: {9, 10}, // Staff -> Staff, Stave - 10: {9, 10}, // Stave -> Staff, Stave - + 1: {1, 5, 8}, // 2H Axe -> 2H Axe, 2H Mace, 2H Sword + 5: {1, 5, 8}, // 2H Mace -> 2H Axe, 2H Mace, 2H Sword + 8: {1, 5, 8}, // 2H Sword -> 2H Axe, 2H Mace, 2H Sword + 6: {6, 17}, // Polearm -> Polearm, Spear + 17: {6, 17}, // Spear -> Polearm, Spear + 9: {9, 10}, // Staff -> Staff, Stave + 10: {9, 10}, // Stave -> Staff, Stave + // One-handed weapons 0: {0, 4, 7, 15}, // 1H Axe -> 1H Axe, 1H Mace, 1H Sword, Fist 4: {0, 4, 7, 15}, // 1H Mace -> 1H Axe, 1H Mace, 1H Sword, Fist 7: {0, 4, 7, 15}, // 1H Sword -> 1H Axe, 1H Mace, 1H Sword, Fist 15: {0, 4, 7, 15}, // Fist -> 1H Axe, 1H Mace, 1H Sword, Fist 13: {13}, // Dagger -> Dagger (unique) - + // Ranged weapons 2: {2, 3, 18}, // Bow -> Bow, Gun, Crossbow 3: {2, 3, 18}, // Gun -> Bow, Gun, Crossbow @@ -202,7 +201,7 @@ func getSimilarWeaponSubclasses(originalSubclass int) []int { 19: {19}, // Wand -> Wand (unique) 20: {20}, // Fishing Pole -> Fishing Pole (unique) } - + if alternatives, exists := weaponGroups[originalSubclass]; exists { // Return alternatives excluding the original subclass var result []int @@ -213,18 +212,18 @@ func getSimilarWeaponSubclasses(originalSubclass int) []int { } return result } - + return []int{} // No alternatives found } // addMissingKeyStats automatically adds missing key stats (SPELL_POWER/ATTACK_POWER) when validation fails func (g *MoltenCoreGenerator) addMissingKeyStats(item *items.Item, classType int) bool { var addedStats []string - + // Determine what key stat should be added based on class type var targetStatType int var statName string - + switch classType { case 4, 5: // Mage, Healer - need SPELL_POWER targetStatType = 45 // SPELL_POWER @@ -235,7 +234,7 @@ func (g *MoltenCoreGenerator) addMissingKeyStats(item *items.Item, classType int default: return false // Unknown class type, can't determine what stat to add } - + // Check if the item already has this key stat for i := 1; i <= 8; i++ { statTypeField := fmt.Sprintf("StatType%d", i) @@ -243,33 +242,33 @@ func (g *MoltenCoreGenerator) addMissingKeyStats(item *items.Item, classType int return false // Already has the key stat } } - + // Find an empty stat slot to add the missing key stat for i := 1; i <= 8; i++ { statTypeField := fmt.Sprintf("StatType%d", i) statValueField := fmt.Sprintf("StatValue%d", i) - + if statType, err := item.GetField(statTypeField); err == nil && statType == 0 { - // Found empty slot, calculate appropriate stat value - baseValue := rand.Intn(151) + 350 // Random between 350-500 - + // Found empty slot, calculate appropriate stat value (reduced by 50% to prevent over-correction) + baseValue := rand.Intn(76) + 175 // Random between 175-250 (50% of original 350-500) + // Apply scaling based on item level and quality scaledValue := g.calculateScaledStatValue(baseValue, targetStatType) - + // Set the stat item.UpdateField(statTypeField, targetStatType) item.UpdateField(statValueField, scaledValue) - + addedStats = append(addedStats, fmt.Sprintf("%s: %d", statName, scaledValue)) - + if g.debug { log.Printf("Auto-added missing key stat %s (%d) = %d to %s", statName, targetStatType, scaledValue, item.Name) } - + return true } } - + return false // No empty slots available } @@ -280,17 +279,17 @@ func (g *MoltenCoreGenerator) calculateScaledStatValue(baseValue, statType int) if factor, exists := config.ScalingFactor[statType]; exists { scalingFactor = factor } - + // Apply item level and quality scaling itemLevelModifier := float64(g.itemLevel) / 100.0 qualityModifier := 1.0 if modifier, exists := config.QualityModifiers[g.quality]; exists { qualityModifier = modifier } - + // Calculate final value finalValue := float64(baseValue) * scalingFactor * itemLevelModifier * qualityModifier - + return int(finalValue) } @@ -305,7 +304,7 @@ func getSimilarArmorSubclasses(originalSubclass int) []int { 6: {6}, // Shield -> Shield only 0: {0}, // Miscellaneous -> Miscellaneous only } - + if alternatives, exists := armorGroups[originalSubclass]; exists { // Return alternatives excluding the original subclass var result []int @@ -316,7 +315,7 @@ func getSimilarArmorSubclasses(originalSubclass int) []int { } return result } - + return []int{} // No alternatives found } @@ -348,6 +347,74 @@ func NewMoltenCoreGenerator(db *mysql.MySqlDb, debug bool) *MoltenCoreGenerator } } +// getInventoryTypeString returns a human-readable string for inventory type +func getInventoryTypeString(inventoryType *int) string { + if inventoryType == nil { + return "nil" + } + + switch *inventoryType { + case 0: + return "Non-equippable" + case 1: + return "Head" + case 2: + return "Neck" + case 3: + return "Shoulder" + case 4: + return "Shirt" + case 5: + return "Chest" + case 6: + return "Waist" + case 7: + return "Legs" + case 8: + return "Feet" + case 9: + return "Wrists" + case 10: + return "Hands" + case 11: + return "Finger" + case 12: + return "Trinket" + case 13: + return "One-Hand" + case 14: + return "Shield" + case 15: + return "Ranged" + case 16: + return "Back" + case 17: + return "Two-Hand" + case 18: + return "Bag" + case 19: + return "Tabard" + case 20: + return "Robe" + case 21: + return "Main-Hand" + case 22: + return "Off-Hand" + case 23: + return "Held-In-Off-Hand" + case 24: + return "Ammo" + case 25: + return "Thrown" + case 26: + return "Ranged-Right" + case 28: + return "Relic" + default: + return fmt.Sprintf("Unknown(%d)", *inventoryType) + } +} + func getClassString(class int) string { switch class { case 1: @@ -367,6 +434,256 @@ func getClassString(class int) string { } } +// generateClassAppropriateStats generates appropriate stats for an item based on class type +func (g *MoltenCoreGenerator) generateClassAppropriateStats(item *items.Item, classType int) { + // Clear existing stats first + g.clearItemStats(item) + + // Determine appropriate stats based on class type and item type + isWeapon := item.Class != nil && *item.Class == 2 + isTrinket := item.InventoryType != nil && *item.InventoryType == 0 + isRing := item.InventoryType != nil && *item.InventoryType == 11 + + // Base stat values scaled for item level + baseStatValue := g.calculateBaseStatValue() + highStatValue := int(float64(baseStatValue) * 1.5) // Primary stats get higher values + + statSlot := 1 + + switch classType { + case 1: // Strength Melee + // Primary stats: Strength, Stamina + statSlot = g.setItemStat(item, statSlot, 4, highStatValue) // Strength + statSlot = g.setItemStat(item, statSlot, 7, baseStatValue) // Stamina + + if isWeapon { + // Weapons: Attack Power + 2 secondary stats + statSlot = g.setItemStat(item, statSlot, 38, highStatValue*2) // Attack Power (higher value) + // Add 2 random secondary stats + secondaryStats := []int{18, 27, 15} // Crit Rating, Haste Rating, Hit Rating + for i := 0; i < 2 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } else { + // Armor: Add more secondary stats + secondaryStats := []int{18, 27, 15, 43} // Crit, Haste, Hit, Armor Pen + for i := 0; i < 3 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } + + case 2: // Agility Melee + // Primary stats: Agility, Stamina + statSlot = g.setItemStat(item, statSlot, 3, highStatValue) // Agility + statSlot = g.setItemStat(item, statSlot, 7, baseStatValue) // Stamina + + if isWeapon { + // Weapons: Attack Power + 2 secondary stats + statSlot = g.setItemStat(item, statSlot, 38, highStatValue*2) // Attack Power + secondaryStats := []int{19, 28, 16} // Crit Ranged, Haste Ranged, Hit Ranged + for i := 0; i < 2 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } else { + // Armor: Add more secondary stats + secondaryStats := []int{19, 28, 16, 43} // Crit Ranged, Haste Ranged, Hit Ranged, Armor Pen + for i := 0; i < 3 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } + + case 3: // Ranged Attacker + // Primary stats: Agility, Stamina + statSlot = g.setItemStat(item, statSlot, 3, highStatValue) // Agility + statSlot = g.setItemStat(item, statSlot, 7, baseStatValue) // Stamina + + if isWeapon { + // Weapons: Ranged Attack Power + 2 secondary stats + statSlot = g.setItemStat(item, statSlot, 39, highStatValue*2) // Ranged Attack Power + secondaryStats := []int{19, 28, 16} // Crit Ranged, Haste Ranged, Hit Ranged + for i := 0; i < 2 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } else { + // Armor: Add more secondary stats + secondaryStats := []int{19, 28, 16, 43} // Crit Ranged, Haste Ranged, Hit Ranged, Armor Pen + for i := 0; i < 3 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } + + case 4: // Mage + // Primary stats: Intellect, Stamina + statSlot = g.setItemStat(item, statSlot, 5, highStatValue) // Intellect + statSlot = g.setItemStat(item, statSlot, 7, baseStatValue) // Stamina + + if isWeapon { + // Weapons: Spell Power + 2 secondary stats + statSlot = g.setItemStat(item, statSlot, 45, highStatValue*2) // Spell Power + secondaryStats := []int{20, 29, 17} // Crit Spell, Haste Spell, Hit Spell + for i := 0; i < 2 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } else { + // Armor: Add more secondary stats + secondaryStats := []int{20, 29, 17, 46} // Crit Spell, Haste Spell, Hit Spell, Spell Penetration + for i := 0; i < 3 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } + + case 5: // Healer + // Primary stats: Intellect, Spirit, Stamina + statSlot = g.setItemStat(item, statSlot, 5, highStatValue) // Intellect + statSlot = g.setItemStat(item, statSlot, 6, highStatValue) // Spirit + statSlot = g.setItemStat(item, statSlot, 7, baseStatValue) // Stamina + + if isWeapon { + // Weapons: Spell Power + 1 secondary stat + statSlot = g.setItemStat(item, statSlot, 45, highStatValue*2) // Spell Power + secondaryStats := []int{42, 29} // Mana Regen, Haste Spell + if statSlot <= 7 { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } else { + // Armor: Add more secondary stats + secondaryStats := []int{42, 29, 40} // Mana Regen, Haste Spell, Spell Healing + for i := 0; i < 2 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } + + case 6: // Tank + // Primary stats: Strength, Stamina + statSlot = g.setItemStat(item, statSlot, 4, highStatValue) // Strength + statSlot = g.setItemStat(item, statSlot, 7, highStatValue*2) // Stamina (higher for tanks) + + if isWeapon { + // Weapons: Defense + 2 secondary stats + statSlot = g.setItemStat(item, statSlot, 12, baseStatValue) // Defense Rating + secondaryStats := []int{14, 13, 47} // Parry, Dodge, Block Value + for i := 0; i < 2 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } else { + // Armor: Add more defensive stats + secondaryStats := []int{12, 14, 13, 47} // Defense, Parry, Dodge, Block Value + for i := 0; i < 3 && statSlot <= 7; i++ { + statType := secondaryStats[rand.Intn(len(secondaryStats))] + statSlot = g.setItemStat(item, statSlot, statType, baseStatValue) + } + } + + default: // Generic - use basic stats + statSlot = g.setItemStat(item, statSlot, 7, baseStatValue) // Stamina + statSlot = g.setItemStat(item, statSlot, 4, baseStatValue) // Strength + statSlot = g.setItemStat(item, statSlot, 3, baseStatValue) // Agility + } + + // Handle special item types + if isTrinket { + // Trinkets should have exactly 2 stats + g.clearItemStats(item) + statSlot = 1 + switch classType { + case 1, 2, 3: // Physical DPS + statSlot = g.setItemStat(item, statSlot, 38, highStatValue*2) // Attack Power + statSlot = g.setItemStat(item, statSlot, 18, baseStatValue) // Crit Rating + case 4, 5: // Casters + statSlot = g.setItemStat(item, statSlot, 45, highStatValue*2) // Spell Power + statSlot = g.setItemStat(item, statSlot, 20, baseStatValue) // Crit Spell Rating + case 6: // Tank + statSlot = g.setItemStat(item, statSlot, 7, highStatValue*2) // Stamina + statSlot = g.setItemStat(item, statSlot, 12, baseStatValue) // Defense Rating + } + } else if isRing { + // Rings should have 3-4 stats, clear and rebuild + g.clearItemStats(item) + statSlot = 1 + switch classType { + case 1: // Strength Melee + statSlot = g.setItemStat(item, statSlot, 4, baseStatValue) // Strength + statSlot = g.setItemStat(item, statSlot, 38, highStatValue) // Attack Power + statSlot = g.setItemStat(item, statSlot, 18, baseStatValue) // Crit Rating + statSlot = g.setItemStat(item, statSlot, 27, baseStatValue) // Haste Rating + case 2, 3: // Agility/Ranged + statSlot = g.setItemStat(item, statSlot, 3, baseStatValue) // Agility + statSlot = g.setItemStat(item, statSlot, 38, highStatValue) // Attack Power + statSlot = g.setItemStat(item, statSlot, 19, baseStatValue) // Crit Ranged Rating + case 4, 5: // Casters + statSlot = g.setItemStat(item, statSlot, 5, baseStatValue) // Intellect + statSlot = g.setItemStat(item, statSlot, 45, highStatValue) // Spell Power + statSlot = g.setItemStat(item, statSlot, 20, baseStatValue) // Crit Spell Rating + case 6: // Tank + statSlot = g.setItemStat(item, statSlot, 7, highStatValue) // Stamina + statSlot = g.setItemStat(item, statSlot, 12, baseStatValue) // Defense Rating + statSlot = g.setItemStat(item, statSlot, 14, baseStatValue) // Parry Rating + } + } + + // Scale the item to the target level and quality + item.ScaleItem(g.itemLevel, g.quality) +} + +// clearItemStats clears all stats from an item +func (g *MoltenCoreGenerator) clearItemStats(item *items.Item) { + zero := 0 + for i := 1; i <= 10; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statValueField := fmt.Sprintf("StatValue%d", i) + item.UpdateField(statTypeField, zero) + item.UpdateField(statValueField, zero) + } +} + +// setItemStat sets a stat on an item and returns the next available slot +func (g *MoltenCoreGenerator) setItemStat(item *items.Item, slot int, statType int, statValue int) int { + if slot > 10 { + return slot // No more slots available + } + + statTypeField := fmt.Sprintf("StatType%d", slot) + statValueField := fmt.Sprintf("StatValue%d", slot) + + item.UpdateField(statTypeField, statType) + item.UpdateField(statValueField, statValue) + + return slot + 1 +} + +// calculateBaseStatValue calculates appropriate base stat values for the item level +func (g *MoltenCoreGenerator) calculateBaseStatValue() int { + // Base calculation similar to what reference items would have + baseValue := int(float64(g.itemLevel) * 2.5) // Rough scaling factor + + // Apply quality modifier + if qualityMod, exists := config.QualityModifiers[g.quality]; exists { + baseValue = int(float64(baseValue) * qualityMod) + } + + // Add some randomness (±10%) + variation := int(float64(baseValue) * 0.1) + baseValue += rand.Intn(variation*2) - variation + + // Ensure minimum value + if baseValue < 10 { + baseValue = 10 + } + + return baseValue +} + func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(log.LstdFlags | log.Lshortfile) @@ -420,8 +737,9 @@ func main() { fmt.Printf("[%d/%d] Processing: %s (Entry: %d)\n", i+1, len(rareItems), dbItem.Name, dbItem.Entry) // Store original item for comparison (before any modifications) - // Create a deep copy to preserve original values + // ItemFromDbItem now creates a deep copy to preserve original values originalItem := items.ItemFromDbItem(dbItem) + // Scale original item to show actual baseline stats at original item level if originalItem.ItemLevel != nil && *originalItem.ItemLevel > 0 { originalItem.ScaleItem(*originalItem.ItemLevel, *originalItem.Quality) @@ -750,14 +1068,20 @@ func (g *MoltenCoreGenerator) GenerateItem(dbItem mysql.DbItem, validateOnly boo return result } - // Filter items by class type compatibility + // Filter items by class type AND inventory type compatibility var compatibleChoices []items.Item for _, highLevelItem := range highLevelItems { highLevelItem := items.ItemFromDbItem(highLevelItem) highLevelItem.ScaleItem(*highLevelItem.ItemLevel, *item.Quality) highClassType := highLevelItem.GetClassUserType() - if highClassType == classType { + // Check both class type and inventory type compatibility + classTypeMatches := highClassType == classType + inventoryTypeMatches := (item.InventoryType != nil && highLevelItem.InventoryType != nil && + *item.InventoryType == *highLevelItem.InventoryType) + + // Use reference item only if both class type and inventory type match + if classTypeMatches && inventoryTypeMatches { compatibleChoices = append(compatibleChoices, highLevelItem) } } @@ -765,48 +1089,141 @@ func (g *MoltenCoreGenerator) GenerateItem(dbItem mysql.DbItem, validateOnly boo if g.debug { log.Printf("Found %d compatible reference items for class type %s", len(compatibleChoices), getClassString(classType)) + + // If no compatible items found, show diagnostic info + if len(compatibleChoices) == 0 { + log.Printf("[DIAGNOSTIC] Total items returned from DB: %d", len(highLevelItems)) + log.Printf("[DIAGNOSTIC] Target item: %s - Class: %d, Subclass: %d, InventoryType: %v, ClassType: %s", + item.Name, *item.Class, *item.Subclass, getInventoryTypeString(item.InventoryType), getClassString(classType)) + + // Show details of items that were considered but rejected + for i, highLevelItem := range highLevelItems { + if i >= 5 { // Limit to first 5 items to avoid spam + log.Printf("[DIAGNOSTIC] ... and %d more items", len(highLevelItems)-5) + break + } + highLevelItem := items.ItemFromDbItem(highLevelItem) + highLevelItem.ScaleItem(*highLevelItem.ItemLevel, *item.Quality) + highClassType := highLevelItem.GetClassUserType() + + log.Printf("[DIAGNOSTIC] Rejected: %s - Class: %d, Subclass: %d, InventoryType: %v, ClassType: %s (ClassMatch: %v, InvTypeMatch: %v)", + highLevelItem.Name, *highLevelItem.Class, *highLevelItem.Subclass, + getInventoryTypeString(highLevelItem.InventoryType), getClassString(highClassType), + highClassType == classType, + (item.InventoryType != nil && highLevelItem.InventoryType != nil && *item.InventoryType == *highLevelItem.InventoryType)) + } + } } - - // If no compatible items found, try similar subclasses as fallback + + // If no compatible items found, try similar subclasses and inventory types as fallback if len(compatibleChoices) == 0 { + // First try similar subclasses with same inventory type var similarSubclasses []int if *item.Class == 2 { // Weapons similarSubclasses = getSimilarWeaponSubclasses(subclassToUse) } else if *item.Class == 4 { // Armor similarSubclasses = getSimilarArmorSubclasses(subclassToUse) } - + if g.debug && len(similarSubclasses) > 0 { log.Printf("No compatible items found for subclass %d, trying similar subclasses: %v", subclassToUse, similarSubclasses) } - - // Try each similar subclass until we find compatible items + + // Try each similar subclass with same inventory type for _, altSubclass := range similarSubclasses { altHighLevelItems, err := g.db.GetRaidPhase1Items(*item.Class, altSubclass, 0, 0) if err != nil { if g.debug { - log.Printf("Failed to get reference items for subclass %d: %v", altSubclass, err) + log.Printf("Error getting items for similar subclass %d: %v", altSubclass, err) } continue } - - // Filter these alternative items by class type compatibility - for _, highLevelItem := range altHighLevelItems { - highLevelItem := items.ItemFromDbItem(highLevelItem) - highLevelItem.ScaleItem(*highLevelItem.ItemLevel, *item.Quality) - highClassType := highLevelItem.GetClassUserType() - if highClassType == classType { - compatibleChoices = append(compatibleChoices, highLevelItem) + // Check these items for compatibility (class type AND inventory type) + for _, altHighLevelItem := range altHighLevelItems { + altHighLevelItem := items.ItemFromDbItem(altHighLevelItem) + altHighLevelItem.ScaleItem(*altHighLevelItem.ItemLevel, *item.Quality) + altHighClassType := altHighLevelItem.GetClassUserType() + + // Check both class type and inventory type compatibility + classTypeMatches := altHighClassType == classType + inventoryTypeMatches := (item.InventoryType != nil && altHighLevelItem.InventoryType != nil && + *item.InventoryType == *altHighLevelItem.InventoryType) + + if classTypeMatches && inventoryTypeMatches { + compatibleChoices = append(compatibleChoices, altHighLevelItem) } } - + if len(compatibleChoices) > 0 { if g.debug { - log.Printf("Found %d compatible fallback reference items using subclass %d for class type %s", - len(compatibleChoices), altSubclass, getClassString(classType)) + log.Printf("Found %d compatible items using similar subclass %d with same inventory type", len(compatibleChoices), altSubclass) + } + break // Found some, no need to try more subclasses + } + } + + // If still no matches, try equivalent inventory type groups + if len(compatibleChoices) == 0 { + equivalentInventoryTypes := getEquivalentInventoryTypes(item.InventoryType) + if g.debug && len(equivalentInventoryTypes) > 0 { + log.Printf("No compatible items found with same inventory type, trying equivalent inventory types: %v", equivalentInventoryTypes) + } + + // Try original subclass with equivalent inventory types + for _, equivInvType := range equivalentInventoryTypes { + for _, highLevelItem := range highLevelItems { + highLevelItem := items.ItemFromDbItem(highLevelItem) + highLevelItem.ScaleItem(*highLevelItem.ItemLevel, *item.Quality) + highClassType := highLevelItem.GetClassUserType() + + // Check class type and equivalent inventory type compatibility + classTypeMatches := highClassType == classType + inventoryTypeMatches := (highLevelItem.InventoryType != nil && + *highLevelItem.InventoryType == equivInvType) + + if classTypeMatches && inventoryTypeMatches { + compatibleChoices = append(compatibleChoices, highLevelItem) + } + } + } + + if len(compatibleChoices) > 0 { + if g.debug { + log.Printf("Found %d compatible items using equivalent inventory types", len(compatibleChoices)) + } + } else { + // Try similar subclasses with equivalent inventory types + for _, altSubclass := range similarSubclasses { + altHighLevelItems, err := g.db.GetRaidPhase1Items(*item.Class, altSubclass, 0, 0) + if err != nil { + continue + } + + for _, equivInvType := range equivalentInventoryTypes { + for _, altHighLevelItem := range altHighLevelItems { + altHighLevelItem := items.ItemFromDbItem(altHighLevelItem) + altHighLevelItem.ScaleItem(*altHighLevelItem.ItemLevel, *item.Quality) + altHighClassType := altHighLevelItem.GetClassUserType() + + // Check class type and equivalent inventory type compatibility + classTypeMatches := altHighClassType == classType + inventoryTypeMatches := (altHighLevelItem.InventoryType != nil && + *altHighLevelItem.InventoryType == equivInvType) + + if classTypeMatches && inventoryTypeMatches { + compatibleChoices = append(compatibleChoices, altHighLevelItem) + } + } + } + + if len(compatibleChoices) > 0 { + if g.debug { + log.Printf("Found %d compatible items using similar subclass %d with equivalent inventory types", len(compatibleChoices), altSubclass) + } + break // Found some, no need to try more subclasses + } } - break // Found compatible items, stop searching } } } @@ -818,6 +1235,7 @@ func (g *MoltenCoreGenerator) GenerateItem(dbItem mysql.DbItem, validateOnly boo randHighLevelItem := compatibleChoices[rand.Intn(len(compatibleChoices))] // Store reference item for comparison selectedReferenceItem = &randHighLevelItem + // Apply stats from reference item (accept the scaling) item.ApplyStats(randHighLevelItem) // Handle spell assignment: clear original spells and use reference item spells (except for trinkets) @@ -849,8 +1267,10 @@ func (g *MoltenCoreGenerator) GenerateItem(dbItem mysql.DbItem, validateOnly boo log.Printf("Cleared original spells from trinket %s (manual scaling)", item.Name) } } - result.Warnings = append(result.Warnings, "No compatible reference items found, using manual scaling") - item.ScaleItem(g.itemLevel, g.quality) + result.Warnings = append(result.Warnings, "No compatible reference items found - FLAGGED FOR MANUAL REVIEW") + result.Errors = append(result.Errors, "MANUAL REVIEW REQUIRED: No reference items available for stat scaling") + // Don't auto-generate stats - flag for manual review instead + return result } // Store reference item in result @@ -885,7 +1305,7 @@ func (g *MoltenCoreGenerator) GenerateItem(dbItem mysql.DbItem, validateOnly boo if !validateOnly { // Try to fix validation errors g.fixValidationErrors(&item, classType, validationErrors) - + // If still failing validation, try adding missing key stats fixedErrors, fixedWarnings, newScore := g.validateItemAdvanced(&item, classType) if len(fixedErrors) > 0 && classType != 7 { // Don't try to fix Generic class type @@ -1820,16 +2240,13 @@ func calculateDPS(item *items.Item) float64 { return 0.0 } - minDmg := getDamageMin(item) - maxDmg := getDamageMax(item) - delay := getWeaponDelay(item) - - if delay == 0 { + // Use the existing GetDPS method from the items module + dps, err := item.GetDPS() + if err != nil { return 0.0 } - avgDamage := float64(minDmg+maxDmg) / 2.0 - return (avgDamage * 1000.0) / float64(delay) // Convert delay from ms to seconds + return dps } // getDamageMin gets minimum damage from item @@ -1970,27 +2387,33 @@ func getWeaponSubclassName(item *items.Item) string { } // calculateOriginalArmor calculates what the armor value would be at the original item level -// using the same formula as ScaleArmor but with the original item level +// using the existing ScaleArmor method from the items module func calculateOriginalArmor(item *items.Item, originalItemLevel int) int { // Only calculate for armor items if item.Class == nil || *item.Class != 4 { return 0 } - // Need quality and subclass for the calculation - if item.Quality == nil || item.Subclass == nil { + // Create a temporary copy of the item to avoid modifying the original + tempItem := *item + if tempItem.Armor == nil { return 0 } - // Use the same modifiers as ScaleArmor - qualityModifier, qOk := config.QualityModifiers[*item.Quality] - materialModifier, mOk := config.MaterialModifiers[*item.Subclass] + // Store the current armor value + currentArmor := *tempItem.Armor - if !qOk || !mOk { - return 0 + // Use the existing ScaleArmor method to calculate armor at original item level + tempItem.ScaleArmor(originalItemLevel) + + // Get the calculated original armor value + originalArmorValue := 0 + if tempItem.Armor != nil { + originalArmorValue = *tempItem.Armor } - // Calculate armor at original item level using the same formula - originalArmorValue := math.Ceil(float64(originalItemLevel) * qualityModifier * materialModifier) - return int(originalArmorValue) + // Restore the original armor value to avoid side effects + *item.Armor = currentArmor + + return originalArmorValue } diff --git a/internal/config/modifier.go b/internal/config/modifier.go index a82eb76..68f03ee 100644 --- a/internal/config/modifier.go +++ b/internal/config/modifier.go @@ -21,7 +21,7 @@ var InvTypeModifiers = map[int]float64{ 19: 1.0, // Tabard (assuming same as Chest for simplicity) 20: 1.0, // Robe (see also Chest = 5) 21: 0.80, // Main hand - 22: 0.50, // Off Hand weapons (see also One-Hand = 13) + 22: 0.60, // Off Hand weapons (see also One-Hand = 13) 23: 0.56, // Held in Off-Hand (class = armor, not weapon even if in weapon slot) 24: 1.0, // Ammo (assuming same as Chest for simplicity) 25: 0.38, // Thrown @@ -89,14 +89,14 @@ var StatModifiers = map[int]float64{ 35: 1.0, // ITEM_MOD_RESILIENCE_RATING 36: 1.0, // ITEM_MOD_HASTE_RATING 37: 1.0, // ITEM_MOD_EXPERTISE_RATING - 38: 0.75, // ITEM_MOD_ATTACK_POWER - 39: 0.75, // ITEM_MOD_RANGED_ATTACK_POWER - 40: 0.75, // ITEM_MOD_FERAL_ATTACK_POWER (not used as of 3.3) - 41: 0.75, // ITEM_MOD_SPELL_HEALING_DONE - 42: 0.75, // ITEM_MOD_SPELL_DAMAGE_DONE + 38: 0.65, // ITEM_MOD_ATTACK_POWER + 39: 0.65, // ITEM_MOD_RANGED_ATTACK_POWER + 40: 0.65, // ITEM_MOD_FERAL_ATTACK_POWER (not used as of 3.3) + 41: 0.65, // ITEM_MOD_SPELL_HEALING_DONE + 42: 0.65, // ITEM_MOD_SPELL_DAMAGE_DONE 43: 2.5, // ITEM_MOD_MANA_REGENERATION 44: 1.0, // ITEM_MOD_ARMOR_PENETRATION_RATING - 45: 0.75, // ITEM_MOD_SPELL_POWER + 45: 0.65, // ITEM_MOD_SPELL_POWER 46: 1.0, // ITEM_MOD_HEALTH_REGEN 47: 1.8, // ITEM_MOD_SPELL_PENETRATION 48: 1.5, // ITEM_MOD_BLOCK_VALUE diff --git a/internal/items/items.go b/internal/items/items.go index 4c02670..d22590a 100644 --- a/internal/items/items.go +++ b/internal/items/items.go @@ -45,10 +45,167 @@ type StatScaleParams struct { StatValue int } -// Create a new item from the database item +// Create a new item from the database item with deep copy to prevent shared pointer issues func ItemFromDbItem(dbItem mysql.DbItem) Item { + // Create a deep copy to avoid shared pointer references + copy := dbItem + + // Create new pointers for all StatType fields + if dbItem.StatType1 != nil { + val := *dbItem.StatType1 + copy.StatType1 = &val + } + if dbItem.StatType2 != nil { + val := *dbItem.StatType2 + copy.StatType2 = &val + } + if dbItem.StatType3 != nil { + val := *dbItem.StatType3 + copy.StatType3 = &val + } + if dbItem.StatType4 != nil { + val := *dbItem.StatType4 + copy.StatType4 = &val + } + if dbItem.StatType5 != nil { + val := *dbItem.StatType5 + copy.StatType5 = &val + } + if dbItem.StatType6 != nil { + val := *dbItem.StatType6 + copy.StatType6 = &val + } + if dbItem.StatType7 != nil { + val := *dbItem.StatType7 + copy.StatType7 = &val + } + if dbItem.StatType8 != nil { + val := *dbItem.StatType8 + copy.StatType8 = &val + } + if dbItem.StatType9 != nil { + val := *dbItem.StatType9 + copy.StatType9 = &val + } + if dbItem.StatType10 != nil { + val := *dbItem.StatType10 + copy.StatType10 = &val + } + + // Create new pointers for all StatValue fields + if dbItem.StatValue1 != nil { + val := *dbItem.StatValue1 + copy.StatValue1 = &val + } + if dbItem.StatValue2 != nil { + val := *dbItem.StatValue2 + copy.StatValue2 = &val + } + if dbItem.StatValue3 != nil { + val := *dbItem.StatValue3 + copy.StatValue3 = &val + } + if dbItem.StatValue4 != nil { + val := *dbItem.StatValue4 + copy.StatValue4 = &val + } + if dbItem.StatValue5 != nil { + val := *dbItem.StatValue5 + copy.StatValue5 = &val + } + if dbItem.StatValue6 != nil { + val := *dbItem.StatValue6 + copy.StatValue6 = &val + } + if dbItem.StatValue7 != nil { + val := *dbItem.StatValue7 + copy.StatValue7 = &val + } + if dbItem.StatValue8 != nil { + val := *dbItem.StatValue8 + copy.StatValue8 = &val + } + if dbItem.StatValue9 != nil { + val := *dbItem.StatValue9 + copy.StatValue9 = &val + } + if dbItem.StatValue10 != nil { + val := *dbItem.StatValue10 + copy.StatValue10 = &val + } + + // Create new pointers for other critical fields + if dbItem.Class != nil { + val := *dbItem.Class + copy.Class = &val + } + if dbItem.Subclass != nil { + val := *dbItem.Subclass + copy.Subclass = &val + } + if dbItem.Quality != nil { + val := *dbItem.Quality + copy.Quality = &val + } + if dbItem.ItemLevel != nil { + val := *dbItem.ItemLevel + copy.ItemLevel = &val + } + if dbItem.Armor != nil { + val := *dbItem.Armor + copy.Armor = &val + } + if dbItem.FireRes != nil { + val := *dbItem.FireRes + copy.FireRes = &val + } + if dbItem.Material != nil { + val := *dbItem.Material + copy.Material = &val + } + if dbItem.InventoryType != nil { + val := *dbItem.InventoryType + copy.InventoryType = &val + } + if dbItem.MinDmg1 != nil { + val := *dbItem.MinDmg1 + copy.MinDmg1 = &val + } + if dbItem.MaxDmg1 != nil { + val := *dbItem.MaxDmg1 + copy.MaxDmg1 = &val + } + if dbItem.Delay != nil { + val := *dbItem.Delay + copy.Delay = &val + } + if dbItem.SpellId1 != nil { + val := *dbItem.SpellId1 + copy.SpellId1 = &val + } + if dbItem.SpellTrigger1 != nil { + val := *dbItem.SpellTrigger1 + copy.SpellTrigger1 = &val + } + if dbItem.SpellId2 != nil { + val := *dbItem.SpellId2 + copy.SpellId2 = &val + } + if dbItem.SpellTrigger2 != nil { + val := *dbItem.SpellTrigger2 + copy.SpellTrigger2 = &val + } + if dbItem.SpellId3 != nil { + val := *dbItem.SpellId3 + copy.SpellId3 = &val + } + if dbItem.SpellTrigger3 != nil { + val := *dbItem.SpellTrigger3 + copy.SpellTrigger3 = &val + } + return Item{ - DbItem: dbItem, + DbItem: copy, } } @@ -883,23 +1040,6 @@ func correctSpellAttackPower(item *Item, allStats map[int]*ItemStat) { * @return int **/ func (item *Item) GetClassUserType() int { - // Debug: Print item details for troubleshooting - log.Printf("[DEBUG] GetClassUserType for %s (Entry: %d)", item.Name, item.Entry) - log.Printf("[DEBUG] Class: %v, Material: %v, InventoryType: %v, Subclass: %v", - item.Class, item.Material, item.InventoryType, item.Subclass) - - // Debug: Print all stats on the item - log.Printf("[DEBUG] Item stats:") - for i := 1; i <= 7; i++ { - statTypeField := fmt.Sprintf("StatType%d", i) - statValueField := fmt.Sprintf("StatValue%d", i) - statType, _ := item.GetField(statTypeField) - statValue, _ := item.GetField(statValueField) - if statType > 0 { - log.Printf("[DEBUG] Stat%d: Type=%d, Value=%d", i, statType, statValue) - } - } - // loop over the stats and check if any of them are parry, defense, block for i := 1; i <= 7; i++ { statTypeField := fmt.Sprintf("StatType%d", i) @@ -928,17 +1068,17 @@ func (item *Item) GetClassUserType() int { // For armor we can use the type to determine the class type if *item.Class == 4 { // if the item is cloth its a mage and did not have healer stats just treat as a mage item - if *item.Material == 1 && *item.InventoryType != 16 { + if *item.Subclass == 1 && *item.InventoryType != 16 { return 4 } // If it is plate and not a tank then it is a strength melee attack - if *item.Material == 4 { + if *item.Subclass == 4 { return 1 } // If it is mail/leather armor then it is limited to Mage, Agility Fighter - if *item.Material == 2 || *item.Material == 3 { + if *item.Subclass == 2 || *item.Subclass == 3 { // check for spellpower, spellcrit, spellhit, intellect for i := 1; i <= 7; i++ { statTypeField := fmt.Sprintf("StatType%d", i) @@ -1009,6 +1149,7 @@ func (item *Item) GetClassUserType() int { for i := 1; i <= 7; i++ { statTypeField := fmt.Sprintf("StatType%d", i) statTypePtr, _ := item.GetField(statTypeField) + // fmt.Printf("itemName: %s StatType%d: %v \n", item.Name, i, statTypePtr) if statTypePtr == STAT.Spirit { return 5 } @@ -1065,7 +1206,19 @@ func (item *Item) GetClassUserType() int { } } - log.Printf("[DEBUG] GetClassUserType for %s returning: 7 (Generic - Could not determine)", item.Name) + // if we have made it here then the only thing left to do is base it purely on armor material type + if *item.Class == 4 && *item.Subclass == 1 { + return 4 + } + + if *item.Class == 4 && *item.Subclass == 4 { + return 1 + } + + if *item.Class == 4 && (*item.Subclass == 2 || *item.Subclass == 3) { + return 2 + } + return 7 }