diff --git a/internal/items/items.go b/internal/items/items.go index d3ad8b9..241e8eb 100644 --- a/internal/items/items.go +++ b/internal/items/items.go @@ -415,6 +415,11 @@ func (item Item) GetDPS() (float64, error) { // Scales and items dps damage numbers based on a desired item level. func (item *Item) ScaleDPS(oldLevel, level int) (float64, error) { + return item.ScaleDPSWithPhase(oldLevel, level, 0) +} + +// ScaleDPSWithPhase scales DPS with phase-based modifiers +func (item *Item) ScaleDPSWithPhase(oldLevel, level, phase int) (float64, error) { if item.ItemLevel == nil { return 0, fmt.Errorf("ItemLevel is not set") } @@ -432,7 +437,23 @@ func (item *Item) ScaleDPS(oldLevel, level int) (float64, error) { scalingFactor := math.Pow(float64(level)/float64(oldLevel), 1.012) dps := modifier * float64(level) * scalingFactor - adjDps := (dps * (*item.Delay / 1000) / 100) + + // Remove variance for weapons to let phase scaling work properly + varianceMultiplier := 1.0 + + // Add phase-based DPS modifier (higher phases = higher DPS) + // Phase 0: base DPS, Phase 1: -15%, Phase 2: -10%, Phase 3: -5%, Phase 4: base, Phase 5: +5% + phaseMultiplier := 1.0 + if phase > 0 { + // Start from -20% at phase 1 and increase by +5% per phase + phaseBonus := -0.20 + (float64(phase) * 0.05) + phaseMultiplier = 1.0 + phaseBonus + } + + adjDps := (dps * (*item.Delay / 1000) / 100) * varianceMultiplier * phaseMultiplier + + log.Printf("DPS DEBUG: phase=%d, phaseMultiplier=%.3f, baseDPS=%.3f, adjDps=%.3f", + phase, phaseMultiplier, (dps * (*item.Delay / 1000) / 100), adjDps) // Use deterministic values based on item entry instead of random values // We'll use the item entry to derive consistent min/max modifiers @@ -672,6 +693,10 @@ func (item *Item) ApplyStats(otherItem Item) (success bool, err error) { // Ceiling of ((ItemLevel * QualityModifier * ItemTypeModifier)^1.7095 * %ofStats) ^ (1/1.7095)) / StatModifier // i.e) Green Strength Helmet (((100 * 1.1 * 1.0)^1.705) * 1)^(1/1.7095) / 1.0 = 110 Strength on item func (item *Item) ScaleItem(itemLevel int, itemQuality int) (bool, error) { + return item.ScaleItemWithPhase(itemLevel, itemQuality, 0) +} + +func (item *Item) ScaleItemWithPhase(itemLevel int, itemQuality int, phase int) (bool, error) { var allSpellStats []spells.ConvItemStat if item.ItemLevel == nil { return false, errors.New("field itemLevel is not set") @@ -755,7 +780,7 @@ func (item *Item) ScaleItem(itemLevel int, itemQuality int) (bool, error) { log.Printf("Failed to get DPS: %v", err) } - dps, err := item.ScaleDPS(fromItemLevel, itemLevel) + dps, err := item.ScaleDPSWithPhase(fromItemLevel, itemLevel, phase) if err != nil { log.Printf("Failed to scale DPS: %v", err) return false, err @@ -1324,12 +1349,16 @@ func (item *Item) ApplyTierModifiers(optionalTier ...int) { tier = 0 } - // Default tier modifier is 1.0 (no modification) - tierModifier := 1.0 - // This is a necessary bonus to catch gear up from previous v2 version catchUpBonus := 1.5 + item.ApplyTierModifiersWithCatchup(tier, catchUpBonus) +} + +func (item *Item) ApplyTierModifiersWithCatchup(tier int, catchUpBonus float64) { + // Default tier modifier is 1.0 (no modification) + tierModifier := 1.0 + // If tier is valid (1-5), get the modifier from config if tier > 0 && tier <= 5 { if mod, ok := config.GearTierModifiers[tier]; ok { @@ -1370,7 +1399,7 @@ func (item *Item) ApplyTierModifiers(optionalTier ...int) { // Apply tier modifier and stat modifier newValue := int(float64(statValuePtr) * tierModifier * inverseModifier * catchUpBonus) - fmt.Printf("DEBUG: Stat %d changed from %d to %d (tier=%.2f, inverse=%.2f, catchup=%.2f)\n", + fmt.Printf("DEBUG: Stat %d changed from %d to %d (tier=%.2f, inverse=%.2f, catchup=%.2f)\n", i, statValuePtr, newValue, tierModifier, inverseModifier, catchUpBonus) // Update the item's stat value diff --git a/internal/items/templates.go b/internal/items/templates.go new file mode 100644 index 0000000..357a3de --- /dev/null +++ b/internal/items/templates.go @@ -0,0 +1,262 @@ +package items + +import ( + "fmt" + "log" + "math/rand" +) + +// StatTemplate represents a template for generating item stats +type StatTemplate struct { + Name string + RequiredStats []StatEntry // Stats that are always present + OptionalStats []StatEntry // Stats that may be present with variance + MaxOptional int // Maximum number of optional stats to add +} + +// StatEntry represents a stat type and value range +type StatEntry struct { + StatType int + BaseValue int + ValueRange int // Random variance range (0 to ValueRange-1) + Multiplier float64 // Multiplier for base stat value +} + +// StatTemplateManager handles stat template application +type StatTemplateManager struct { + debug bool +} + +// NewStatTemplateManager creates a new stat template manager +func NewStatTemplateManager(debug bool) *StatTemplateManager { + return &StatTemplateManager{debug: debug} +} + +// ApplySimpleStatTemplate applies a basic stat template to an item for phase 0 scaling +func (stm *StatTemplateManager) ApplySimpleStatTemplate(item *Item) { + if stm.debug { + log.Printf("Applying simple stat template for %s (Class: %d, Subclass: %d)", + item.Name, *item.Class, *item.Subclass) + } + + // Clear existing stats + stm.clearItemStats(item) + + // Get base stat value with variance + baseStatValue := 6 + rand.Intn(5) // Random between 6-10 for variance + + // Apply template based on item type + if *item.Class == 2 { // Weapons + stm.applyWeaponTemplate(item, baseStatValue) + } else if *item.Class == 4 { // Armor + stm.applyArmorTemplate(item, baseStatValue) + } + + // Update stats count + stm.updateStatsCount(item) + + if stm.debug { + statsCount, _ := item.GetField("StatsCount") + log.Printf("Applied simple template: %d stats for %s", statsCount, item.Name) + } +} + +// clearItemStats clears all existing stats on an item +func (stm *StatTemplateManager) clearItemStats(item *Item) { + for i := 1; i <= 10; i++ { + item.UpdateField(fmt.Sprintf("StatType%d", i), 0) + item.UpdateField(fmt.Sprintf("StatValue%d", i), 0) + } +} + +// applyWeaponTemplate applies weapon-specific stat templates +func (stm *StatTemplateManager) applyWeaponTemplate(item *Item, baseStatValue int) { + classType := item.GetClassUserType() + + // Determine weapon type + isPhysicalWeapon := stm.isPhysicalWeapon(*item.Subclass, classType) + isTankWeapon := (*item.Subclass == 6) // Shield + + if stm.debug { + log.Printf("Weapon %s (subclass %d, classType %d) determined as physical: %t", + item.Name, *item.Subclass, classType, isPhysicalWeapon) + } + + if isPhysicalWeapon { + stm.applyPhysicalWeaponTemplate(item, baseStatValue, isTankWeapon) + } else { + stm.applyCasterWeaponTemplate(item, baseStatValue, isTankWeapon) + } +} + +// isPhysicalWeapon determines if a weapon should use physical stats +func (stm *StatTemplateManager) isPhysicalWeapon(subclass, classType int) bool { + // Physical weapon subclasses + physicalSubclasses := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 13, 15, 16, 17, 18} + for _, sc := range physicalSubclasses { + if subclass == sc { + // Special case: daggers can be physical or caster + if subclass == 15 { // Dagger + // If classType is generic (7), assume physical for daggers + return classType == 7 || classType == 1 || classType == 2 + } + return true + } + } + + // Caster weapon subclasses + casterSubclasses := []int{10, 19, 20} // Staff, Wand, Fishing Pole + for _, sc := range casterSubclasses { + if subclass == sc { + return false + } + } + + return false // Default to caster if unknown +} + +// applyPhysicalWeaponTemplate applies physical DPS weapon stats +func (stm *StatTemplateManager) applyPhysicalWeaponTemplate(item *Item, baseStatValue int, isTankWeapon bool) { + statSlot := 1 + + // Primary stat (ALWAYS present) - choose based on weapon type + primaryStat := 3 // Agility default + + // Weapon-specific stat assignment + if *item.Subclass == 15 { // Dagger - ALWAYS Agility + primaryStat = 3 // Agility + } else if *item.Subclass == 0 || *item.Subclass == 1 || *item.Subclass == 8 { // Axe, Sword, Two-handed Sword + // These can be Strength or Agility, with slight preference for Strength + if rand.Intn(4) < 3 { // 75% chance for Strength + primaryStat = 4 // Strength + } else { + primaryStat = 3 // Agility + } + } else if *item.Subclass == 2 || *item.Subclass == 3 || *item.Subclass == 18 { // Bow, Gun, Crossbow + primaryStat = 3 // Agility (ranged weapons) + } else { + // Other weapons - random choice + if rand.Intn(2) == 0 { + primaryStat = 3 // Agility + } else { + primaryStat = 4 // Strength + } + } + + *item.StatType1 = primaryStat + *item.StatValue1 = baseStatValue + 3 + rand.Intn(2) // 3-4 bonus, consistent + statSlot++ + + // Attack Power (ALWAYS present for physical weapons) + *item.StatType2 = 38 // Attack Power + *item.StatValue2 = baseStatValue*4 + rand.Intn(2) // Consistent high value with small variance + statSlot++ + + // Secondary stats (VARY these) - choose 1-3 randomly + availableSecondaries := []int{32, 36, 31, 44} // Crit, Haste, Hit, Armor Pen + stm.addRandomSecondaryStats(item, availableSecondaries, baseStatValue, &statSlot, 1+rand.Intn(3)) + + // Only add stamina for tank weapons + if isTankWeapon && statSlot <= 5 { + item.UpdateField(fmt.Sprintf("StatType%d", statSlot), 7) // Stamina + item.UpdateField(fmt.Sprintf("StatValue%d", statSlot), baseStatValue+8+rand.Intn(3)) // 8-10 bonus + } +} + +// applyCasterWeaponTemplate applies caster weapon stats +func (stm *StatTemplateManager) applyCasterWeaponTemplate(item *Item, baseStatValue int, isTankWeapon bool) { + statSlot := 1 + + // Intellect (ALWAYS present for casters) + *item.StatType1 = 5 // Intellect + *item.StatValue1 = baseStatValue + 3 + rand.Intn(2) // 3-4 bonus, consistent + statSlot++ + + // Spell Power (ALWAYS present for caster weapons) + *item.StatType2 = 45 // Spell Power + *item.StatValue2 = baseStatValue*3 + rand.Intn(2) // Consistent high value with small variance + statSlot++ + + // Secondary stats (VARY these) - choose 1-3 randomly + availableSecondaries := []int{32, 36, 31, 43, 6} // Crit, Haste, Hit, MP5, Spirit + stm.addRandomSecondaryStats(item, availableSecondaries, baseStatValue, &statSlot, 1+rand.Intn(3)) + + // Only add stamina for tank weapons (rare for casters) + if isTankWeapon && statSlot <= 5 { + item.UpdateField(fmt.Sprintf("StatType%d", statSlot), 7) // Stamina + item.UpdateField(fmt.Sprintf("StatValue%d", statSlot), baseStatValue+8+rand.Intn(3)) // 8-10 bonus + } +} + +// applyArmorTemplate applies armor-specific stat templates +func (stm *StatTemplateManager) applyArmorTemplate(item *Item, baseStatValue int) { + if *item.Subclass == 0 { // Trinkets + stm.applyTrinketTemplate(item, baseStatValue) + } else { + stm.applyGenericArmorTemplate(item, baseStatValue) + } +} + +// applyTrinketTemplate applies trinket stats (minimal stats, power from spells) +func (stm *StatTemplateManager) applyTrinketTemplate(item *Item, baseStatValue int) { + *item.StatType1 = 7 // Stamina + *item.StatValue1 = baseStatValue + 3 + rand.Intn(3) // 3-5 bonus with variance +} + +// applyGenericArmorTemplate applies generic armor stats +func (stm *StatTemplateManager) applyGenericArmorTemplate(item *Item, baseStatValue int) { + classType := item.GetClassUserType() + statSlot := 1 + + // Stamina (ALWAYS present on armor) + *item.StatType1 = 7 // Stamina + *item.StatValue1 = baseStatValue + 4 + rand.Intn(2) // 4-5 bonus, consistent + statSlot++ + + // Primary stat (ALWAYS present) based on class type + primaryStat := 5 // Default to Intellect + if classType == 1 { // Strength + primaryStat = 4 // Strength + } else if classType == 2 { // Agility + primaryStat = 3 // Agility + } else if classType == 7 { // Generic/Unknown - random choice + primaryStats := []int{3, 4, 5} // Agi, Str, Int + primaryStat = primaryStats[rand.Intn(len(primaryStats))] + } + // classType 3,4,5,6 (casters) get Intellect by default + + *item.StatType2 = primaryStat + *item.StatValue2 = baseStatValue + 2 + rand.Intn(2) // 2-3 bonus, consistent + statSlot++ + + // Secondary stats (VARY these) - choose 0-2 randomly + availableSecondaries := []int{32, 36, 31, 6} // Crit, Haste, Hit, Spirit + stm.addRandomSecondaryStats(item, availableSecondaries, baseStatValue-3, &statSlot, rand.Intn(3)) +} + +// addRandomSecondaryStats adds random secondary stats to an item +func (stm *StatTemplateManager) addRandomSecondaryStats(item *Item, availableStats []int, baseValue int, statSlot *int, count int) { + // Shuffle available stats + rand.Shuffle(len(availableStats), func(i, j int) { + availableStats[i], availableStats[j] = availableStats[j], availableStats[i] + }) + + // Add the specified number of secondary stats + for i := 0; i < count && i < len(availableStats) && *statSlot <= 5; i++ { + item.UpdateField(fmt.Sprintf("StatType%d", *statSlot), availableStats[i]) + item.UpdateField(fmt.Sprintf("StatValue%d", *statSlot), baseValue+rand.Intn(4)) // 0 to +3 variance + (*statSlot)++ + } +} + +// updateStatsCount updates the StatsCount field based on non-zero stats +func (stm *StatTemplateManager) updateStatsCount(item *Item) { + statsCount := 0 + for i := 1; i <= 10; i++ { + statType, _ := item.GetField(fmt.Sprintf("StatType%d", i)) + if statType > 0 { + statsCount++ + } + } + *item.StatsCount = statsCount +} diff --git a/internal/spells/spells.go b/internal/spells/spells.go index e358581..b1cef17 100644 --- a/internal/spells/spells.go +++ b/internal/spells/spells.go @@ -483,80 +483,95 @@ func (s *Spell) ForceScaleSpell(fromItemLevel int, toItemLevel int, itemQuality // Scale Effect1 if s.EffectBasePoints1 != 0 { - effectMultiplier := 1.0 + // Skip scaling for percentage-based effects (buffs/debuffs) + // Look for percentage indicators in spell description + skipScaling := strings.Contains(s.Description, "$s1%") || + strings.Contains(s.Description, "$s1\\%") || + strings.Contains(s.Description, "by $s1%") || + // Common percentage-based aura effects that should not scale + (s.EffectAura1 != 0 && ( + s.EffectAura1 == 33 || // Modify Movement Speed + s.EffectAura1 == 31 || // Modify Stat (percentage) + s.EffectAura1 == 52 || // Modify Damage Done (percentage) + s.EffectAura1 == 79 || // Modify Resistance (percentage) + s.EffectAura1 == 137)) // Modify Total Stat Percentage - // Check for attack power and spell power in the description regardless of effect type - if strings.Contains(s.Description, "attack power") || - strings.Contains(s.Description, "Attack Power") || - strings.Contains(s.Description, "spell power") || - strings.Contains(s.Description, "Spell Power") || - strings.Contains(s.Description, "healing") || - strings.Contains(s.Description, "Healing") { - effectMultiplier = 2.0 // Higher multiplier for attack/spell power - } + if !skipScaling { + effectMultiplier := 1.0 - // Determine effect category and apply appropriate multiplier - if s.Effect1 != 0 { - // Direct damage effects scale more aggressively at higher item levels - if funk.Contains(directDamageEffects, s.Effect1) { - effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) // +0.5% per every 10 item levels + // Check for attack power and spell power in the description regardless of effect type + if strings.Contains(s.Description, "attack power") || + strings.Contains(s.Description, "Attack Power") || + strings.Contains(s.Description, "spell power") || + strings.Contains(s.Description, "Spell Power") || + strings.Contains(s.Description, "healing") || + strings.Contains(s.Description, "Healing") { + effectMultiplier = 2.0 // Higher multiplier for attack/spell power } - // Flat Base Stat modifier for all other stats. - if funk.Contains(statBuffEffects, s.Effect1) && effectMultiplier < 1.5 { - effectMultiplier = 1.45 - } - } + // Determine effect category and apply appropriate multiplier + if s.Effect1 != 0 { + // Direct damage effects scale more aggressively at higher item levels + if funk.Contains(directDamageEffects, s.Effect1) { + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) // +0.5% per every 10 item levels + } - // Special handling for aura effects - if s.EffectAura1 != 0 { - // DOT effects (Aura 3: Periodic Damage) - if s.EffectAura1 == 3 && funk.Contains(periodEffects, s.Effect1) { - effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) - - // Scale DOTs more with higher quality items - if itemQuality >= 5 { // Legendary or higher - effectMultiplier += 0.5 + // Flat Base Stat modifier for all other stats. + if funk.Contains(statBuffEffects, s.Effect1) && effectMultiplier < 1.5 { + effectMultiplier = 1.45 } } - // HOT effects (Aura 8: Periodic Heal) - if s.EffectAura1 == 8 && funk.Contains(periodEffects, s.Effect1) { - effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) - // Healing scales slightly higher than damage - } + // Special handling for aura effects + if s.EffectAura1 != 0 { + // DOT effects (Aura 3: Periodic Damage) + if s.EffectAura1 == 3 && funk.Contains(periodEffects, s.Effect1) { + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) - // Damage Shield effects (Aura 15) - if s.EffectAura1 == 15 && funk.Contains(periodEffects, s.Effect1) { - // Damage shields scale with item level difference - effectMultiplier = 1.5 + (float64(toItemLevel) * 0.1 * 0.005) - if effectMultiplier > 2.5 { - effectMultiplier = 2.5 // Cap at 2.5x + // Scale DOTs more with higher quality items + if itemQuality >= 5 { // Legendary or higher + effectMultiplier += 0.5 + } + } + + // HOT effects (Aura 8: Periodic Heal) + if s.EffectAura1 == 8 && funk.Contains(periodEffects, s.Effect1) { + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) + // Healing scales slightly higher than damage + } + + // Damage Shield effects (Aura 15) + if s.EffectAura1 == 15 && funk.Contains(periodEffects, s.Effect1) { + // Damage shields scale with item level difference + effectMultiplier = 1.5 + (float64(toItemLevel) * 0.1 * 0.005) + if effectMultiplier > 2.5 { + effectMultiplier = 2.5 // Cap at 2.5x + } + } + + // Proc chance effects (various auras) + if s.ProcChance > 0 && s.ProcChance < 100 { + // For proc effects, we might want to scale the effect more aggressively + // since they don't happen all the time + procFactor := 100.0 / float64(s.ProcChance) // Inverse of proc chance + // Limit the proc factor to avoid excessive scaling + if procFactor > 2.0 { + procFactor = 2.0 + } + effectMultiplier += math.Sqrt(procFactor) // Scale by square root of proc factor } } - // Proc chance effects (various auras) - if s.ProcChance > 0 && s.ProcChance < 100 { - // For proc effects, we might want to scale the effect more aggressively - // since they don't happen all the time - procFactor := 100.0 / float64(s.ProcChance) // Inverse of proc chance - // Limit the proc factor to avoid excessive scaling - if procFactor > 2.0 { - procFactor = 2.0 + // Special handling for mana restoration + if s.Effect1 == 30 { + if strings.Contains(s.Description, "Mana") || strings.Contains(s.Description, "mana") { + // Mana effects scale with level but with diminishing returns + effectMultiplier = 1.0 + (math.Log10(float64(ilevelDiff+1)) * 0.3) } - effectMultiplier += math.Sqrt(procFactor) // Scale by square root of proc factor } + // Apply the scaling with the appropriate multiplier + s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) * levelRatio * qualModifier * effectMultiplier) } - - // Special handling for mana restoration - if s.Effect1 == 30 { - if strings.Contains(s.Description, "Mana") || strings.Contains(s.Description, "mana") { - // Mana effects scale with level but with diminishing returns - effectMultiplier = 1.0 + (math.Log10(float64(ilevelDiff+1)) * 0.3) - } - } - // Apply the scaling with the appropriate multiplier - s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) * levelRatio * qualModifier * effectMultiplier) } // Scale Effect2 with similar logic @@ -851,43 +866,7 @@ func SpellToSql(spell Spell, quality int) string { EffectChainAmplitude_1, EffectChainAmplitude_2, EffectChainAmplitude_3, MinFactionID, MinReputation, RequiredAuraVision, RequiredTotemCategoryID_1, RequiredTotemCategoryID_2, RequiredAreasID, SchoolMask, RuneCostID, SpellMissileID, PowerDisplayID, EffectBonusMultiplier_1, EffectBonusMultiplier_2, EffectBonusMultiplier_3, SpellDescriptionVariableID, SpellDifficultyID - ) SELECT - ID + %v, Category, DispelType, Mechanic, Attributes, AttributesEx, AttributesEx2, AttributesEx3, AttributesEx4, - AttributesEx5, AttributesEx6, AttributesEx7, ShapeshiftMask, unk_320_2, ShapeshiftExclude, unk_320_3, Targets, - TargetCreatureType, RequiresSpellFocus, FacingCasterFlags, CasterAuraState, TargetAuraState, ExcludeCasterAuraState, - ExcludeTargetAuraState, CasterAuraSpell, TargetAuraSpell, ExcludeCasterAuraSpell, ExcludeTargetAuraSpell, CastingTimeIndex, - RecoveryTime, CategoryRecoveryTime, InterruptFlags, AuraInterruptFlags, ChannelInterruptFlags, ProcTypeMask, ProcChance, - ProcCharges, MaxLevel, BaseLevel, SpellLevel, DurationIndex, PowerType, ManaCost, ManaCostPerLevel, ManaPerSecond, - ManaPerSecondPerLevel, RangeIndex, Speed, ModalNextSpell, CumulativeAura, Totem_1, Totem_2, Reagent_1, Reagent_2, Reagent_3, - Reagent_4, Reagent_5, Reagent_6, Reagent_7, Reagent_8, ReagentCount_1, ReagentCount_2, ReagentCount_3, ReagentCount_4, - ReagentCount_5, ReagentCount_6, ReagentCount_7, ReagentCount_8, EquippedItemClass, EquippedItemSubclass, EquippedItemInvTypes, - Effect_1, Effect_2, Effect_3, EffectDieSides_1, EffectDieSides_2, EffectDieSides_3, EffectRealPointsPerLevel_1, - EffectRealPointsPerLevel_2, EffectRealPointsPerLevel_3, EffectBasePoints_1, EffectBasePoints_2, EffectBasePoints_3, - EffectMechanic_1, EffectMechanic_2, EffectMechanic_3, ImplicitTargetA_1, ImplicitTargetA_2, ImplicitTargetA_3, ImplicitTargetB_1, - ImplicitTargetB_2, ImplicitTargetB_3, EffectRadiusIndex_1, EffectRadiusIndex_2, EffectRadiusIndex_3, EffectAura_1, - EffectAura_2, EffectAura_3, EffectAuraPeriod_1, EffectAuraPeriod_2, EffectAuraPeriod_3, EffectMultipleValue_1, EffectMultipleValue_2, - EffectMultipleValue_3, EffectChainTargets_1, EffectChainTargets_2, EffectChainTargets_3, EffectItemType_1, EffectItemType_2, - EffectItemType_3, EffectMiscValue_1, EffectMiscValue_2, EffectMiscValue_3, EffectMiscValueB_1, EffectMiscValueB_2, EffectMiscValueB_3, - EffectTriggerSpell_1, EffectTriggerSpell_2, EffectTriggerSpell_3, EffectPointsPerCombo_1, EffectPointsPerCombo_2, EffectPointsPerCombo_3, - EffectSpellClassMaskA_1, EffectSpellClassMaskA_2, EffectSpellClassMaskA_3, EffectSpellClassMaskB_1, EffectSpellClassMaskB_2, - EffectSpellClassMaskB_3, EffectSpellClassMaskC_1, EffectSpellClassMaskC_2, EffectSpellClassMaskC_3, SpellVisualID_1, SpellVisualID_2, - SpellIconID, ActiveIconID, SpellPriority, Name_Lang_enUS, Name_Lang_enGB, Name_Lang_koKR, Name_Lang_frFR, Name_Lang_deDE, - Name_Lang_enCN, Name_Lang_zhCN, Name_Lang_enTW, Name_Lang_zhTW, Name_Lang_esES, Name_Lang_esMX, Name_Lang_ruRU, Name_Lang_ptPT, - Name_Lang_ptBR, Name_Lang_itIT, Name_Lang_Unk, Name_Lang_Mask, NameSubtext_Lang_enUS, NameSubtext_Lang_enGB, NameSubtext_Lang_koKR, - NameSubtext_Lang_frFR, NameSubtext_Lang_deDE, NameSubtext_Lang_enCN, NameSubtext_Lang_zhCN, NameSubtext_Lang_enTW, NameSubtext_Lang_zhTW, - NameSubtext_Lang_esES, NameSubtext_Lang_esMX, NameSubtext_Lang_ruRU, NameSubtext_Lang_ptPT, NameSubtext_Lang_ptBR, NameSubtext_Lang_itIT, - NameSubtext_Lang_Unk, NameSubtext_Lang_Mask, Description_Lang_enUS, Description_Lang_enGB, Description_Lang_koKR, Description_Lang_frFR, - Description_Lang_deDE, Description_Lang_enCN, Description_Lang_zhCN, Description_Lang_enTW, Description_Lang_zhTW, Description_Lang_esES, - Description_Lang_esMX, Description_Lang_ruRU, Description_Lang_ptPT, Description_Lang_ptBR, Description_Lang_itIT, Description_Lang_Unk, - Description_Lang_Mask, AuraDescription_Lang_enUS, AuraDescription_Lang_enGB, AuraDescription_Lang_koKR, AuraDescription_Lang_frFR, - AuraDescription_Lang_deDE, AuraDescription_Lang_enCN, AuraDescription_Lang_zhCN, AuraDescription_Lang_enTW, AuraDescription_Lang_zhTW, - AuraDescription_Lang_esES, AuraDescription_Lang_esMX, AuraDescription_Lang_ruRU, AuraDescription_Lang_ptPT, AuraDescription_Lang_ptBR, - AuraDescription_Lang_itIT, AuraDescription_Lang_Unk, AuraDescription_Lang_Mask, ManaCostPct, StartRecoveryCategory, StartRecoveryTime, - MaxTargetLevel, SpellClassSet, SpellClassMask_1, SpellClassMask_2, SpellClassMask_3, MaxTargets, DefenseType, PreventionType, StanceBarOrder, - EffectChainAmplitude_1, EffectChainAmplitude_2, EffectChainAmplitude_3, MinFactionID, MinReputation, RequiredAuraVision, RequiredTotemCategoryID_1, - RequiredTotemCategoryID_2, RequiredAreasID, SchoolMask, RuneCostID, SpellMissileID, PowerDisplayID, EffectBonusMultiplier_1, EffectBonusMultiplier_2, - EffectBonusMultiplier_3, SpellDescriptionVariableID, SpellDifficultyID from acore_world.spell_dbc as src - WHERE src.ID = %v ON DUPLICATE KEY UPDATE ID = src.ID + %v;`, entryBump, spell.ID, entryBump) + from spell_dbc as src WHERE src.ID = %v;`, entryBump, spell.ID) update := fmt.Sprintf(` UPDATE acore_world.spell_dbc diff --git a/internal/spells/thematic.go b/internal/spells/thematic.go new file mode 100644 index 0000000..1ed7ec6 --- /dev/null +++ b/internal/spells/thematic.go @@ -0,0 +1,349 @@ +package spells + +import ( + "fmt" + "log" + "strings" + + "github.com/araxiaonline/endgame-item-generator/internal/db/mysql" +) + +// SpellTheme represents different magical themes for spell assignment +type SpellTheme int + +const ( + ThemeNone SpellTheme = iota + ThemeFrost + ThemeFire + ThemeNature + ThemeShadow + ThemeArcane + ThemeHoly +) + +// String returns the string representation of a SpellTheme +func (t SpellTheme) String() string { + switch t { + case ThemeFrost: + return "Frost" + case ThemeFire: + return "Fire" + case ThemeNature: + return "Nature" + case ThemeShadow: + return "Shadow" + case ThemeArcane: + return "Arcane" + case ThemeHoly: + return "Holy" + default: + return "None" + } +} + +// ThematicSpellAssigner handles assignment of thematic spells to items +type ThematicSpellAssigner struct { + db *mysql.MySqlDb +} + +// NewThematicSpellAssigner creates a new thematic spell assigner +func NewThematicSpellAssigner(db *mysql.MySqlDb) *ThematicSpellAssigner { + return &ThematicSpellAssigner{db: db} +} + +// ShouldHaveProc determines if an item should have a proc spell based on its properties +func (tsa *ThematicSpellAssigner) ShouldHaveProc(itemClass, itemSubclass, itemLevel, quality int, itemName string) bool { + log.Printf("DEBUG: ShouldHaveProc - Class: %d, Subclass: %d, Level: %d, Quality: %d, Name: %s", + itemClass, itemSubclass, itemLevel, quality, itemName) + + // Weapons should have procs + if itemClass == 2 { + log.Printf("DEBUG: Item is a weapon (class 2)") + // Higher quality items more likely to have procs + if quality >= 3 && itemLevel >= 40 { + log.Printf("DEBUG: Weapon qualifies - quality %d >= 3 and level %d >= 40", quality, itemLevel) + return true + } else { + log.Printf("DEBUG: Weapon does not qualify - quality %d < 3 or level %d < 40", quality, itemLevel) + } + } + + // Trinkets should have procs + if itemClass == 4 && itemSubclass == 0 { + log.Printf("DEBUG: Item is a trinket - qualifies for proc") + return true + } + + // Jewelry with thematic names + if itemClass == 4 && (itemSubclass == 1 || itemSubclass == 2) { // Neck, Ring + theme := tsa.DetectThemeFromName(itemName) + if theme != ThemeNone { + log.Printf("DEBUG: Jewelry with theme %s - qualifies for proc", theme.String()) + return true + } + } + + log.Printf("DEBUG: Item does not qualify for proc") + return false +} + +// DetectThemeFromName analyzes item name and description to determine magical theme +func (tsa *ThematicSpellAssigner) DetectThemeFromName(itemName string) SpellTheme { + name := strings.ToLower(itemName) + + // Frost theme keywords - spells like Frostbolt, Frost Nova, Ice Lance + frostKeywords := []string{ + "frost", "cold", "ice", "frozen", "chill", "winter", "arctic", "glacial", + "coldrage", "frostbite", "icicle", "blizzard", "freeze", "shiver", "crystal", + "permafrost", "hoarfrost", "rimefang", "icecrown", "northrend", "wintergrasp", + "frostmourne", "sindragosa", "arthas", "lich", "undead", "scourge", + } + + // Fire theme keywords - spells like Fireball, Flame Strike, Pyroblast + fireKeywords := []string{ + "fire", "flame", "burn", "ember", "inferno", "molten", "volcanic", + "blazing", "scorching", "flaming", "ignite", "pyre", "magma", "lava", + "sulfuron", "ragnaros", "flamewaker", "firelands", "phoenix", "immolate", + "conflagrate", "incinerate", "combustion", "pyroblast", "scorch", + } + + // Nature theme keywords - spells like Lightning Bolt, Earth Shock, Entangling Roots + natureKeywords := []string{ + "nature", "earth", "storm", "lightning", "thunder", "wind", "leaf", + "thorn", "root", "growth", "poison", "venom", "toxic", "acid", "druid", + "shaman", "elemental", "totem", "spirit", "wolf", "bear", "cat", "tree", + "hurricane", "typhoon", "earthquake", "rockbiter", "windfury", "stormstrike", + "maelstrom", "chain", "healing", "rejuvenation", "regrowth", "lifebloom", + } + + // Shadow theme keywords - spells like Shadow Bolt, Drain Life, Fear + shadowKeywords := []string{ + "shadow", "dark", "void", "curse", "doom", "death", "soul", "spirit", + "nightmare", "corruption", "plague", "blight", "necro", "warlock", "priest", + "undead", "bone", "skull", "grave", "tomb", "crypt", "wraith", "specter", + "banshee", "lich", "drain", "siphon", "vampiric", "life", "mana", "burn", + "fear", "horror", "terror", "agony", "torment", "suffering", "pain", + } + + // Arcane theme keywords - spells like Arcane Missiles, Polymorph, Counterspell + arcaneKeywords := []string{ + "arcane", "magic", "mystic", "enchant", "spell", "mana", "ethereal", + "astral", "cosmic", "temporal", "dimensional", "mage", "wizard", "sorcerer", + "intellect", "wisdom", "knowledge", "study", "tome", "grimoire", "staff", + "orb", "crystal", "gem", "power", "energy", "force", "teleport", "portal", + "polymorph", "counterspell", "dispel", "silence", "interrupt", + } + + // Holy theme keywords - spells like Heal, Flash of Light, Consecration + holyKeywords := []string{ + "holy", "divine", "blessed", "sacred", "light", "radiant", "celestial", + "righteous", "pure", "sanctified", "hallowed", "paladin", "priest", "cleric", + "angel", "seraph", "cherub", "heaven", "paradise", "salvation", "redemption", + "consecration", "blessing", "grace", "mercy", "faith", "devotion", "prayer", + "heal", "cure", "restore", "renew", "resurrect", "revive", "sanctuary", + } + + // Check each theme + for _, keyword := range frostKeywords { + if strings.Contains(name, keyword) { + return ThemeFrost + } + } + + for _, keyword := range fireKeywords { + if strings.Contains(name, keyword) { + return ThemeFire + } + } + + for _, keyword := range natureKeywords { + if strings.Contains(name, keyword) { + return ThemeNature + } + } + + for _, keyword := range shadowKeywords { + if strings.Contains(name, keyword) { + return ThemeShadow + } + } + + for _, keyword := range arcaneKeywords { + if strings.Contains(name, keyword) { + return ThemeArcane + } + } + + for _, keyword := range holyKeywords { + if strings.Contains(name, keyword) { + return ThemeHoly + } + } + + return ThemeNone +} + +// FindThematicSpell finds an appropriate spell for the given theme +func (tsa *ThematicSpellAssigner) FindThematicSpell(theme SpellTheme, itemClass, itemSubclass int) (int, error) { + log.Printf("Finding thematic spell for theme: %s, class: %d, subclass: %d", theme.String(), itemClass, itemSubclass) + + // First, try to find existing scaled spells (31000000+) + scaledSpellId, err := tsa.findScaledThematicSpell(theme, itemClass, itemSubclass) + if err == nil && scaledSpellId > 0 { + log.Printf("Found existing scaled spell: %d for theme %s", scaledSpellId, theme.String()) + return scaledSpellId, nil + } + + // If no scaled spell found, look in spell_dbc for base spells + baseSpellId, err := tsa.findBaseThematicSpell(theme, itemClass, itemSubclass) + if err == nil && baseSpellId > 0 { + log.Printf("Found base spell: %d for theme %s", baseSpellId, theme.String()) + return baseSpellId, nil + } + + return 0, fmt.Errorf("no suitable spell found for theme %s", theme.String()) +} + +// findScaledThematicSpell looks for existing scaled spells (31000000+) that match the theme +func (tsa *ThematicSpellAssigner) findScaledThematicSpell(theme SpellTheme, itemClass, itemSubclass int) (int, error) { + // Define spell pools for each theme (existing scaled spells starting from 31000000) + // These would be spells that have already been scaled by the system + scaledSpellPools := map[SpellTheme][]int{ + ThemeFrost: { + // Start with high-priority frost spells for weapons/trinkets + 31013439, // Scaled Frostbolt (damage + slow) + 31012737, // Scaled Frost Armor (defensive) + 31021401, // Scaled Frost Nova (AoE slow) + }, + ThemeFire: { + // Fire damage spells + 31013140, // Scaled Fireball (direct damage) + 31008413, // Scaled Fire Nova (AoE damage) + 31011564, // Scaled Flame Strike (ground effect) + }, + ThemeNature: { + // Nature/Lightning spells + 31000421, // Scaled Lightning Bolt (instant damage) + 31008050, // Scaled Earth Shock (damage + interrupt) + 31010414, // Scaled Entangling Roots (root effect) + }, + ThemeShadow: { + // Shadow/Dark magic spells + 31000980, // Scaled Shadow Bolt (shadow damage) + 31007648, // Scaled Drain Life (damage + heal) + 31011671, // Scaled Shadow Word: Pain (DoT) + }, + ThemeArcane: { + // Arcane magic spells + 31005143, // Scaled Arcane Missiles (channeled damage) + 31001953, // Scaled Blink (teleport) + 31000118, // Scaled Polymorph (crowd control) + }, + ThemeHoly: { + // Holy/Light spells + 31000139, // Scaled Heal (direct healing) + 31010060, // Scaled Flash of Light (fast heal) + 31001244, // Scaled Divine Favor (buff) + }, + } + + spells, exists := scaledSpellPools[theme] + if !exists || len(spells) == 0 { + return 0, fmt.Errorf("no scaled spells available for theme %s", theme.String()) + } + + // For weapons, prefer damage spells; for trinkets/jewelry, prefer utility + if itemClass == 2 { // Weapons + // Return first damage spell (they're ordered by preference) + return spells[0], nil + } + + // For trinkets/jewelry, prefer utility spells (later in list) + if len(spells) > 1 { + return spells[len(spells)-1], nil + } + + return spells[0], nil +} + +// findBaseThematicSpell looks in spell_dbc for base spells that match the theme +func (tsa *ThematicSpellAssigner) findBaseThematicSpell(theme SpellTheme, itemClass, itemSubclass int) (int, error) { + // Define base spell pools for each theme + baseSpellPools := map[SpellTheme][]int{ + ThemeFrost: { + 13439, // Frostbolt + 21401, // Frost Nova + 12737, // Frost Armor + 11113, // Blast Wave (Frost) + }, + ThemeFire: { + 13140, // Fireball + 8413, // Fire Nova + 11564, // Flame Strike + 11129, // Combustion + }, + ThemeNature: { + 421, // Lightning Bolt + 8050, // Earth Shock + 10414, // Entangling Roots + 16689, // Nature's Grasp + }, + ThemeShadow: { + 980, // Shadow Bolt + 7648, // Drain Life + 11671, // Shadow Word: Pain + 18265, // Siphon Soul + }, + ThemeArcane: { + 5143, // Arcane Missiles + 1953, // Blink + 118, // Polymorph + 12051, // Evocation + }, + ThemeHoly: { + 139, // Heal + 1244, // Divine Favor + 10060, // Flash of Light + 10308, // Hammer of Justice + }, + } + + spells, exists := baseSpellPools[theme] + if !exists || len(spells) == 0 { + return 0, fmt.Errorf("no base spells available for theme %s", theme.String()) + } + + // Verify spell exists in database + for _, spellId := range spells { + _, err := tsa.db.GetSpell(spellId) + if err == nil { + return spellId, nil + } + } + + return 0, fmt.Errorf("no valid base spells found for theme %s", theme.String()) +} + +// AssignThematicSpell assigns a thematic spell to an item if appropriate +func (tsa *ThematicSpellAssigner) AssignThematicSpell(itemEntry, itemClass, itemSubclass, itemLevel, quality int, itemName string) (int, error) { + // Check if item should have a proc + if !tsa.ShouldHaveProc(itemClass, itemSubclass, itemLevel, quality, itemName) { + return 0, fmt.Errorf("item does not qualify for proc assignment") + } + + // Detect theme from name + theme := tsa.DetectThemeFromName(itemName) + if theme == ThemeNone { + // For weapons without clear theme, don't assign a spell + return 0, fmt.Errorf("no clear theme detected for item: %s", itemName) + } + + // Find appropriate spell + spellId, err := tsa.FindThematicSpell(theme, itemClass, itemSubclass) + if err != nil { + return 0, fmt.Errorf("failed to find thematic spell: %v", err) + } + + log.Printf("Assigned spell %d (theme: %s) to item %s (%d)", spellId, theme.String(), itemName, itemEntry) + return spellId, nil +}