bug: fixes to items and adds and new template for item gen.

This commit is contained in:
2025-09-16 23:38:57 -04:00
parent b80f7b314f
commit 4b6e5320d2
4 changed files with 721 additions and 102 deletions

View File

@@ -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 {

262
internal/items/templates.go Normal file
View File

@@ -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
}

View File

@@ -483,6 +483,20 @@ func (s *Spell) ForceScaleSpell(fromItemLevel int, toItemLevel int, itemQuality
// Scale Effect1
if s.EffectBasePoints1 != 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
if !skipScaling {
effectMultiplier := 1.0
// Check for attack power and spell power in the description regardless of effect type
@@ -558,6 +572,7 @@ func (s *Spell) ForceScaleSpell(fromItemLevel int, toItemLevel int, itemQuality
// Apply the scaling with the appropriate multiplier
s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) * levelRatio * qualModifier * effectMultiplier)
}
}
// Scale Effect2 with similar logic
if s.EffectBasePoints2 != 0 {
@@ -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

349
internal/spells/thematic.go Normal file
View File

@@ -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
}