mirror of
https://github.com/araxiaonline/wow-item-generator.git
synced 2026-06-13 03:02:22 -04:00
bug: fixes to items and adds and new template for item gen.
This commit is contained in:
@@ -415,6 +415,11 @@ func (item Item) GetDPS() (float64, error) {
|
|||||||
|
|
||||||
// Scales and items dps damage numbers based on a desired item level.
|
// Scales and items dps damage numbers based on a desired item level.
|
||||||
func (item *Item) ScaleDPS(oldLevel, level int) (float64, error) {
|
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 {
|
if item.ItemLevel == nil {
|
||||||
return 0, fmt.Errorf("ItemLevel is not set")
|
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)
|
scalingFactor := math.Pow(float64(level)/float64(oldLevel), 1.012)
|
||||||
|
|
||||||
dps := modifier * float64(level) * scalingFactor
|
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
|
// Use deterministic values based on item entry instead of random values
|
||||||
// We'll use the item entry to derive consistent min/max modifiers
|
// 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
|
// 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
|
// 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) {
|
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
|
var allSpellStats []spells.ConvItemStat
|
||||||
if item.ItemLevel == nil {
|
if item.ItemLevel == nil {
|
||||||
return false, errors.New("field itemLevel is not set")
|
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)
|
log.Printf("Failed to get DPS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dps, err := item.ScaleDPS(fromItemLevel, itemLevel)
|
dps, err := item.ScaleDPSWithPhase(fromItemLevel, itemLevel, phase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to scale DPS: %v", err)
|
log.Printf("Failed to scale DPS: %v", err)
|
||||||
return false, err
|
return false, err
|
||||||
@@ -1324,12 +1349,16 @@ func (item *Item) ApplyTierModifiers(optionalTier ...int) {
|
|||||||
tier = 0
|
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
|
// This is a necessary bonus to catch gear up from previous v2 version
|
||||||
catchUpBonus := 1.5
|
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 is valid (1-5), get the modifier from config
|
||||||
if tier > 0 && tier <= 5 {
|
if tier > 0 && tier <= 5 {
|
||||||
if mod, ok := config.GearTierModifiers[tier]; ok {
|
if mod, ok := config.GearTierModifiers[tier]; ok {
|
||||||
@@ -1370,7 +1399,7 @@ func (item *Item) ApplyTierModifiers(optionalTier ...int) {
|
|||||||
// Apply tier modifier and stat modifier
|
// Apply tier modifier and stat modifier
|
||||||
newValue := int(float64(statValuePtr) * tierModifier * inverseModifier * catchUpBonus)
|
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)
|
i, statValuePtr, newValue, tierModifier, inverseModifier, catchUpBonus)
|
||||||
|
|
||||||
// Update the item's stat value
|
// Update the item's stat value
|
||||||
|
|||||||
262
internal/items/templates.go
Normal file
262
internal/items/templates.go
Normal 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
|
||||||
|
}
|
||||||
@@ -483,80 +483,95 @@ func (s *Spell) ForceScaleSpell(fromItemLevel int, toItemLevel int, itemQuality
|
|||||||
|
|
||||||
// Scale Effect1
|
// Scale Effect1
|
||||||
if s.EffectBasePoints1 != 0 {
|
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 !skipScaling {
|
||||||
if strings.Contains(s.Description, "attack power") ||
|
effectMultiplier := 1.0
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine effect category and apply appropriate multiplier
|
// Check for attack power and spell power in the description regardless of effect type
|
||||||
if s.Effect1 != 0 {
|
if strings.Contains(s.Description, "attack power") ||
|
||||||
// Direct damage effects scale more aggressively at higher item levels
|
strings.Contains(s.Description, "Attack Power") ||
|
||||||
if funk.Contains(directDamageEffects, s.Effect1) {
|
strings.Contains(s.Description, "spell power") ||
|
||||||
effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) // +0.5% per every 10 item levels
|
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.
|
// Determine effect category and apply appropriate multiplier
|
||||||
if funk.Contains(statBuffEffects, s.Effect1) && effectMultiplier < 1.5 {
|
if s.Effect1 != 0 {
|
||||||
effectMultiplier = 1.45
|
// 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
|
// Flat Base Stat modifier for all other stats.
|
||||||
if s.EffectAura1 != 0 {
|
if funk.Contains(statBuffEffects, s.Effect1) && effectMultiplier < 1.5 {
|
||||||
// DOT effects (Aura 3: Periodic Damage)
|
effectMultiplier = 1.45
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HOT effects (Aura 8: Periodic Heal)
|
// Special handling for aura effects
|
||||||
if s.EffectAura1 == 8 && funk.Contains(periodEffects, s.Effect1) {
|
if s.EffectAura1 != 0 {
|
||||||
effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005)
|
// DOT effects (Aura 3: Periodic Damage)
|
||||||
// Healing scales slightly higher than damage
|
if s.EffectAura1 == 3 && funk.Contains(periodEffects, s.Effect1) {
|
||||||
}
|
effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005)
|
||||||
|
|
||||||
// Damage Shield effects (Aura 15)
|
// Scale DOTs more with higher quality items
|
||||||
if s.EffectAura1 == 15 && funk.Contains(periodEffects, s.Effect1) {
|
if itemQuality >= 5 { // Legendary or higher
|
||||||
// Damage shields scale with item level difference
|
effectMultiplier += 0.5
|
||||||
effectMultiplier = 1.5 + (float64(toItemLevel) * 0.1 * 0.005)
|
}
|
||||||
if effectMultiplier > 2.5 {
|
}
|
||||||
effectMultiplier = 2.5 // Cap at 2.5x
|
|
||||||
|
// 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)
|
// Special handling for mana restoration
|
||||||
if s.ProcChance > 0 && s.ProcChance < 100 {
|
if s.Effect1 == 30 {
|
||||||
// For proc effects, we might want to scale the effect more aggressively
|
if strings.Contains(s.Description, "Mana") || strings.Contains(s.Description, "mana") {
|
||||||
// since they don't happen all the time
|
// Mana effects scale with level but with diminishing returns
|
||||||
procFactor := 100.0 / float64(s.ProcChance) // Inverse of proc chance
|
effectMultiplier = 1.0 + (math.Log10(float64(ilevelDiff+1)) * 0.3)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
// 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
|
// 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,
|
EffectChainAmplitude_1, EffectChainAmplitude_2, EffectChainAmplitude_3, MinFactionID, MinReputation, RequiredAuraVision, RequiredTotemCategoryID_1,
|
||||||
RequiredTotemCategoryID_2, RequiredAreasID, SchoolMask, RuneCostID, SpellMissileID, PowerDisplayID, EffectBonusMultiplier_1, EffectBonusMultiplier_2,
|
RequiredTotemCategoryID_2, RequiredAreasID, SchoolMask, RuneCostID, SpellMissileID, PowerDisplayID, EffectBonusMultiplier_1, EffectBonusMultiplier_2,
|
||||||
EffectBonusMultiplier_3, SpellDescriptionVariableID, SpellDifficultyID
|
EffectBonusMultiplier_3, SpellDescriptionVariableID, SpellDifficultyID
|
||||||
) SELECT
|
from spell_dbc as src WHERE src.ID = %v;`, entryBump, spell.ID)
|
||||||
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)
|
|
||||||
|
|
||||||
update := fmt.Sprintf(`
|
update := fmt.Sprintf(`
|
||||||
UPDATE acore_world.spell_dbc
|
UPDATE acore_world.spell_dbc
|
||||||
|
|||||||
349
internal/spells/thematic.go
Normal file
349
internal/spells/thematic.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user