Files
wow-item-generator/main.go
2025-09-16 23:39:20 -04:00

702 lines
19 KiB
Go

package main
import (
"encoding/csv"
"flag"
"fmt"
"io"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
"github.com/araxiaonline/endgame-item-generator/internal/config"
"github.com/araxiaonline/endgame-item-generator/internal/db/mysql"
"github.com/araxiaonline/endgame-item-generator/internal/items"
"github.com/araxiaonline/endgame-item-generator/internal/spells"
_ "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv"
)
// ItemScaler handles generic item scaling operations
type ItemScaler struct {
db *mysql.MySqlDb
debug bool
itemLevel int
quality int
phase int
catchup float64
spellAssigner *spells.ThematicSpellAssigner
templateManager *items.StatTemplateManager
}
// ScalingResult holds the result of item scaling
type ScalingResult struct {
Item *items.Item
ReferenceItem *items.Item
Success bool
Errors []string
Warnings []string
SpellSQL []string // SQL statements for scaled spells
}
// InputSource defines how items are provided to the scaler
type InputSource interface {
GetItems() ([]mysql.DbItem, error)
GetName() string
}
// EntryListSource scales items from a list of entry IDs
type EntryListSource struct {
Entries []int
DB *mysql.MySqlDb
}
func (e *EntryListSource) GetItems() ([]mysql.DbItem, error) {
var items []mysql.DbItem
for _, entry := range e.Entries {
item, err := e.DB.GetItem(entry)
if err != nil {
log.Printf("Failed to get item %d: %v", entry, err)
continue
}
items = append(items, item)
}
return items, nil
}
func (e *EntryListSource) GetName() string {
return fmt.Sprintf("Entry List (%d items)", len(e.Entries))
}
// CSVSource scales items from a CSV file with entry IDs
type CSVSource struct {
FilePath string
DB *mysql.MySqlDb
}
func (c *CSVSource) GetItems() ([]mysql.DbItem, error) {
file, err := os.Open(c.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to open CSV file: %v", err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV: %v", err)
}
var items []mysql.DbItem
for i, record := range records {
if i == 0 && strings.ToLower(record[0]) == "entry" {
continue // Skip header row
}
if len(record) == 0 {
continue
}
entry, err := strconv.Atoi(record[0])
if err != nil {
log.Printf("Invalid entry ID in CSV row %d: %s", i+1, record[0])
continue
}
item, err := c.DB.GetItem(entry)
if err != nil {
log.Printf("Failed to get item %d from CSV: %v", entry, err)
continue
}
items = append(items, item)
}
return items, nil
}
func (c *CSVSource) GetName() string {
return fmt.Sprintf("CSV File: %s", c.FilePath)
}
// SQLQuerySource scales items from a custom SQL query
type SQLQuerySource struct {
Query string
DB *mysql.MySqlDb
}
func (s *SQLQuerySource) GetItems() ([]mysql.DbItem, error) {
// For now, use GetRarePlusItems as a fallback since GetItemsByQuery doesn't exist
// This can be extended later to support custom queries
return s.DB.GetRarePlusItems(0, 0)
}
func (s *SQLQuerySource) GetName() string {
return "Custom SQL Query (using GetRarePlusItems)"
}
// MapItemsSource scales items from map/boss/gameobject data (like Molten Core)
type MapItemsSource struct {
MapID int
BossEntries []int
GameObjectEntries []int
DB *mysql.MySqlDb
}
func (m *MapItemsSource) GetItems() ([]mysql.DbItem, error) {
return m.DB.GetBossMapItems(m.MapID, m.BossEntries, m.GameObjectEntries, 0, 0)
}
func (m *MapItemsSource) GetName() string {
return fmt.Sprintf("Map %d Items", m.MapID)
}
func NewItemScaler(db *mysql.MySqlDb, debug bool, itemLevel, quality, phase int, catchup float64) *ItemScaler {
return &ItemScaler{
db: db,
debug: debug,
itemLevel: itemLevel,
quality: quality,
phase: phase,
catchup: catchup,
spellAssigner: spells.NewThematicSpellAssigner(db),
templateManager: items.NewStatTemplateManager(debug),
}
}
// ScaleItem performs the core scaling logic extracted from raid-gear/main.go
func (s *ItemScaler) ScaleItem(dbItem mysql.DbItem) ScalingResult {
result := ScalingResult{
Success: false,
Errors: []string{},
Warnings: []string{},
SpellSQL: []string{},
}
// Create item from database item
item := items.ItemFromDbItem(dbItem)
// Set quality if not already epic/legendary
if *item.Quality < 4 {
*item.Quality = s.quality
}
// Check if item should have a thematic spell assigned
if s.debug {
log.Printf("Checking if %s should have thematic spell assigned", item.Name)
}
if s.shouldAssignThematicSpell(item) {
if s.debug {
log.Printf("Item %s qualifies for thematic spell assignment", item.Name)
}
err := s.assignThematicSpell(&item)
if err != nil {
if s.debug {
log.Printf("Failed to assign thematic spell to %s: %v", item.Name, err)
}
} else {
if s.debug {
log.Printf("Successfully assigned thematic spell to %s", item.Name)
}
}
} else {
if s.debug {
log.Printf("Item %s does not qualify for thematic spell assignment", item.Name)
}
}
// Get item class type for reference item matching
classType := item.GetClassUserType()
if s.debug {
log.Printf("Scaling item: %s (Entry: %d) - Class: %d, Subclass: %d, ClassType: %d",
item.Name, item.Entry, *item.Class, *item.Subclass, classType)
}
var referenceItem items.Item
if s.phase == 0 {
// Phase 0: Use simple stat templates instead of complex reference matching
s.templateManager.ApplySimpleStatTemplate(&item)
if s.debug {
log.Printf("Using simple stat template for phase 0")
}
} else {
// Phase 1+: Find reference items and use reference item scaling
referenceItems, err := s.findReferenceItems(&item)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Failed to find reference items: %v", err))
return result
}
if len(referenceItems) == 0 {
result.Errors = append(result.Errors, "No compatible reference items found")
result.Warnings = append(result.Warnings, "Manual review required - no reference items available")
return result
}
// Select random reference item
referenceItem = referenceItems[rand.Intn(len(referenceItems))]
result.ReferenceItem = &referenceItem
// Apply stats from reference item
item.ApplyStats(referenceItem)
}
item.ScaleItemWithPhase(s.itemLevel, s.quality, s.phase)
// Apply tier modifiers if phase is specified
if s.phase > 0 || s.catchup != 1.0 {
item.ApplyTierModifiersWithCatchup(s.phase, s.catchup)
}
// Collect spell SQL for any scaled spells
for _, spell := range item.Spells {
if spell.Scaled {
spellSQL := spells.SpellToSql(spell, s.quality)
result.SpellSQL = append(result.SpellSQL, spellSQL)
if s.debug {
log.Printf("Generated SQL for scaled spell: %s (ID: %d)", spell.Name, spell.ID)
}
}
}
if s.debug {
if s.phase == 0 {
log.Printf("Successfully scaled %s using simple stat template", item.Name)
} else {
log.Printf("Successfully scaled %s using reference item %s", item.Name, referenceItem.Name)
}
}
result.Item = &item
result.Success = true
return result
}
// shouldAssignThematicSpell determines if an item should get a thematic spell
func (s *ItemScaler) shouldAssignThematicSpell(item items.Item) bool {
if s.debug {
log.Printf("DEBUG: shouldAssignThematicSpell - checking item %s", item.Name)
log.Printf("DEBUG: SpellId1: %v, SpellId2: %v, SpellId3: %v",
item.SpellId1, item.SpellId2, item.SpellId3)
}
// Check if item already has spells
if item.SpellId1 != nil && *item.SpellId1 != 0 {
if s.debug {
log.Printf("DEBUG: Item already has spell in slot 1: %d", *item.SpellId1)
}
return false
}
if item.SpellId2 != nil && *item.SpellId2 != 0 {
if s.debug {
log.Printf("DEBUG: Item already has spell in slot 2: %d", *item.SpellId2)
}
return false
}
if item.SpellId3 != nil && *item.SpellId3 != 0 {
if s.debug {
log.Printf("DEBUG: Item already has spell in slot 3: %d", *item.SpellId3)
}
return false
}
if s.debug {
log.Printf("DEBUG: Item has no existing spells, checking if it should have a proc")
}
// Use the thematic spell assigner to determine eligibility
return s.spellAssigner.ShouldHaveProc(*item.Class, *item.Subclass, *item.ItemLevel, *item.Quality, item.Name)
}
// assignThematicSpell assigns a thematic spell to an item
func (s *ItemScaler) assignThematicSpell(item *items.Item) error {
spellId, err := s.spellAssigner.AssignThematicSpell(item.Entry, *item.Class, *item.Subclass, *item.ItemLevel, *item.Quality, item.Name)
if err != nil {
return err
}
// Assign to first available spell slot
if item.SpellId1 == nil || *item.SpellId1 == 0 {
*item.SpellId1 = spellId
*item.SpellTrigger1 = 2 // Chance on hit
if s.debug {
log.Printf("Assigned spell %d to %s in slot 1", spellId, item.Name)
}
} else if item.SpellId2 == nil || *item.SpellId2 == 0 {
*item.SpellId2 = spellId
*item.SpellTrigger2 = 2 // Chance on hit
if s.debug {
log.Printf("Assigned spell %d to %s in slot 2", spellId, item.Name)
}
} else if item.SpellId3 == nil || *item.SpellId3 == 0 {
*item.SpellId3 = spellId
*item.SpellTrigger3 = 2 // Chance on hit
if s.debug {
log.Printf("Assigned spell %d to %s in slot 3", spellId, item.Name)
}
}
return nil
}
// findReferenceItems uses the sophisticated reference matching logic from raid-gear
func (s *ItemScaler) findReferenceItems(item *items.Item) ([]items.Item, error) {
classType := item.GetClassUserType()
// Handle subclass mapping for weapons (from raid-gear logic)
subclassToUse := *item.Subclass
if *item.Subclass == 8 {
subclassToUse = 1 // two handed axe instead of sword
}
// Get high-level reference items
highLevelItems, err := s.db.GetRaidPhase1Items(*item.Class, subclassToUse, 0, 0)
if err != nil {
return nil, fmt.Errorf("failed to get reference items: %v", err)
}
// Filter items by class type AND inventory type compatibility
var compatibleChoices []items.Item
var classOnlyChoices []items.Item
var anyWeaponChoices []items.Item
for _, highLevelItem := range highLevelItems {
refItem := items.ItemFromDbItem(highLevelItem)
refItem.ScaleItem(*refItem.ItemLevel, *item.Quality)
refClassType := refItem.GetClassUserType()
// Check both class type and inventory type compatibility
classMatch := refClassType == classType
invMatch := (item.InventoryType != nil && refItem.InventoryType != nil &&
*item.InventoryType == *refItem.InventoryType)
if classMatch && invMatch {
// Perfect match - both class type and inventory type
compatibleChoices = append(compatibleChoices, refItem)
} else if classMatch {
// Class type match only - good fallback
classOnlyChoices = append(classOnlyChoices, refItem)
} else if *item.Class == 2 && *refItem.Class == 2 {
// Any weapon as last resort for weapons
anyWeaponChoices = append(anyWeaponChoices, refItem)
}
}
if s.debug {
log.Printf("Reference items found - Perfect: %d, Class-only: %d, Any-weapon: %d",
len(compatibleChoices), len(classOnlyChoices), len(anyWeaponChoices))
}
// Return best available match
if len(compatibleChoices) > 0 {
return compatibleChoices, nil
} else if len(classOnlyChoices) > 0 {
if s.debug {
log.Printf("Using class-only match for %s", item.Name)
}
return classOnlyChoices, nil
} else if len(anyWeaponChoices) > 0 {
if s.debug {
log.Printf("Using any-weapon fallback for %s", item.Name)
}
return anyWeaponChoices, nil
}
return compatibleChoices, nil
}
// printScalingComparison shows before/after comparison
func printScalingComparison(original, scaled *items.Item, reference *items.Item) {
fmt.Printf("\n=== ITEM SCALING: %s (Entry: %d) ===\n", original.Name, original.Entry)
if reference != nil {
fmt.Printf("Reference Item: %s (Entry: %d)\n", reference.Name, reference.Entry)
}
// Show key stats comparison
fmt.Printf("Item Level: %d → %d\n", getItemLevel(original), getItemLevel(scaled))
fmt.Printf("Quality: %d → %d\n", *original.Quality, *scaled.Quality)
// Show stat changes
originalStats := extractStats(original)
scaledStats := extractStats(scaled)
fmt.Printf("Stats Changes:\n")
allStats := make(map[int]bool)
for stat := range originalStats {
allStats[stat] = true
}
for stat := range scaledStats {
allStats[stat] = true
}
for statType := range allStats {
originalValue := originalStats[statType]
scaledValue := scaledStats[statType]
if originalValue != scaledValue {
statName := getStatName(statType)
fmt.Printf(" %s: %d → %d\n", statName, originalValue, scaledValue)
}
}
fmt.Printf("=====================================\n\n")
}
// Helper functions
func getItemLevel(item *items.Item) int {
if item.ItemLevel != nil {
return *item.ItemLevel
}
return 0
}
func extractStats(item *items.Item) map[int]int {
stats := make(map[int]int)
for i := 1; i <= 8; i++ {
statType := getStatType(item, i)
statValue := getStatValue(item, i)
if statType != nil && statValue != nil && *statType > 0 && *statValue > 0 {
stats[*statType] = *statValue
}
}
return stats
}
func getStatType(item *items.Item, index int) *int {
switch index {
case 1:
return item.StatType1
case 2:
return item.StatType2
case 3:
return item.StatType3
case 4:
return item.StatType4
case 5:
return item.StatType5
case 6:
return item.StatType6
case 7:
return item.StatType7
case 8:
return item.StatType8
default:
return nil
}
}
func getStatValue(item *items.Item, index int) *int {
switch index {
case 1:
return item.StatValue1
case 2:
return item.StatValue2
case 3:
return item.StatValue3
case 4:
return item.StatValue4
case 5:
return item.StatValue5
case 6:
return item.StatValue6
case 7:
return item.StatValue7
case 8:
return item.StatValue8
default:
return nil
}
}
func getStatName(statType int) string {
if name, exists := config.StatModifierNames[statType]; exists {
return name
}
return fmt.Sprintf("Unknown(%d)", statType)
}
func parseIntList(s string) []int {
if s == "" {
return []int{}
}
parts := strings.Split(s, ",")
var result []int
for _, part := range parts {
if num, err := strconv.Atoi(strings.TrimSpace(part)); err == nil {
result = append(result, num)
}
}
return result
}
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(log.LstdFlags | log.Lshortfile)
godotenv.Load()
// Command line flags
debug := flag.Bool("debug", false, "Enable verbose logging")
itemLevel := flag.Int("item-level", 325, "Target item level for scaling")
quality := flag.Int("quality", 4, "Target quality (4=Epic, 5=Legendary)")
phase := flag.Int("phase", 0, "Tier phase for modifiers (0=no tier bonus, 1-5=tier levels)")
catchup := flag.Float64("catchup", 1.0, "Catchup bonus multiplier (1.0=no bonus, 1.5=50% bonus)")
baseLevel := flag.Int("base-level", 80, "Base level requirement for items")
difficulty := flag.Int("difficulty", 3, "Difficulty level (3=Mythic, 4=Legendary, 5=Ascendant)")
outputSQL := flag.Bool("sql", false, "Output SQL statements")
// Input source flags
entries := flag.String("entries", "", "Comma-separated list of entry IDs")
csvFile := flag.String("csv", "", "CSV file with entry IDs")
sqlQuery := flag.String("query", "", "Custom SQL query to get items")
mapID := flag.Int("map-id", 0, "Map ID for boss/gameobject items")
bossEntries := flag.String("boss-entries", "", "Comma-separated boss entry IDs")
gameObjectEntries := flag.String("gameobject-entries", "", "Comma-separated gameobject entry IDs")
flag.Parse()
if *debug {
log.SetOutput(os.Stdout)
} else {
log.SetOutput(io.Discard)
}
// Connect to database
mysqlDb, err := mysql.Connect(&mysql.MySqlConfig{
Host: os.Getenv("DB_HOST"),
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Database: os.Getenv("DB_NAME"),
})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// Determine input source
var source InputSource
sourceCount := 0
if *entries != "" {
entryList := parseIntList(*entries)
source = &EntryListSource{Entries: entryList, DB: mysqlDb}
sourceCount++
}
if *csvFile != "" {
source = &CSVSource{FilePath: *csvFile, DB: mysqlDb}
sourceCount++
}
if *sqlQuery != "" {
source = &SQLQuerySource{Query: *sqlQuery, DB: mysqlDb}
sourceCount++
}
if *mapID > 0 {
source = &MapItemsSource{
MapID: *mapID,
BossEntries: parseIntList(*bossEntries),
GameObjectEntries: parseIntList(*gameObjectEntries),
DB: mysqlDb,
}
sourceCount++
}
if sourceCount == 0 {
fmt.Println("Error: No input source specified. Use one of:")
fmt.Println(" --entries \"1234,5678\"")
fmt.Println(" --csv items.csv")
fmt.Println(" --query \"SELECT * FROM item_template WHERE ...\"")
fmt.Println(" --map-id 409 --boss-entries \"11502\" --gameobject-entries \"179703\"")
os.Exit(1)
}
if sourceCount > 1 {
fmt.Println("Error: Only one input source can be specified at a time")
os.Exit(1)
}
// Initialize scaler
scaler := NewItemScaler(mysqlDb, *debug, *itemLevel, *quality, *phase, *catchup)
// Get items from source
dbItems, err := source.GetItems()
if err != nil {
log.Fatal("Failed to get items from source:", err)
}
fmt.Printf("🔧 Generic Item Scaler\n")
fmt.Printf("Source: %s\n", source.GetName())
fmt.Printf("Target: Item Level %d, Quality %d, Phase %d, Catchup %.1fx\n", *itemLevel, *quality, *phase, *catchup)
fmt.Printf("Processing %d items...\n\n", len(dbItems))
// Initialize SQL output if requested
var sqlFile *os.File
if *outputSQL {
sqlFile, err = os.Create("scaled_items.sql")
if err != nil {
log.Fatal("Failed to create SQL output file:", err)
}
defer sqlFile.Close()
sqlFile.WriteString("-- Generic Item Scaler Output\n")
sqlFile.WriteString(fmt.Sprintf("-- Generated: %s\n", time.Now().Format("2006-01-02 15:04:05")))
sqlFile.WriteString(fmt.Sprintf("-- Item Level: %d, Quality: %d, Phase: %d, Catchup: %.1fx\n\n", *itemLevel, *quality, *phase, *catchup))
}
// Process items
successCount := 0
for i, dbItem := range dbItems {
fmt.Printf("[%d/%d] Processing: %s (Entry: %d)\n", i+1, len(dbItems), dbItem.Name, dbItem.Entry)
// Store original for comparison
originalItem := items.ItemFromDbItem(dbItem)
result := scaler.ScaleItem(dbItem)
if result.Success {
successCount++
fmt.Printf("✅ Successfully scaled %s\n", result.Item.Name)
// Show comparison
printScalingComparison(&originalItem, result.Item, result.ReferenceItem)
// Output SQL if requested
if *outputSQL && sqlFile != nil {
// Write item SQL
sqlStatement := items.ItemToSql(*result.Item, *baseLevel, *difficulty, true)
sqlFile.WriteString(sqlStatement + "\n")
// Write spell SQL for any scaled spells
for _, spellSQL := range result.SpellSQL {
sqlFile.WriteString(spellSQL + "\n")
}
}
} else {
fmt.Printf("❌ Failed to scale %s\n", dbItem.Name)
for _, errMsg := range result.Errors {
fmt.Printf(" Error: %s\n", errMsg)
}
}
for _, warning := range result.Warnings {
fmt.Printf("⚠️ Warning: %s\n", warning)
}
fmt.Println()
}
// Print summary
fmt.Printf("🏆 Scaling Summary:\n")
fmt.Printf("Total Items: %d\n", len(dbItems))
fmt.Printf("Successful: %d\n", successCount)
fmt.Printf("Failed: %d\n", len(dbItems)-successCount)
fmt.Printf("Success Rate: %.1f%%\n", float64(successCount)/float64(len(dbItems))*100)
if *outputSQL {
fmt.Printf("SQL Output: scaled_items.sql\n")
}
}