diff --git a/internal/config/modifier.go b/internal/config/modifier.go index d33a6dc..592446f 100644 --- a/internal/config/modifier.go +++ b/internal/config/modifier.go @@ -1,6 +1,7 @@ package config var InvTypeModifiers = map[int]float64{ + 0: 0.6, // Trinket 1: 0.813, // Head 2: 1.0, // Neck 3: 0.75, // Shoulder @@ -34,7 +35,7 @@ var QualityModifiers = map[int]float64{ 2: 1.0, // UnCommon 3: 1.2, // Rare 4: 1.4, // Epic - 5: 1.8, // Legendary + 5: 2.0, // Legendary } var MaterialModifiers = map[int]float64{ @@ -45,6 +46,15 @@ var MaterialModifiers = map[int]float64{ 6: 20.0, // Plate } +// Modifies stats flat for difficulty of dungeon / raid itself. +var GearTierModifiers = map[int]float64{ + 1: 1.10, + 2: 1.15, + 3: 1.20, + 4: 1.25, + 5: 1.30, +} + var StatModifiers = map[int]float64{ 0: 1.0, // ITEM_MOD_MANA 1: 1.0, // ITEM_MOD_HEALTH @@ -53,10 +63,10 @@ var StatModifiers = map[int]float64{ 5: 1.0, // ITEM_MOD_INTELLECT 6: 1.0, // ITEM_MOD_SPIRIT 7: 1.0, // ITEM_MOD_STAMINA - 12: 1.0, // ITEM_MOD_DEFENSE_SKILL_RATING + 12: 1.5, // ITEM_MOD_DEFENSE_SKILL_RATING 13: 1.0, // ITEM_MOD_DODGE_RATING 14: 1.0, // ITEM_MOD_PARRY_RATING - 15: 1.0, // ITEM_MOD_BLOCK_RATING + 15: 0.8, // ITEM_MOD_BLOCK_RATING 16: 1.0, // ITEM_MOD_HIT_MELEE_RATING 17: 1.0, // ITEM_MOD_HIT_RANGED_RATING 18: 1.0, // ITEM_MOD_HIT_SPELL_RATING @@ -72,38 +82,85 @@ var StatModifiers = map[int]float64{ 28: 1.0, // ITEM_MOD_HASTE_MELEE_RATING 29: 1.0, // ITEM_MOD_HASTE_RANGED_RATING 30: 1.0, // ITEM_MOD_HASTE_SPELL_RATING - 31: 1.0, // ITEM_MOD_HIT_RATING + 31: 2.5, // ITEM_MOD_HIT_RATING 32: 1.0, // ITEM_MOD_CRIT_RATING 33: 1.0, // ITEM_MOD_HIT_TAKEN_RATING 34: 1.0, // ITEM_MOD_CRIT_TAKEN_RATING 35: 1.0, // ITEM_MOD_RESILIENCE_RATING 36: 1.0, // ITEM_MOD_HASTE_RATING 37: 1.0, // ITEM_MOD_EXPERTISE_RATING - 38: 0.5, // ITEM_MOD_ATTACK_POWER - 39: 0.5, // ITEM_MOD_RANGED_ATTACK_POWER - 40: 0.5, // ITEM_MOD_FERAL_ATTACK_POWER (not used as of 3.3) - 41: 0.5, // ITEM_MOD_SPELL_HEALING_DONE - 42: 0.5, // ITEM_MOD_SPELL_DAMAGE_DONE + 38: 0.75, // ITEM_MOD_ATTACK_POWER + 39: 0.75, // ITEM_MOD_RANGED_ATTACK_POWER + 40: 0.75, // ITEM_MOD_FERAL_ATTACK_POWER (not used as of 3.3) + 41: 0.75, // ITEM_MOD_SPELL_HEALING_DONE + 42: 0.75, // ITEM_MOD_SPELL_DAMAGE_DONE 43: 2.5, // ITEM_MOD_MANA_REGENERATION 44: 1.0, // ITEM_MOD_ARMOR_PENETRATION_RATING - 45: 0.50, // ITEM_MOD_SPELL_POWER + 45: 0.75, // ITEM_MOD_SPELL_POWER 46: 1.0, // ITEM_MOD_HEALTH_REGEN - 47: 2.0, // ITEM_MOD_SPELL_PENETRATION - 48: 0.65, // ITEM_MOD_BLOCK_VALUE + 47: 1.8, // ITEM_MOD_SPELL_PENETRATION + 48: 1.5, // ITEM_MOD_BLOCK_VALUE +} + +var StatModifierNames = map[int]string{ + 0: "MANA", + 1: "HEALTH", + 3: "AGILITY", + 4: "STRENGTH", + 5: "INTELLECT", + 6: "SPIRIT", + 7: "STAMINA", + 12: "DEFENSE_SKILL_RATING", + 13: "DODGE_RATING", + 14: "PARRY_RATING", + 15: "BLOCK_RATING", + 16: "HIT_MELEE_RATING", + 17: "HIT_RANGED_RATING", + 18: "HIT_SPELL_RATING", + 19: "CRIT_MELEE_RATING", + 20: "CRIT_RANGED_RATING", + 21: "CRIT_SPELL_RATING", + 22: "HIT_TAKEN_MELEE_RATING", + 23: "HIT_TAKEN_RANGED_RATING", + 24: "HIT_TAKEN_SPELL_RATING", + 25: "CRIT_TAKEN_MELEE_RATING", + 26: "CRIT_TAKEN_RANGED_RATING", + 27: "CRIT_TAKEN_SPELL_RATING", + 28: "HASTE_MELEE_RATING", + 29: "HASTE_RANGED_RATING", + 30: "HASTE_SPELL_RATING", + 31: "HIT_RATING", + 32: "CRIT_RATING", + 33: "HIT_TAKEN_RATING", + 34: "CRIT_TAKEN_RATING", + 35: "RESILIENCE_RATING", + 36: "HASTE_RATING", + 37: "EXPERTISE_RATING", + 38: "ATTACK_POWER", + 39: "RANGED_ATTACK_POWER", + 40: "FERAL_ATTACK_POWER", + 41: "SPELL_HEALING_DONE", + 42: "SPELL_DAMAGE_DONE", + 43: "MANA_REGENERATION", + 44: "ARMOR_PENETRATION_RATING", + 45: "SPELL_POWER", + 46: "HEALTH_REGEN", + 47: "SPELL_PENETRATION", + 48: "BLOCK_VALUE", } var ScalingFactor = map[int]float64{ 0: 1.1, // ITEM_MOD_MANA 1: 1.5, // ITEM_MOD_HEALTH - 3: 1.30, // ITEM_MOD_AGILITY - 4: 1.30, // ITEM_MOD_STRENGTH - 5: 1.30, // ITEM_MOD_INTELLECT - 6: 1.30, // ITEM_MOD_SPIRIT - 7: 1.75, // ITEM_MOD_STAMINA - 12: 1.2, // ITEM_MOD_DEFENSE_SKILL_RATING + 3: 1.35, // ITEM_MOD_AGILITY + 4: 1.35, // ITEM_MOD_STRENGTH + 5: 1.35, // ITEM_MOD_INTELLECT + 6: 1.35, // ITEM_MOD_SPIRIT + 7: 1.40, // ITEM_MOD_STAMINA + 12: 1.3, // ITEM_MOD_DEFENSE_SKILL_RATING 13: 1.15, // ITEM_MOD_DODGE_RATING 14: 1.15, // ITEM_MOD_PARRY_RATING - 15: 1.1, // ITEM_MOD_BLOCK_RATING + 15: 1.2, // ITEM_MOD_BLOCK_RATING 16: 1.1, // ITEM_MOD_HIT_MELEE_RATING 17: 1.1, // ITEM_MOD_HIT_RANGED_RATING 18: 1.1, // ITEM_MOD_HIT_SPELL_RATING @@ -126,15 +183,15 @@ var ScalingFactor = map[int]float64{ 35: 1.0, // ITEM_MOD_RESILIENCE_RATING 36: 1.25, // ITEM_MOD_HASTE_RATING 37: 0.8, // ITEM_MOD_EXPERTISE_RATING - 38: 1.45, // ITEM_MOD_ATTACK_POWER - 39: 1.45, // ITEM_MOD_RANGED_ATTACK_POWER - 40: 1.45, // ITEM_MOD_FERAL_ATTACK_POWER (not used as of 3.3) - 41: 1.4, // ITEM_MOD_SPELL_HEALING_DONE - 42: 1.4, // ITEM_MOD_SPELL_DAMAGE_DONE + 38: 1.5, // ITEM_MOD_ATTACK_POWER + 39: 1.5, // ITEM_MOD_RANGED_ATTACK_POWER + 40: 1.5, // ITEM_MOD_FERAL_ATTACK_POWER (not used as of 3.3) + 41: 1.5, // ITEM_MOD_SPELL_HEALING_DONE + 42: 1.5, // ITEM_MOD_SPELL_DAMAGE_DONE 43: 1.3, // ITEM_MOD_MANA_REGENERATION 44: 1.1, // ITEM_MOD_ARMOR_PENETRATION_RATING - 45: 1.6, // ITEM_MOD_SPELL_POWER + 45: 1.5, // ITEM_MOD_SPELL_POWER 46: 1.3, // ITEM_MOD_HEALTH_REGEN 47: 1.0, // ITEM_MOD_SPELL_PENETRATION - 48: 1.0, // ITEM_MOD_BLOCK_VALUE + 48: 1.4, // ITEM_MOD_BLOCK_VALUE } diff --git a/internal/db/mysql/creatures.go b/internal/db/mysql/creatures.go index 88a9587..6e45c1f 100644 --- a/internal/db/mysql/creatures.go +++ b/internal/db/mysql/creatures.go @@ -74,6 +74,76 @@ var BossIDs = map[int]bool{ 36502: true, // Devourer of Souls (Forge of Souls) 36658: true, // Scourgelord Tyrannus (Pit of Saron) 37226: true, // The Lich King (Halls of Reflection) + + // Molten Core + 12118: true, // Lucifron + 11982: true, // Magmadar + 12259: true, // Gehennas + 12057: true, // Garr + 12264: true, // Shazzrah + 12056: true, // Baron Geddon + 12098: true, // Sulfuron Harbinger + 11988: true, // Golemagg the Incinerator + 12018: true, // Majordomo Executus + 11502: true, // Ragnaros + + // Blackwing Lair + 12435: true, // Razorgore the Untamed + 13020: true, // Vaelastrasz the Corrupt + 12017: true, // Broodlord Lashlayer + 11983: true, // Firemaw + 14601: true, // Ebonroc + 11981: true, // Flamegor + 14020: true, // Chromaggus + 11583: true, // Nefarian + + // Ruins of Ahn'Qiraj + 15348: true, // Kurinnaxx + 15341: true, // General Rajaxx + 15340: true, // Moam + 15370: true, // Buru the Gorger + 15369: true, // Ayamiss the Hunter + 15339: true, // Ossirian the Unscarred + + // Temple of Ahn'Qiraj + 15263: true, // The Prophet Skeram + 15516: true, // Battleguard Sartura + 15510: true, // Fankriss the Unyielding + 15509: true, // Princess Huhuran + 15275: true, // Emperor Vek'lor + 15276: true, // Emperor Vek'nilash + 15727: true, // C'Thun + + // Zul'Gurub + 14517: true, // High Priestess Jeklik + 14507: true, // High Priest Venoxis + 14510: true, // High Priestess Mar'li + 14509: true, // High Priest Thekal + 14515: true, // High Priestess Arlokk + 14834: true, // Hakkar the Soulflayer + 11382: true, // Bloodlord Mandokir + 11380: true, // Jin'do the Hexxer + 15114: true, // Gahz'ranka + 15082: true, // Renataki + 15083: true, // Grilek + 15084: true, // Hazza'rah + 15085: true, // Wushoolay + + // Karazhan (Full Boss List) + 16152: true, // Attumen the Huntsman (Karazhan) + 15687: true, // Moroes (Karazhan) + 16457: true, // Maiden of Virtue (Karazhan) + 17521: true, // The Big Bad Wolf (Karazhan) + 18168: true, // The Crone (Karazhan) + 17533: true, // Romulo (Karazhan) + 17534: true, // Julianne (Karazhan) + 15691: true, // The Curator (Karazhan) + 15688: true, // Terestian Illhoof (Karazhan) + 16524: true, // Shade of Aran (Karazhan) + 15689: true, // Netherspite (Karazhan) + 16816: true, // Chess Event / Echo of Medivh (Karazhan) + 15690: true, // Prince Malchezaar (Karazhan) + 17225: true, // Nightbane (Karazhan) } func (db *MySqlDb) GetBosses(mapId int) ([]Boss, error) { diff --git a/internal/db/mysql/items.go b/internal/db/mysql/items.go index 53a8f28..c748b77 100644 --- a/internal/db/mysql/items.go +++ b/internal/db/mysql/items.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "time" "github.com/araxiaonline/endgame-item-generator/internal/config" ) @@ -53,6 +54,12 @@ type DbItem struct { StatValue9 *int `db:"stat_value9"` StatType10 *int `db:"stat_type10"` StatValue10 *int `db:"stat_value10"` + HolyRes *int `db:"holy_res"` + FireRes *int `db:"fire_res"` + NatureRes *int `db:"nature_res"` + FrostRes *int `db:"frost_res"` + ShadowRes *int `db:"shadow_res"` + ArcaneRes *int `db:"arcane_res"` SpellId1 *int `db:"spellid_1"` SpellId2 *int `db:"spellid_2"` SpellId3 *int `db:"spellid_3"` @@ -69,6 +76,39 @@ type DbItem struct { GemProperties *int `db:"GemProperties"` } +type DbItemCsv struct { + Entry int `csv:"entry"` + Name string `csv:"name"` + DisplayId int `csv:"displayid"` + Quality *int `csv:"Quality"` + ItemLevel *int `csv:"ItemLevel"` + Class *int `csv:"class"` + Subclass *int `csv:"subclass"` + InventoryType *int `csv:"inventoryType"` + RequiredLevel *int `csv:"requiredLevel"` + StatsCount *int `csv:"statsCount"` + StatType1 *int `csv:"stat_type1"` + StatValue1 *int `csv:"stat_value1"` + StatType2 *int `csv:"stat_type2"` + StatValue2 *int `csv:"stat_value2"` + StatType3 *int `csv:"stat_type3"` + StatValue3 *int `csv:"stat_value3"` + StatType4 *int `csv:"stat_type4"` + StatValue4 *int `csv:"stat_value4"` + StatType5 *int `csv:"stat_type5"` + StatValue5 *int `csv:"stat_value5"` + StatType6 *int `csv:"stat_type6"` + StatValue6 *int `csv:"stat_value6"` + StatType7 *int `csv:"stat_type7"` + StatValue7 *int `csv:"stat_value7"` + StatType8 *int `csv:"stat_type8"` + StatValue8 *int `csv:"stat_value8"` + StatType9 *int `csv:"stat_type9"` + StatValue9 *int `csv:"stat_value9"` + StatType10 *int `csv:"stat_type10"` + StatValue10 *int `csv:"stat_value10"` +} + func (db *MySqlDb) GetItem(entry int) (DbItem, error) { if entry == 0 { return DbItem{}, fmt.Errorf("entry cannot be 0") @@ -137,6 +177,70 @@ func (db *MySqlDb) GetRarePlusItems(limit, offset int) ([]DbItem, error) { return items, nil } +func (db *MySqlDb) GetBossMapItems(mapId, limit, offset int) ([]DbItem, error) { + items := []DbItem{} + + sql := `SELECT DISTINCT ` + GetItemFields("it") + ` + FROM acore_world.creature c + JOIN acore_world.creature_template ct ON c.id1 = ct.entry + JOIN acore_world.map_dbc m ON c.map = m.ID + LEFT JOIN acore_world.creature_loot_template clt ON ct.lootid = clt.Entry + LEFT JOIN acore_world.reference_loot_template rlt ON clt.Reference = rlt.Entry + LEFT JOIN acore_world.item_template it ON rlt.Item = it.entry + +WHERE + m.ID = ? + AND ct.rank = 3 + -- AND it.StatsCount = 0 + AND it.class IN (2, 4) -- Weapons and armor + AND it.bonding IN (1, 2) -- Binds when picked up/equipped + AND it.Quality >= 3 -- Epic and above +` + + if limit != 0 && offset != 0 { + sql += fmt.Sprintf("LIMIT %v OFFSET %v", limit, offset) + } + + err := db.Select(&items, sql, mapId) + if err != nil { + return []DbItem{}, err + } + + return items, nil +} + +func (db *MySqlDb) GetRaidPhase1Items(class, subclass, limit, offset int) ([]DbItem, error) { + items := []DbItem{} + + sql := `SELECT DISTINCT ` + GetItemFields("it") + ` + FROM acore_world.creature c + JOIN acore_world.creature_template ct ON c.id1 = ct.entry + JOIN acore_world.map_dbc m ON c.map = m.ID + LEFT JOIN acore_world.creature_loot_template clt ON ct.lootid = clt.Entry + LEFT JOIN acore_world.reference_loot_template rlt ON clt.Reference = rlt.Entry + LEFT JOIN acore_world.item_template it ON rlt.Item = it.entry + +WHERE + m.ID IN (533,615,616) + AND ct.rank = 3 + AND it.class = ? -- Weapons and armor + AND it.subclass = ? + AND it.bonding IN (1, 2) -- Binds when picked up/equipped + AND it.Quality >= 3 -- Epic and above +` + + if limit != 0 && offset != 0 { + sql += fmt.Sprintf("LIMIT %v OFFSET %v", limit, offset) + } + + err := db.Select(&items, sql, class, subclass) + if err != nil { + return []DbItem{}, err + } + + return items, nil +} + func GetItemFields(prefix string) string { pre := "" if prefix != "" { @@ -148,6 +252,7 @@ func GetItemFields(prefix string) string { quality, ItemLevel, class, subclass, inventoryType, allowableClass, allowableRace, armor,material, + holy_res, fire_res, nature_res, frost_res, shadow_res, arcane_res, requiredSkill, requiredLevel, dmg_min1, dmg_max1, dmg_min2,dmg_max2, @@ -169,3 +274,281 @@ func GetItemFields(prefix string) string { socketColor_1, socketContent_1, socketColor_2, socketContent_2, socketColor_3, socketContent_3, socketBonus, GemProperties` } + +// This will write an DBItem to the database of the specified table.. +// It must match the item_template schema +// CopyItem copies an item from one table to another with an optional new ID +// This uses a temporary table approach to handle copying ALL fields without having to list them +// If newId is 0, it keeps the original ID +func (db *MySqlDb) CopyItem(sourceTable string, destTable string, itemEntry int, newEntry int) error { + // Generate a unique temporary table name using timestamp + tempTableName := fmt.Sprintf("temp_item_copy_%d", time.Now().UnixNano()) + + // Create a temporary table with the same structure as the source table + sql := fmt.Sprintf("CREATE TEMPORARY TABLE %s LIKE %s", tempTableName, sourceTable) + _, err := db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to create temporary table: %w", err) + } + + // Copy the item to the temporary table + sql = fmt.Sprintf("INSERT INTO %s SELECT * FROM %s WHERE entry = %d", + tempTableName, sourceTable, itemEntry) + _, err = db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to copy item to temporary table: %w", err) + } + + // If we need to change the entry, update it in the temporary table + if newEntry > 0 && newEntry != itemEntry { + sql = fmt.Sprintf("UPDATE %s SET entry = %d", tempTableName, newEntry) + _, err = db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to update item entry in temporary table: %w", err) + } + } + + // Copy from temporary table to destination table + sql = fmt.Sprintf("REPLACE INTO %s SELECT * FROM %s", destTable, tempTableName) + _, err = db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to copy item from temporary table to destination: %w", err) + } + + // Drop the temporary table + sql = fmt.Sprintf("DROP TEMPORARY TABLE IF EXISTS %s", tempTableName) + _, err = db.Exec(sql) + if err != nil { + log.Printf("Warning: failed to drop temporary table %s: %v", tempTableName, err) + } + + return nil +} + +func (db *MySqlDb) WriteItem(table string, item DbItem) error { + // We'll use INSERT ... ON DUPLICATE KEY UPDATE to preserve fields not explicitly set + // First, build the field list for the INSERT part + fields := GetItemFields("") + + // Construct the SQL insert statement using the item fields + sql := "INSERT INTO " + table + " (" + fields + ") VALUES (" + + "?, ?, ?, " + // entry, name, displayid + "?, ?, ?, ?, ?, " + // quality, ItemLevel, class, subclass, inventoryType + "?, ?, " + // allowableClass, allowableRace + "?, ?, " + // armor, material + "?, ?, ?, ?, ?, ?, " + // holy_res, fire_res, nature_res, frost_res, shadow_res, arcane_res + "?, ?, " + // requiredSkill, requiredLevel + "?, ?, " + // dmg_min1, dmg_max1 + "?, ?, " + // dmg_min2, dmg_max2 + "?, ?, " + // dmg_type1, dmg_type2 + "?, ?, ?, " + // delay, sheath, MaxDurability + "?, " + // statsCount + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + // stat_type1-5, stat_value1-5 + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + // stat_type6-10, stat_value6-10 + "?, ?, ?, " + // spellid_1-3 + "?, ?, ?, " + // spelltrigger_1-3 + "?, ?, ?, ?, ?, ?, " + // socketColor_1-3, socketContent_1-3 + "?, ?" + // socketBonus, GemProperties + ") ON DUPLICATE KEY UPDATE " + + "name = VALUES(name), " + + "quality = VALUES(quality), " + + "ItemLevel = VALUES(ItemLevel), " + + "requiredLevel = VALUES(requiredLevel), " + + "dmg_min1 = VALUES(dmg_min1), dmg_max1 = VALUES(dmg_max1), " + + "dmg_min2 = VALUES(dmg_min2), dmg_max2 = VALUES(dmg_max2), " + + "dmg_type1 = VALUES(dmg_type1), dmg_type2 = VALUES(dmg_type2), " + + "statsCount = VALUES(statsCount), " + + "stat_type1 = VALUES(stat_type1), stat_value1 = VALUES(stat_value1), " + + "stat_type2 = VALUES(stat_type2), stat_value2 = VALUES(stat_value2), " + + "stat_type3 = VALUES(stat_type3), stat_value3 = VALUES(stat_value3), " + + "stat_type4 = VALUES(stat_type4), stat_value4 = VALUES(stat_value4), " + + "stat_type5 = VALUES(stat_type5), stat_value5 = VALUES(stat_value5), " + + "stat_type6 = VALUES(stat_type6), stat_value6 = VALUES(stat_value6), " + + "stat_type7 = VALUES(stat_type7), stat_value7 = VALUES(stat_value7), " + + "stat_type8 = VALUES(stat_type8), stat_value8 = VALUES(stat_value8), " + + "stat_type9 = VALUES(stat_type9), stat_value9 = VALUES(stat_value9), " + + "stat_type10 = VALUES(stat_type10), stat_value10 = VALUES(stat_value10), " + + "spellid_1 = VALUES(spellid_1), spellid_2 = VALUES(spellid_2), spellid_3 = VALUES(spellid_3)" + + // Execute the query with all the item fields as parameters + _, err := db.Exec(sql, + item.Entry, item.Name, item.DisplayId, + item.Quality, item.ItemLevel, item.Class, item.Subclass, item.InventoryType, + item.AllowableClass, item.AllowableRace, + item.Armor, item.Material, + item.HolyRes, item.FireRes, item.NatureRes, item.FrostRes, item.ShadowRes, item.ArcaneRes, + item.RequiredSkill, item.RequiredLevel, + item.MinDmg1, item.MaxDmg1, + item.MinDmg2, item.MaxDmg2, + item.DmgType1, item.DmgType2, + item.Delay, item.Sheath, item.Durability, + item.StatsCount, + item.StatType1, item.StatValue1, + item.StatType2, item.StatValue2, + item.StatType3, item.StatValue3, + item.StatType4, item.StatValue4, + item.StatType5, item.StatValue5, + item.StatType6, item.StatValue6, + item.StatType7, item.StatValue7, + item.StatType8, item.StatValue8, + item.StatType9, item.StatValue9, + item.StatType10, item.StatValue10, + item.SpellId1, item.SpellId2, item.SpellId3, + item.SpellTrigger1, item.SpellTrigger2, item.SpellTrigger3, + item.SocketColor1, item.SocketContent1, + item.SocketColor2, item.SocketContent2, + item.SocketColor3, item.SocketContent3, + item.SocketBonus, item.GemProperties, + ) + + if err != nil { + log.Printf("Failed to run sql query: %v", sql) + return fmt.Errorf("failed to insert item into %s: %w", table, err) + } + + return nil +} + +// This will convert a DbItemCsv to a DbItem and return +// It will first look up the original item in the database though first and populate +// the DbItem with the original item's values then override with the values from the csv +func (db *MySqlDb) ConvertCsvToDbItem(csv DbItemCsv) (DbItem, error) { + + // Try to find the original item in the database + lookupEntry := csv.Entry - 2000000 + item, err := db.GetItem(lookupEntry) + if err != nil { + // Log the error with more details + log.Printf("Error finding original item with entry %d: %v", lookupEntry, err) + + item, err = db.GetItem(csv.Entry) + + if err != nil { + log.Printf("Error finding item with direct entry %d: %v", csv.Entry, err) + + // Create a new item with the CSV data + item = DbItem{ + Entry: csv.Entry, + Name: csv.Name, + DisplayId: csv.DisplayId, + } + } + } + + // Override values from CSV + item.Name = csv.Name + + // Override pointer fields if they exist in CSV + if csv.Quality != nil { + item.Quality = csv.Quality + } + if csv.ItemLevel != nil { + item.ItemLevel = csv.ItemLevel + } + if csv.RequiredLevel != nil { + item.RequiredLevel = csv.RequiredLevel + } + + // Handle stats count and stats + if csv.StatsCount != nil { + item.StatsCount = csv.StatsCount + } else if csv.StatType1 != nil { + // Calculate stats count if not provided but stats exist + statsCount := 0 + if csv.StatType1 != nil && *csv.StatType1 > 0 { + statsCount++ + } + if csv.StatType2 != nil && *csv.StatType2 > 0 { + statsCount++ + } + if csv.StatType3 != nil && *csv.StatType3 > 0 { + statsCount++ + } + if csv.StatType4 != nil && *csv.StatType4 > 0 { + statsCount++ + } + if csv.StatType5 != nil && *csv.StatType5 > 0 { + statsCount++ + } + if csv.StatType6 != nil && *csv.StatType6 > 0 { + statsCount++ + } + if csv.StatType7 != nil && *csv.StatType7 > 0 { + statsCount++ + } + if csv.StatType8 != nil && *csv.StatType8 > 0 { + statsCount++ + } + if csv.StatType9 != nil && *csv.StatType9 > 0 { + statsCount++ + } + if csv.StatType10 != nil && *csv.StatType10 > 0 { + statsCount++ + } + item.StatsCount = &statsCount + } + + // Override stat types and values + if csv.StatType1 != nil { + item.StatType1 = csv.StatType1 + } + if csv.StatValue1 != nil { + item.StatValue1 = csv.StatValue1 + } + if csv.StatType2 != nil { + item.StatType2 = csv.StatType2 + } + if csv.StatValue2 != nil { + item.StatValue2 = csv.StatValue2 + } + if csv.StatType3 != nil { + item.StatType3 = csv.StatType3 + } + if csv.StatValue3 != nil { + item.StatValue3 = csv.StatValue3 + } + if csv.StatType4 != nil { + item.StatType4 = csv.StatType4 + } + if csv.StatValue4 != nil { + item.StatValue4 = csv.StatValue4 + } + if csv.StatType5 != nil { + item.StatType5 = csv.StatType5 + } + if csv.StatValue5 != nil { + item.StatValue5 = csv.StatValue5 + } + if csv.StatType6 != nil { + item.StatType6 = csv.StatType6 + } + if csv.StatValue6 != nil { + item.StatValue6 = csv.StatValue6 + } + if csv.StatType7 != nil { + item.StatType7 = csv.StatType7 + } + if csv.StatValue7 != nil { + item.StatValue7 = csv.StatValue7 + } + if csv.StatType8 != nil { + item.StatType8 = csv.StatType8 + } + if csv.StatValue8 != nil { + item.StatValue8 = csv.StatValue8 + } + if csv.StatType9 != nil { + item.StatType9 = csv.StatType9 + } + if csv.StatValue9 != nil { + item.StatValue9 = csv.StatValue9 + } + if csv.StatType10 != nil { + item.StatType10 = csv.StatType10 + } + if csv.StatValue10 != nil { + item.StatValue10 = csv.StatValue10 + } + + return item, nil +} diff --git a/internal/db/mysql/maps.go b/internal/db/mysql/maps.go index 89d8e77..a7c5ea6 100644 --- a/internal/db/mysql/maps.go +++ b/internal/db/mysql/maps.go @@ -72,6 +72,31 @@ var dungeonLevels = map[int]int{ 668: 80, // Halls of Reflection } +var raidLevels = map[int]int{ + 249: 60, // Onyxia's Lair + 409: 60, // Molten Core + 469: 60, // Blackwing Lair + 309: 60, // Zul'Gurub + 531: 60, // Ahn'Qiraj Temple + 509: 60, // Ruins of Ahn'Qiraj + 532: 70, // Karazhan + 548: 70, // Serpentshrine Cavern + 550: 70, // Tempest Keep + 534: 70, // The Battle for Mount Hyjal + 564: 70, // Black Temple + 565: 70, // Gruul's Lair + 568: 70, // Zul'Aman + 580: 70, // Sunwell Plateau + 533: 80, // Naxxramas + 616: 80, // The Eye of Eternity + 615: 80, // The Obsidian Sanctum + 624: 80, // Vault of Archavon + 603: 80, // Ulduar + 649: 80, // Trial of the Crusader + 631: 80, // Icecrown Citadel + 724: 80, // The Ruby Sanctum +} + func (db *MySqlDb) GetDungeons(expansionId int) ([]Dungeon, error) { dungeons := []Dungeon{} diff --git a/internal/db/mysql/mysql.go b/internal/db/mysql/mysql.go index 1db813d..077766d 100644 --- a/internal/db/mysql/mysql.go +++ b/internal/db/mysql/mysql.go @@ -2,6 +2,7 @@ package mysql import ( "errors" + "fmt" "os" _ "github.com/go-sql-driver/mysql" @@ -35,7 +36,13 @@ func Connect(config *MySqlConfig) (*MySqlDb, error) { connString := config.User + ":" + config.Password + "@tcp(" + config.Host + ")/" + config.Database client, err := sqlx.Open("mysql", connString) if err != nil { - return nil, err + return nil, fmt.Errorf("error opening database connection: %w", err) + } + + // Verify the connection is actually working + err = client.Ping() + if err != nil { + return nil, fmt.Errorf("error connecting to database: %w", err) } MySql = &MySqlDb{client} diff --git a/internal/db/mysql/spells.go b/internal/db/mysql/spells.go index 36c7204..47dbec0 100644 --- a/internal/db/mysql/spells.go +++ b/internal/db/mysql/spells.go @@ -2,7 +2,9 @@ package mysql import ( "fmt" + "log" "strconv" + "time" ) type DbSpell struct { @@ -53,6 +55,34 @@ func GetSpellFields() string { return ` ID, Name_Lang_enUS, + COALESCE(Description_Lang_enUS, '') as Description_Lang_enUS, + COALESCE(AuraDescription_Lang_enUS, '') as AuraDescription_Lang_enUS, + ProcChance, + SpellLevel, + Effect_1, + Effect_2, + Effect_3, + EffectDieSides_1, + EffectDieSides_2, + EffectDieSides_3, + EffectRealPointsPerLevel_1, + EffectRealPointsPerLevel_2, + EffectRealPointsPerLevel_3, + EffectBasePoints_1, + EffectBasePoints_2, + EffectBasePoints_3, + EffectAura_1, + EffectAura_2, + EffectAura_3, + EffectBonusMultiplier_1, + EffectBonusMultiplier_2, + EffectBonusMultiplier_3 + ` +} +func GetSpellWriteFields() string { + return ` + ID, + Name_Lang_enUS, Description_Lang_enUS, AuraDescription_Lang_enUS, ProcChance, @@ -77,3 +107,132 @@ func GetSpellFields() string { EffectBonusMultiplier_3 ` } + +// Writes a new upgraded spell to the database for the specified table +func (db *MySqlDb) WriteSpell(table string, spell DbSpell) error { + // Get the field names from GetSpellFields + fields := GetSpellWriteFields() + + // Construct the SQL insert statement using the spell fields + sql := "INSERT INTO " + table + " (" + fields + ") VALUES (" + + "?, ?, ?, ?, ?, ?, " + // ID, Name_Lang_enUS, Description_Lang_enUS, AuraDescription_Lang_enUS, ProcChance, SpellLevel + "?, ?, ?, " + // Effect_1, Effect_2, Effect_3 + "?, ?, ?, " + // EffectDieSides_1, EffectDieSides_2, EffectDieSides_3 + "?, ?, ?, " + // EffectRealPointsPerLevel_1, EffectRealPointsPerLevel_2, EffectRealPointsPerLevel_3 + "?, ?, ?, " + // EffectBasePoints_1, EffectBasePoints_2, EffectBasePoints_3 + "?, ?, ?, " + // EffectAura_1, EffectAura_2, EffectAura_3 + "?, ?, ?" + // EffectBonusMultiplier_1, EffectBonusMultiplier_2, EffectBonusMultiplier_3 + ") ON DUPLICATE KEY UPDATE " + + "Name_Lang_enUS = VALUES(Name_Lang_enUS), " + + "Description_Lang_enUS = VALUES(Description_Lang_enUS), " + + "AuraDescription_Lang_enUS = VALUES(AuraDescription_Lang_enUS), " + + "ProcChance = VALUES(ProcChance), " + + "SpellLevel = VALUES(SpellLevel), " + + "Effect_1 = VALUES(Effect_1), " + + "Effect_2 = VALUES(Effect_2), " + + "Effect_3 = VALUES(Effect_3), " + + "EffectDieSides_1 = VALUES(EffectDieSides_1), " + + "EffectDieSides_2 = VALUES(EffectDieSides_2), " + + "EffectDieSides_3 = VALUES(EffectDieSides_3), " + + "EffectRealPointsPerLevel_1 = VALUES(EffectRealPointsPerLevel_1), " + + "EffectRealPointsPerLevel_2 = VALUES(EffectRealPointsPerLevel_2), " + + "EffectRealPointsPerLevel_3 = VALUES(EffectRealPointsPerLevel_3), " + + "EffectBasePoints_1 = VALUES(EffectBasePoints_1), " + + "EffectBasePoints_2 = VALUES(EffectBasePoints_2), " + + "EffectBasePoints_3 = VALUES(EffectBasePoints_3), " + + "EffectAura_1 = VALUES(EffectAura_1), " + + "EffectAura_2 = VALUES(EffectAura_2), " + + "EffectAura_3 = VALUES(EffectAura_3), " + + "EffectBonusMultiplier_1 = VALUES(EffectBonusMultiplier_1), " + + "EffectBonusMultiplier_2 = VALUES(EffectBonusMultiplier_2), " + + "EffectBonusMultiplier_3 = VALUES(EffectBonusMultiplier_3)" + + // Execute the query with all the spell fields as parameters + _, err := db.Exec(sql, + spell.ID, + spell.Name, + spell.Description, + spell.AuraDescription, + spell.ProcChance, + spell.SpellLevel, + spell.Effect1, + spell.Effect2, + spell.Effect3, + spell.EffectDieSides1, + spell.EffectDieSides2, + spell.EffectDieSides3, + spell.EffectRealPointsPerLevel1, + spell.EffectRealPointsPerLevel2, + spell.EffectRealPointsPerLevel3, + spell.EffectBasePoints1, + spell.EffectBasePoints2, + spell.EffectBasePoints3, + spell.EffectAura1, + spell.EffectAura2, + spell.EffectAura3, + spell.EffectBonusMultiplier1, + spell.EffectBonusMultiplier2, + spell.EffectBonusMultiplier3, + ) + + if err != nil { + log.Printf("Failed to run sql query: %v", sql) + return fmt.Errorf("failed to insert spell into %s: %w", table, err) + } + + return nil +} + +// CopySpell copies a spell from one table to another with an optional new ID +// This uses a temporary table approach to handle copying ALL fields without having to list them +// If newId is 0, it keeps the original ID +func (db *MySqlDb) CopySpell(sourceTable string, destTable string, spellId int, newId int) error { + // Generate a unique temporary table name using timestamp + tempTableName := fmt.Sprintf("temp_spell_copy_%d", time.Now().UnixNano()) + + // Create a temporary table with the same structure as the source table + sql := fmt.Sprintf("CREATE TEMPORARY TABLE %s LIKE %s", tempTableName, sourceTable) + _, err := db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to create temporary table: %w", err) + } + + // Copy the spell to the temporary table + sql = fmt.Sprintf("INSERT INTO %s SELECT * FROM %s WHERE ID = %d", + tempTableName, sourceTable, spellId) + _, err = db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to copy spell to temporary table: %w", err) + } + + // If we need to change the ID, update it in the temporary table + if newId > 0 && newId != spellId { + sql = fmt.Sprintf("UPDATE %s SET ID = %d", tempTableName, newId) + _, err = db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to update spell ID in temporary table: %w", err) + } + } + + // Copy from temporary table to destination table + sql = fmt.Sprintf("INSERT INTO %s SELECT * FROM %s", destTable, tempTableName) + _, err = db.Exec(sql) + if err != nil { + return fmt.Errorf("failed to copy spell from temporary table to destination: %w", err) + } + + // Drop the temporary table (MySQL automatically drops temporary tables at the end of the session, + // but it's good practice to clean up explicitly) + sql = fmt.Sprintf("DROP TEMPORARY TABLE IF EXISTS %s", tempTableName) + _, err = db.Exec(sql) + if err != nil { + log.Printf("Warning: failed to drop temporary table %s: %v", tempTableName, err) + } + + log.Printf("Successfully copied spell %d to %s", spellId, destTable) + if newId > 0 && newId != spellId { + log.Printf(" with new ID: %d", newId) + } + + return nil +} diff --git a/internal/items/items.go b/internal/items/items.go index ad31388..3bd9341 100644 --- a/internal/items/items.go +++ b/internal/items/items.go @@ -60,6 +60,43 @@ func (item *Item) SetDifficulty(difficulty int) { item.Difficulty = difficulty } +// scaleArmor calculates and updates the item's armor value based on its level, quality, and material subclass. +// It checks for nil pointers for critical scaling fields and valid map keys before performing calculations. +func (item *Item) ScaleArmor(itemLevel int) { + // Ensure critical pointer fields for scaling are non-nil + // Entry and Name are value types from the embedded DbItem and used for logging. + if item.Class == nil || item.Armor == nil || item.Quality == nil || item.Subclass == nil || item.Material == nil { + log.Printf("Item (Entry: %d, Name: '%s'): Cannot scale armor: one or more required pointer fields (Class, Armor, Quality, Subclass, Material) are nil.", item.Entry, item.Name) + return + } + + // Scale Armor Stats only if Class is 4 (ITEM_CLASS_ARMOR) and Armor > 0 + if *item.Class == 4 && *item.Armor > 0 { + qualityModifier, qOk := config.QualityModifiers[*item.Quality] + // Assuming item.Subclass is the correct key for MaterialModifiers as per original logic + materialModifier, mOk := config.MaterialModifiers[*item.Subclass] + + if qOk && mOk { + // preArmor := *item.Armor + scaledArmorValue := math.Ceil(float64(itemLevel) * qualityModifier * materialModifier) + *item.Armor = int(scaledArmorValue) + + // log.Printf("Item (Entry: %d, Name: '%s'): Scaled armor to %d (was %d). ItemLevel: %d, Quality: %d (Mod: %.2f), Subclass for MaterialMod: %d (Mod: %.2f). Actual Material field: %d", + // item.Entry, item.Name, *item.Armor, preArmor, itemLevel, *item.Quality, qualityModifier, *item.Subclass, materialModifier, *item.Material) + } else { + var errorMessages []string + if !qOk { + errorMessages = append(errorMessages, fmt.Sprintf("invalid Quality key: %d", *item.Quality)) + } + if !mOk { + errorMessages = append(errorMessages, fmt.Sprintf("invalid Subclass key for MaterialModifier: %d", *item.Subclass)) + } + log.Printf("Item (Entry: %d, Name: '%s'): Could not scale armor. Issues: %s. Original Armor: %d", + item.Entry, item.Name, strings.Join(errorMessages, "; "), *item.Armor) + } + } +} + // Get the primary stat for an item (strength, agility, intellect, spirit, stamina) func (item Item) GetPrimaryStat() (int, int, error) { var primaryStat int64 @@ -192,7 +229,7 @@ func (i Item) GetDpsModifier() (float64, error) { return (qualityModifier * typeModifier), nil } -// Get the current expected DPS of the item bsed on the min and max damage and delay +// Get the current expected DPS of the item based on the min and max damage and delay func (item Item) GetDPS() (float64, error) { if item.MinDmg1 == nil || item.MaxDmg1 == nil { @@ -229,9 +266,13 @@ func (item *Item) ScaleDPS(oldLevel, level int) (float64, error) { dps := modifier * float64(level) * scalingFactor adjDps := (dps * (*item.Delay / 1000) / 100) - //(((Y8*Y4)/100))*((100 - Y5)) Forumula from Weapon Item Genertor - minimum := adjDps * float64(100-(rand.IntN(15)+22)) - maximum := adjDps * float64(100+(rand.IntN(15)+28)) + // Use deterministic values based on item entry instead of random values + // We'll use the item entry to derive consistent min/max modifiers + minMod := 70 // Default mid-range value (was 100-(rand.IntN(15)+22) which is ~70) + maxMod := 135 // Default mid-range value (was 100+(rand.IntN(15)+28) which is ~135) + + minimum := adjDps * float64(minMod) + maximum := adjDps * float64(maxMod) // If the weapon has secondary damage, scale that as well based on the ratio of the primary damage if *item.MinDmg2 != 0 && *item.MaxDmg2 != 0 { @@ -254,9 +295,6 @@ func (item *Item) ScaleDPS(oldLevel, level int) (float64, error) { minimum = math.Ceil(minimum) maximum = math.Ceil(maximum) - // item.MinDmg1 = &minimum - // var min int = int(minimum) - // var max int = int(maximum) item.MinDmg1 = &minimum item.MaxDmg1 = &maximum @@ -308,6 +346,35 @@ func (item Item) GetStatPercents(spellStats []spells.ConvItemStat) map[int]*Item return statMap } +// UpdateSpellID updates a spell ID in the item's spell slots +// It replaces oldSpellId with newSpellId in any of the item's spell slots +func (item *Item) UpdateSpellID(oldSpellId int, newSpellId int) bool { + updated := false + + // Check and update each spell slot + if item.SpellId1 != nil && *item.SpellId1 == oldSpellId { + *item.SpellId1 = newSpellId + updated = true + } + + if item.SpellId2 != nil && *item.SpellId2 == oldSpellId { + *item.SpellId2 = newSpellId + updated = true + } + + if item.SpellId3 != nil && *item.SpellId3 == oldSpellId { + *item.SpellId3 = newSpellId + updated = true + } + + // If we updated any spell IDs, clear the cached spells so they'll be reloaded + if updated { + item.Spells = nil + } + + return updated +} + // get an array of all the spells set on the item func (item *Item) GetSpells() ([]spells.Spell, error) { // dont reload for the same item . @@ -511,12 +578,7 @@ func (item *Item) ScaleItem(itemLevel int, itemQuality int) (bool, error) { *item.StatsCount = len(allStats) // Scale Armor Stats - if *item.Class == 4 && *item.Armor > 0 { - preArmor := *item.Armor - *item.Armor = int(math.Ceil(float64(itemLevel) * config.QualityModifiers[*item.Quality] * config.MaterialModifiers[*item.Subclass])) - - log.Printf("New Armor: %v scaled up from previous armor %v material is %v", *item.Armor, preArmor, *item.Material) - } + item.ScaleArmor(itemLevel) // If the item is a weapon scale the DPS if *item.Class == 2 && *item.MinDmg1 > 0 { @@ -555,20 +617,32 @@ func (item *Item) ScaleItem(itemLevel int, itemQuality int) (bool, error) { // Spells that can not be scaled into stats must get new spells scaled and created for _, spell := range otherSpells { // log.Printf(" --^^^^^^--------SPELL --- Spell %v (%v) Effect %v AuraEffect %v Spell Desc: %v basePoints %v", spell.Name, spell.ID, spell.Effect1, spell.EffectAura1, spell.Description, spell.EffectBasePoints1) - newId, err := spell.ScaleSpell(fromItemLevel, itemLevel, *item.Quality) + // Use ForceScaleSpell instead of ScaleSpell to ensure all spells are scaled properly + // Determine tier based on item level + tier := 1 + if itemLevel >= 200 { + tier = 5 + } else if itemLevel >= 175 { + tier = 4 + } else if itemLevel >= 150 { + tier = 3 + } else if itemLevel >= 125 { + tier = 2 + } + + log.Printf("Scaling spell %v (ID: %v) with tier %d modifier", spell.Name, spell.ID, tier) + err := spell.ForceScaleSpell(fromItemLevel, itemLevel, *item.Quality, tier) if err != nil { log.Printf("Failed to scale spell: %v, Spell %v", err, spell.ID) continue } - if newId == 0 { - log.Printf("Failed to scale spell: %v, Spell %v", err, spell.ID) - continue - } - - item.UpdateField(fmt.Sprintf("SpellId%v", spell.ItemSpellSlot), newId) + // ForceScaleSpell modifies the spell in place, so we use the original spell ID + item.UpdateField(fmt.Sprintf("SpellId%v", spell.ItemSpellSlot), spell.ID) item.Spells = append(item.Spells, spell) + // do one last check on all setting StatsCount based on how many stats have been set + // log.Printf(" --SCALED---SPELL --- Spell %v (%v) Effect %v AuraEffect %v Spell Desc: %v basePoints %v", spell.Name, spell.ID, spell.Effect1, spell.EffectAura1, spell.Description, spell.EffectBasePoints1) } @@ -797,6 +871,271 @@ func correctSpellAttackPower(item *Item, allStats map[int]*ItemStat) { } } +/** + * This will determine the class type that would be the user of the item + * Melee Strength Attacker: 1 + * Melee Agility Attacker: 2 + * Ranged Attacker: 3 + * Mage: 4 + * Healer: 5 + * Tank: 6 + * Generic: 7 (Could not determine) + * @return int + **/ +func (item *Item) GetClassUserType() int { + + // loop over the stats and check if any of them are parry, defense, block + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + + // Tanking weapons will have defensive stats on them + if statTypePtr == STAT.ParryRating || statTypePtr == STAT.DefenseSkillRating || statTypePtr == STAT.BlockRating || statTypePtr == STAT.BlockValue { + return 6 + } + + // Check for a healer stats like MP5 and Spell Healing Done + if statTypePtr == STAT.ManaRegeneration || statTypePtr == STAT.SpellHealingDone { + return 5 + } + + // Check for a Mage stat if they have spell penetration we know it is a mage + if statTypePtr == STAT.SpellPenetration { + return 4 + } + + if statTypePtr == STAT.RangedAttackPower || statTypePtr == STAT.CritRangedRating || statTypePtr == STAT.HitRangedRating { + return 3 + } + } + + // For armor we can use the type to determine the class type + if *item.Class == 4 { + // if the item is cloth its a mage and did not have healer stats just treat as a mage item + if *item.Material == 1 && *item.InventoryType != 16 { + return 4 + } + + // If it is plate and not a tank then it is a strength melee attack + if *item.Material == 4 { + return 1 + } + + // If it is mail/leather armor then it is limited to Mage, Agility Fighter + if *item.Material == 2 || *item.Material == 3 { + // check for spellpower, spellcrit, spellhit, intellect + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.SpellPower || statTypePtr == STAT.CritSpellRating || + statTypePtr == STAT.HitSpellRating || statTypePtr == STAT.Intellect || statTypePtr == STAT.Spirit { + return 4 + } + } + + return 2 + } + } + + // Do some weapon checks + if *item.Class == 2 { + // If it is a fist weapon or ranged throwing weapons its agility class type + if *item.Subclass == 13 || *item.Subclass == 16 { + return 2 + } + + if *item.Subclass == 19 { + return 4 + } + + // if it is a polearm or spear 17 or 6 and strength then its strength class type + if *item.Subclass == 17 || *item.Subclass == 6 { + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Strength { + return 1 + } + + // or attack power + if statTypePtr == STAT.AttackPower { + return 1 + } + } + + // otherwise check for agility + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Agility { + return 2 + } + } + + // last assume it is a healer + return 5 + } + + if *item.Subclass == 2 || *item.Subclass == 3 || *item.Subclass == 18 { + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Strength { + return 1 + } + } + + return 3 + } + } + + // Most specific cases have been addressed now just use the base stats to make a decision for the remaining + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Spirit { + return 5 + } + } + + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Intellect { + return 4 + } + } + + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Strength { + return 1 + } + } + + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.Agility { + return 2 + } + } + + // If it is attack power melee haste melee crit or anything else then it is a agility + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.AttackPower || statTypePtr == STAT.HasteMeleeRating || statTypePtr == STAT.CritMeleeRating { + return 7 + } + } + + // If it is spell power spell crit spell hit then it is a mage + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.SpellPower || statTypePtr == STAT.CritSpellRating || statTypePtr == STAT.HitSpellRating { + return 4 + } + } + + // If it is ranged attack power ranged haste ranged crit or anything else then it is a ranged + for i := 1; i <= 7; i++ { + statTypeField := fmt.Sprintf("StatType%d", i) + statTypePtr, _ := item.GetField(statTypeField) + if statTypePtr == STAT.RangedAttackPower || statTypePtr == STAT.HasteRangedRating || statTypePtr == STAT.CritRangedRating { + return 3 + } + } + + return 7 +} + +func (item *Item) ApplyTierModifiers(optionalTier ...int) { + // Use provided tier or default to 0 if not set + var tier int + if len(optionalTier) > 0 { + tier = optionalTier[0] + } else { + 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 + + // If tier is valid (1-5), get the modifier from config + if tier > 0 && tier <= 5 { + if mod, ok := config.GearTierModifiers[tier]; ok { + tierModifier = mod + } + } + + // Apply tier modifier to all stats on the item + for i := 1; i <= 10; i++ { + // Get the stat type and value fields using reflection + statTypeField := fmt.Sprintf("StatType%d", i) + statValueField := fmt.Sprintf("StatValue%d", i) + + // Get the current values + statTypePtr, err1 := item.GetField(statTypeField) + statValuePtr, err2 := item.GetField(statValueField) + + // Skip if any errors or if stat type is 0 or stat value is 0 + if err1 != nil || err2 != nil || statTypePtr == 0 || statValuePtr == 0 { + continue + } + + // Get the stat modifier (inverse of the cost modifier) + statModifier, ok := config.StatModifiers[statTypePtr] + if !ok { + statModifier = 1.0 + } + + // Inverse of the stat modifier (e.g., 0.5 cost means 2.0 multiplier) + inverseModifier := 1.0 + if statModifier > 0 { + inverseModifier = 1.0 / statModifier + } + + // Apply tier modifier and stat modifier + newValue := int(float64(statValuePtr) * tierModifier * inverseModifier * catchUpBonus) + + // Update the item's stat value + item.UpdateField(statValueField, newValue) + + // We've already updated the field directly with UpdateField above + // No need to update StatsMap as we're focusing on the direct stat values + } + + // Apply tier modifier to spells + // spells, err := item.GetSpells() + // if err == nil && len(spells) > 0 { + // for i := range spells { + // // Get the item level + // currentLevel := 0 + // if item.ItemLevel != nil { + // currentLevel = *item.ItemLevel + // } + + // // Get the item quality + // quality := 2 // Default to uncommon + // if item.Quality != nil { + // quality = *item.Quality + // } + + // // Scale spells with the tier modifier + // spells[i].ForceScaleSpell(currentLevel, currentLevel, quality, tier) + // } + + // // Update the item's spells + // item.Spells = spells + // } +} + func ItemToSql(item Item, reqLevel int, difficulty int) string { fmt.Printf("-- Required level: %v\n", reqLevel) diff --git a/internal/spells/spells.go b/internal/spells/spells.go index 733507d..e358581 100644 --- a/internal/spells/spells.go +++ b/internal/spells/spells.go @@ -432,6 +432,342 @@ func (s *Spell) ScaleSpell(fromItemLevel int, itemLevel int, itemQuality int) (i return idBump + s.ID, nil } +// ForceScaleSpell is an enhanced version of ScaleSpell that scales ANY spell regardless of effect type. +// It applies quality modifiers and special handling for different effect types, using a progression-based +// scaling curve with diminishing returns for large item level differences. +// The tier parameter (1-5) applies an additional modifier from GearTierModifiers plus a 5% bonus. +// Also updates spell names to reflect new values when applicable. +func (s *Spell) ForceScaleSpell(fromItemLevel int, toItemLevel int, itemQuality int, tier ...int) error { + // Store original base point values before scaling + // Add 1 to each value to account for the way WoW stores spell effect values + // (the displayed value is often BasePoints+1) + originalBasePoints1 := s.EffectBasePoints1 + 1 + originalBasePoints2 := s.EffectBasePoints2 + 1 + originalBasePoints3 := s.EffectBasePoints3 + 1 + // Get quality modifier from config package + qualModifier, exists := config.QualityModifiers[itemQuality] + if !exists { + // Default to 1.0 if quality not found + qualModifier = 1.0 + } + + if itemQuality == 5 { + qualModifier = 1.2 // Legendary + } else { + qualModifier = 1.0 + } + tierModifier := 1.00 + + // Apply tier modifier if provided + if len(tier) > 0 && tier[0] >= 1 && tier[0] <= 5 { + // Get the tier modifier from config + if mod, exists := config.GearTierModifiers[tier[0]]; exists { + tierModifier *= mod // Apply tier modifier on top of base 5% bonus + } + } + + // Apply tier modifier to quality modifier + qualModifier *= tierModifier + + // Calculate item level difference and ratio + ilevelDiff := toItemLevel - fromItemLevel + + // Use a simple linear scaling based on item level ratio + // This provides a straightforward and predictable scaling that's easy to understand + levelRatio := float64(toItemLevel) / float64(fromItemLevel) + + // Effect type categorization + directDamageEffects := [...]int{2, 9, 10} // School damage, % weapon damage, etc. + statBuffEffects := [...]int{35, 29, 99, 124} // Apply stat, add flat stat, etc. + periodEffects := [...]int{6, 77} // Apply aura, periodic trigger + + // Scale Effect1 + if s.EffectBasePoints1 != 0 { + effectMultiplier := 1.0 + + // 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 + } + + // 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 + } + + // Flat Base Stat modifier for all other stats. + if funk.Contains(statBuffEffects, s.Effect1) && effectMultiplier < 1.5 { + effectMultiplier = 1.45 + } + } + + // 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 + } + } + + // 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 + } + } + + // 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 + if s.EffectBasePoints2 != 0 { + effectMultiplier := 1.0 + + // 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 + } + + // Determine effect category and apply appropriate multiplier + if s.Effect2 != 0 { + // Direct damage effects scale more aggressively at higher item levels + if funk.Contains(directDamageEffects, s.Effect2) { + // Scale damage more aggressively for higher item levels + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) // +0.5% per every 10 item levels + } + + // Flat Base Stat modifier for all other stats. + if funk.Contains(statBuffEffects, s.Effect2) && effectMultiplier < 1.5 { + effectMultiplier = 1.45 + } + } + + // Special handling for aura effects + if s.EffectAura2 != 0 { + // DOT effects (Aura 3: Periodic Damage) + if s.EffectAura2 == 3 && funk.Contains(periodEffects, s.Effect2) { + 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) + if s.EffectAura2 == 8 && funk.Contains(periodEffects, s.Effect2) { + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) + // Healing scales slightly higher than damage + } + + // Damage Shield effects (Aura 15) + if s.EffectAura2 == 15 && funk.Contains(periodEffects, s.Effect2) { + // 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 + } + } + + // Special handling for mana restoration + if s.Effect2 == 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.EffectBasePoints2 = int(float64(s.EffectBasePoints2) * levelRatio * qualModifier * effectMultiplier) + } + + // Scale Effect3 with similar logic + if s.EffectBasePoints3 != 0 { + effectMultiplier := 1.0 + + // 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 + } + + // Determine effect category and apply appropriate multiplier + if s.Effect3 != 0 { + // Direct damage effects scale more aggressively at higher item levels + if funk.Contains(directDamageEffects, s.Effect3) { + // Scale damage more aggressively for higher item levels + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) // +0.5% per every 10 item levels + } + + // Flat Base Stat modifier for all other stats. + if funk.Contains(statBuffEffects, s.Effect3) && effectMultiplier < 1.5 { + effectMultiplier = 1.45 + } + } + // Special handling for aura effects + if s.EffectAura3 != 0 { + // DOT effects (Aura 3: Periodic Damage) + if s.EffectAura3 == 3 && funk.Contains(periodEffects, s.Effect3) { + 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) + if s.EffectAura3 == 8 && funk.Contains(periodEffects, s.Effect3) { + effectMultiplier = 2.5 + (float64(toItemLevel) * 0.1 * 0.005) + // Healing scales slightly higher than damage + } + + // Damage Shield effects (Aura 15) + if s.EffectAura3 == 15 && funk.Contains(periodEffects, s.Effect3) { + // 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 + } + } + + // Special handling for mana restoration + if s.Effect3 == 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.EffectBasePoints3 = int(float64(s.EffectBasePoints3) * levelRatio * qualModifier * effectMultiplier) + } + + // Update spell name with new values if it contains the old values + // This helps keep the spell name in sync with the actual effect values + newValues := []int{ + s.EffectBasePoints1 + 1, // Add 1 to match WoW's display format + s.EffectBasePoints2 + 1, + s.EffectBasePoints3 + 1, + } + + // Store the original name before any modifications + originalName := s.Name + + // Try different formats of the number that might appear in the name + for i, oldValue := range []int{originalBasePoints1, originalBasePoints2, originalBasePoints3} { + if oldValue <= 1 { // Skip if original value is 0 or 1 (likely not meaningful) + continue + } + + newValue := newValues[i] + if newValue <= 1 { // Skip if new value is 0 or 1 (likely not meaningful) + continue + } + + // Skip if values are the same (no change needed) + if oldValue == newValue { + continue + } + + // Convert both old and new values to strings + oldValueStr := fmt.Sprintf("%d", oldValue) + newValueStr := fmt.Sprintf("%d", newValue) + + // Only replace if the old value actually appears in the name + if strings.Contains(s.Name, oldValueStr) { + s.Name = strings.Replace(s.Name, oldValueStr, newValueStr, -1) + //log.Printf("Updated ID %d spell name from '%s' to '%s' (replaced %s with %s)", + // s.ID, originalName, s.Name, oldValueStr, newValueStr) + } + + // Also try with a plus sign (e.g., "+15" in name) + oldValueWithPlus := "+" + oldValueStr + newValueWithPlus := "+" + newValueStr + if strings.Contains(s.Name, oldValueWithPlus) { + s.Name = strings.Replace(s.Name, oldValueWithPlus, newValueWithPlus, -1) + log.Printf("Updated spell name from '%s' to '%s' (replaced %s with %s)", + originalName, s.Name, oldValueWithPlus, newValueWithPlus) + } + } + + s.Scaled = true + return nil +} + func SpellToSql(spell Spell, quality int) string { entryBump := 30000000 @@ -514,6 +850,42 @@ func SpellToSql(spell Spell, quality int) string { 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 + ) 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)