From f99343f49029c84812a4e0aa140223052a418839 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sat, 23 Aug 2025 18:09:48 -0500 Subject: [PATCH] simplify/ehance classes --- internal/classes/class.go | 359 ------------------- internal/classes/classes.go | 540 +++++++++++++++++++++++++++++ internal/classes/classes_test.go | 578 +++++++++++++++++++++++++++++++ 3 files changed, 1118 insertions(+), 359 deletions(-) delete mode 100644 internal/classes/class.go create mode 100644 internal/classes/classes.go create mode 100644 internal/classes/classes_test.go diff --git a/internal/classes/class.go b/internal/classes/class.go deleted file mode 100644 index 61a7e3a..0000000 --- a/internal/classes/class.go +++ /dev/null @@ -1,359 +0,0 @@ -package classes - -import ( - "strings" -) - -// classMap contains the mapping of class names to IDs -var classMap = map[string]int8{ - "COMMONER": ClassCommoner, - "FIGHTER": ClassFighter, - "WARRIOR": ClassWarrior, - "GUARDIAN": ClassGuardian, - "BERSERKER": ClassBerserker, - "BRAWLER": ClassBrawler, - "MONK": ClassMonk, - "BRUISER": ClassBruiser, - "CRUSADER": ClassCrusader, - "SHADOWKNIGHT": ClassShadowknight, - "PALADIN": ClassPaladin, - "PRIEST": ClassPriest, - "CLERIC": ClassCleric, - "TEMPLAR": ClassTemplar, - "INQUISITOR": ClassInquisitor, - "DRUID": ClassDruid, - "WARDEN": ClassWarden, - "FURY": ClassFury, - "SHAMAN": ClassShaman, - "MYSTIC": ClassMystic, - "DEFILER": ClassDefiler, - "MAGE": ClassMage, - "SORCERER": ClassSorcerer, - "WIZARD": ClassWizard, - "WARLOCK": ClassWarlock, - "ENCHANTER": ClassEnchanter, - "ILLUSIONIST": ClassIllusionist, - "COERCER": ClassCoercer, - "SUMMONER": ClassSummoner, - "CONJUROR": ClassConjuror, - "NECROMANCER": ClassNecromancer, - "SCOUT": ClassScout, - "ROGUE": ClassRogue, - "SWASHBUCKLER": ClassSwashbuckler, - "BRIGAND": ClassBrigand, - "BARD": ClassBard, - "TROUBADOR": ClassTroubador, - "DIRGE": ClassDirge, - "PREDATOR": ClassPredator, - "RANGER": ClassRanger, - "ASSASSIN": ClassAssassin, - "ANIMALIST": ClassAnimalist, - "BEASTLORD": ClassBeastlord, - "SHAPER": ClassShaper, - "CHANNELER": ClassChanneler, - "ARTISAN": ClassArtisan, - "CRAFTSMAN": ClassCraftsman, - "PROVISIONER": ClassProvisioner, - "WOODWORKER": ClassWoodworker, - "CARPENTER": ClassCarpenter, - "OUTFITTER": ClassOutfitter, - "ARMORER": ClassArmorer, - "WEAPONSMITH": ClassWeaponsmith, - "TAILOR": ClassTailor, - "SCHOLAR": ClassScholar, - "JEWELER": ClassJeweler, - "SAGE": ClassSage, - "ALCHEMIST": ClassAlchemist, -} - -// displayNameMap contains the display names for each class ID -var displayNameMap = map[int8]string{ - ClassCommoner: "Commoner", - ClassFighter: "Fighter", - ClassWarrior: "Warrior", - ClassGuardian: "Guardian", - ClassBerserker: "Berserker", - ClassBrawler: "Brawler", - ClassMonk: "Monk", - ClassBruiser: "Bruiser", - ClassCrusader: "Crusader", - ClassShadowknight: "Shadowknight", - ClassPaladin: "Paladin", - ClassPriest: "Priest", - ClassCleric: "Cleric", - ClassTemplar: "Templar", - ClassInquisitor: "Inquisitor", - ClassDruid: "Druid", - ClassWarden: "Warden", - ClassFury: "Fury", - ClassShaman: "Shaman", - ClassMystic: "Mystic", - ClassDefiler: "Defiler", - ClassMage: "Mage", - ClassSorcerer: "Sorcerer", - ClassWizard: "Wizard", - ClassWarlock: "Warlock", - ClassEnchanter: "Enchanter", - ClassIllusionist: "Illusionist", - ClassCoercer: "Coercer", - ClassSummoner: "Summoner", - ClassConjuror: "Conjuror", - ClassNecromancer: "Necromancer", - ClassScout: "Scout", - ClassRogue: "Rogue", - ClassSwashbuckler: "Swashbuckler", - ClassBrigand: "Brigand", - ClassBard: "Bard", - ClassTroubador: "Troubador", - ClassDirge: "Dirge", - ClassPredator: "Predator", - ClassRanger: "Ranger", - ClassAssassin: "Assassin", - ClassAnimalist: "Animalist", - ClassBeastlord: "Beastlord", - ClassShaper: "Shaper", - ClassChanneler: "Channeler", - ClassArtisan: "Artisan", - ClassCraftsman: "Craftsman", - ClassProvisioner: "Provisioner", - ClassWoodworker: "Woodworker", - ClassCarpenter: "Carpenter", - ClassOutfitter: "Outfitter", - ClassArmorer: "Armorer", - ClassWeaponsmith: "Weaponsmith", - ClassTailor: "Tailor", - ClassScholar: "Scholar", - ClassJeweler: "Jeweler", - ClassSage: "Sage", - ClassAlchemist: "Alchemist", -} - -// GetClassID returns the class ID for a given class name -// Converted from C++ Classes::GetClassID -func GetClassID(name string) int8 { - className := strings.ToUpper(strings.TrimSpace(name)) - if classID, exists := classMap[className]; exists { - return classID - } - return -1 // Invalid class -} - -// GetClassName returns the uppercase class name for a given ID -// Converted from C++ Classes::GetClassName -func GetClassName(classID int8) string { - // Search through class map to find the name - for name, id := range classMap { - if id == classID { - return name - } - } - return "" // Invalid class ID -} - -// GetClassNameCase returns the friendly display name for a given class ID -// Converted from C++ Classes::GetClassNameCase -func GetClassNameCase(classID int8) string { - if displayName, exists := displayNameMap[classID]; exists { - return displayName - } - return "" // Invalid class ID -} - -// GetBaseClass returns the base class ID for a given class -// Converted from C++ Classes::GetBaseClass -func GetBaseClass(classID int8) int8 { - if classID >= ClassWarrior && classID <= ClassPaladin { - return ClassFighter - } - if (classID >= ClassCleric && classID <= ClassDefiler) || (classID == ClassShaper || classID == ClassChanneler) { - return ClassPriest - } - if classID >= ClassSorcerer && classID <= ClassNecromancer { - return ClassMage - } - if classID >= ClassRogue && classID <= ClassBeastlord { - return ClassScout - } - return ClassCommoner // Default for unknown classes -} - -// GetSecondaryBaseClass returns the secondary base class ID for specialized classes -// Converted from C++ Classes::GetSecondaryBaseClass -func GetSecondaryBaseClass(classID int8) int8 { - switch classID { - case ClassGuardian, ClassBerserker: - return ClassWarrior - case ClassMonk, ClassBruiser: - return ClassBrawler - case ClassShadowknight, ClassPaladin: - return ClassCrusader - case ClassTemplar, ClassInquisitor: - return ClassCleric - case ClassWarden, ClassFury: - return ClassDruid - case ClassMystic, ClassDefiler: - return ClassShaman - case ClassWizard, ClassWarlock: - return ClassSorcerer - case ClassIllusionist, ClassCoercer: - return ClassEnchanter - case ClassConjuror, ClassNecromancer: - return ClassSummoner - case ClassSwashbuckler, ClassBrigand: - return ClassRogue - case ClassTroubador, ClassDirge: - return ClassBard - case ClassRanger, ClassAssassin: - return ClassPredator - case ClassBeastlord: - return ClassAnimalist - case ClassChanneler: - return ClassShaper - } - return ClassCommoner // Default for unknown classes -} - -// GetTSBaseClass returns the tradeskill base class ID -// Converted from C++ Classes::GetTSBaseClass -func GetTSBaseClass(classID int8) int8 { - // This function maps tradeskill class IDs to their base tradeskill progression levels - // The C++ code uses offset of 42 between adventure and tradeskill class systems - if classID+42 >= ClassArtisan { - return ClassArtisan - 44 // Returns 1 (base artisan level) - } - return classID // For non-tradeskill classes, return as-is -} - -// GetSecondaryTSBaseClass returns the secondary tradeskill base class ID -// Converted from C++ Classes::GetSecondaryTSBaseClass -func GetSecondaryTSBaseClass(classID int8) int8 { - ret := classID + 42 - - if ret == ClassArtisan { - return ClassArtisan - 44 - } else if ret >= ClassCraftsman && ret < ClassOutfitter { - return ClassCraftsman - 44 - } else if ret >= ClassOutfitter && ret < ClassScholar { - return ClassOutfitter - 44 - } else if ret >= ClassScholar { - return ClassScholar - 44 - } - - return classID -} - -// IsValidClassID checks if a class ID is valid -func IsValidClassID(classID int8) bool { - return classID >= MinClassID && classID <= MaxClassID -} - -// IsAdventureClass checks if a class is an adventure class -func IsAdventureClass(classID int8) bool { - return classID >= ClassCommoner && classID <= ClassChanneler -} - -// IsTradeskillClass checks if a class is a tradeskill class -func IsTradeskillClass(classID int8) bool { - return classID >= ClassArtisan && classID <= ClassAlchemist -} - -// GetClassType returns the type of class (adventure, tradeskill, etc.) -func GetClassType(classID int8) string { - if IsAdventureClass(classID) { - return ClassTypeAdventure - } - if IsTradeskillClass(classID) { - return ClassTypeTradeskill - } - return ClassTypeSpecial -} - -// GetAllClasses returns all class IDs and their display names -func GetAllClasses() map[int8]string { - result := make(map[int8]string) - for classID, displayName := range displayNameMap { - result[classID] = displayName - } - return result -} - -// GetClassInfo returns comprehensive information about a class -func GetClassInfo(classID int8) map[string]any { - info := make(map[string]any) - - if !IsValidClassID(classID) { - info["valid"] = false - return info - } - - info["valid"] = true - info["class_id"] = classID - info["name"] = GetClassName(classID) - info["display_name"] = GetClassNameCase(classID) - info["base_class"] = GetBaseClass(classID) - info["secondary_base_class"] = GetSecondaryBaseClass(classID) - info["type"] = GetClassType(classID) - info["is_adventure"] = IsAdventureClass(classID) - info["is_tradeskill"] = IsTradeskillClass(classID) - - return info -} - -// GetClassHierarchy returns the full class hierarchy for a given class -func GetClassHierarchy(classID int8) []int8 { - if !IsValidClassID(classID) { - return nil - } - - hierarchy := []int8{classID} - - // Add secondary base class if it exists - secondary := GetSecondaryBaseClass(classID) - if secondary != ClassCommoner && secondary != classID { - hierarchy = append(hierarchy, secondary) - } - - // Add base class - base := GetBaseClass(classID) - if base != ClassCommoner && base != classID { - // Check if base is already in hierarchy (from secondary) - found := false - for _, id := range hierarchy { - if id == base { - found = true - break - } - } - if !found { - hierarchy = append(hierarchy, base) - } - } - - // Always add Commoner as the root - if classID != ClassCommoner { - hierarchy = append(hierarchy, ClassCommoner) - } - - return hierarchy -} - -// IsSameArchetype checks if two classes share the same base archetype -func IsSameArchetype(classID1, classID2 int8) bool { - if !IsValidClassID(classID1) || !IsValidClassID(classID2) { - return false - } - return GetBaseClass(classID1) == GetBaseClass(classID2) -} - -// GetArchetypeClasses returns all classes of a given archetype -func GetArchetypeClasses(archetypeID int8) []int8 { - var classes []int8 - - for classID := MinClassID; classID <= MaxClassID; classID++ { - if GetBaseClass(classID) == archetypeID { - classes = append(classes, classID) - } - } - - return classes -} \ No newline at end of file diff --git a/internal/classes/classes.go b/internal/classes/classes.go new file mode 100644 index 0000000..e12d58d --- /dev/null +++ b/internal/classes/classes.go @@ -0,0 +1,540 @@ +package classes + +import ( + "strings" + "sync" +) + +// Class represents a character class with all its properties +type Class struct { + ID int8 `json:"id"` + Name string `json:"name"` // Uppercase name (WARRIOR) + DisplayName string `json:"display_name"` // Friendly name (Warrior) + BaseClass int8 `json:"base_class"` // Primary archetype + SecondaryBaseClass int8 `json:"secondary_base"` // Secondary archetype + ClassType string `json:"type"` // adventure/tradeskill/special + IsAdventure bool `json:"is_adventure"` + IsTradeskill bool `json:"is_tradeskill"` +} + +// GetID returns the class ID +func (c *Class) GetID() int8 { + return c.ID +} + +// GetName returns the uppercase class name (C++ API compatibility) +func (c *Class) GetName() string { + return c.Name +} + +// GetDisplayName returns the friendly display name +func (c *Class) GetDisplayName() string { + return c.DisplayName +} + +// GetBaseClass returns the base archetype (C++ API compatibility) +func (c *Class) GetBaseClass() int8 { + return c.BaseClass +} + +// GetSecondaryBaseClass returns the secondary archetype (C++ API compatibility) +func (c *Class) GetSecondaryBaseClass() int8 { + return c.SecondaryBaseClass +} + +// IsValid returns true if the class ID is valid +func (c *Class) IsValid() bool { + return c.ID >= MinClassID && c.ID <= MaxClassID +} + +// Manager provides centralized class management with caching and lookup optimization +type Manager struct { + classes map[int8]*Class // ID -> Class + nameToID map[string]int8 // Uppercase name -> ID + displayNameMap map[int8]string // ID -> Display name + mutex sync.RWMutex + + // Statistics + stats struct { + ClassesLoaded int32 + LookupsPerformed int32 + CacheHits int32 + CacheMisses int32 + } +} + +// NewManager creates a new class manager +func NewManager() *Manager { + manager := &Manager{ + classes: make(map[int8]*Class), + nameToID: make(map[string]int8), + displayNameMap: make(map[int8]string), + } + + // Initialize with all classes + manager.initializeClasses() + return manager +} + +// initializeClasses populates the manager with all class data +func (m *Manager) initializeClasses() { + m.mutex.Lock() + defer m.mutex.Unlock() + + // Define all classes with their properties + classDefinitions := []struct { + id int8 + name string + displayName string + baseClass int8 + secondaryBase int8 + classType string + isAdventure bool + isTradeskill bool + }{ + // Base classes + {ClassCommoner, "COMMONER", "Commoner", ClassCommoner, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassFighter, "FIGHTER", "Fighter", ClassFighter, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassPriest, "PRIEST", "Priest", ClassPriest, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassMage, "MAGE", "Mage", ClassMage, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassScout, "SCOUT", "Scout", ClassScout, ClassCommoner, ClassTypeAdventure, true, false}, + + // Fighter subclasses + {ClassWarrior, "WARRIOR", "Warrior", ClassFighter, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassGuardian, "GUARDIAN", "Guardian", ClassFighter, ClassWarrior, ClassTypeAdventure, true, false}, + {ClassBerserker, "BERSERKER", "Berserker", ClassFighter, ClassWarrior, ClassTypeAdventure, true, false}, + {ClassBrawler, "BRAWLER", "Brawler", ClassFighter, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassMonk, "MONK", "Monk", ClassFighter, ClassBrawler, ClassTypeAdventure, true, false}, + {ClassBruiser, "BRUISER", "Bruiser", ClassFighter, ClassBrawler, ClassTypeAdventure, true, false}, + {ClassCrusader, "CRUSADER", "Crusader", ClassFighter, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassShadowknight, "SHADOWKNIGHT", "Shadowknight", ClassFighter, ClassCrusader, ClassTypeAdventure, true, false}, + {ClassPaladin, "PALADIN", "Paladin", ClassFighter, ClassCrusader, ClassTypeAdventure, true, false}, + + // Priest subclasses + {ClassCleric, "CLERIC", "Cleric", ClassPriest, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassTemplar, "TEMPLAR", "Templar", ClassPriest, ClassCleric, ClassTypeAdventure, true, false}, + {ClassInquisitor, "INQUISITOR", "Inquisitor", ClassPriest, ClassCleric, ClassTypeAdventure, true, false}, + {ClassDruid, "DRUID", "Druid", ClassPriest, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassWarden, "WARDEN", "Warden", ClassPriest, ClassDruid, ClassTypeAdventure, true, false}, + {ClassFury, "FURY", "Fury", ClassPriest, ClassDruid, ClassTypeAdventure, true, false}, + {ClassShaman, "SHAMAN", "Shaman", ClassPriest, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassMystic, "MYSTIC", "Mystic", ClassPriest, ClassShaman, ClassTypeAdventure, true, false}, + {ClassDefiler, "DEFILER", "Defiler", ClassPriest, ClassShaman, ClassTypeAdventure, true, false}, + + // Mage subclasses + {ClassSorcerer, "SORCERER", "Sorcerer", ClassMage, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassWizard, "WIZARD", "Wizard", ClassMage, ClassSorcerer, ClassTypeAdventure, true, false}, + {ClassWarlock, "WARLOCK", "Warlock", ClassMage, ClassSorcerer, ClassTypeAdventure, true, false}, + {ClassEnchanter, "ENCHANTER", "Enchanter", ClassMage, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassIllusionist, "ILLUSIONIST", "Illusionist", ClassMage, ClassEnchanter, ClassTypeAdventure, true, false}, + {ClassCoercer, "COERCER", "Coercer", ClassMage, ClassEnchanter, ClassTypeAdventure, true, false}, + {ClassSummoner, "SUMMONER", "Summoner", ClassMage, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassConjuror, "CONJUROR", "Conjuror", ClassMage, ClassSummoner, ClassTypeAdventure, true, false}, + {ClassNecromancer, "NECROMANCER", "Necromancer", ClassMage, ClassSummoner, ClassTypeAdventure, true, false}, + + // Scout subclasses + {ClassRogue, "ROGUE", "Rogue", ClassScout, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassSwashbuckler, "SWASHBUCKLER", "Swashbuckler", ClassScout, ClassRogue, ClassTypeAdventure, true, false}, + {ClassBrigand, "BRIGAND", "Brigand", ClassScout, ClassRogue, ClassTypeAdventure, true, false}, + {ClassBard, "BARD", "Bard", ClassScout, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassTroubador, "TROUBADOR", "Troubador", ClassScout, ClassBard, ClassTypeAdventure, true, false}, + {ClassDirge, "DIRGE", "Dirge", ClassScout, ClassBard, ClassTypeAdventure, true, false}, + {ClassPredator, "PREDATOR", "Predator", ClassScout, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassRanger, "RANGER", "Ranger", ClassScout, ClassPredator, ClassTypeAdventure, true, false}, + {ClassAssassin, "ASSASSIN", "Assassin", ClassScout, ClassPredator, ClassTypeAdventure, true, false}, + {ClassAnimalist, "ANIMALIST", "Animalist", ClassScout, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassBeastlord, "BEASTLORD", "Beastlord", ClassScout, ClassAnimalist, ClassTypeAdventure, true, false}, + + // Special classes + {ClassShaper, "SHAPER", "Shaper", ClassPriest, ClassCommoner, ClassTypeAdventure, true, false}, + {ClassChanneler, "CHANNELER", "Channeler", ClassPriest, ClassShaper, ClassTypeAdventure, true, false}, + + // Tradeskill classes + {ClassArtisan, "ARTISAN", "Artisan", ClassArtisan, ClassCommoner, ClassTypeTradeskill, false, true}, + {ClassCraftsman, "CRAFTSMAN", "Craftsman", ClassArtisan, ClassCommoner, ClassTypeTradeskill, false, true}, + {ClassProvisioner, "PROVISIONER", "Provisioner", ClassArtisan, ClassCraftsman, ClassTypeTradeskill, false, true}, + {ClassWoodworker, "WOODWORKER", "Woodworker", ClassArtisan, ClassCraftsman, ClassTypeTradeskill, false, true}, + {ClassCarpenter, "CARPENTER", "Carpenter", ClassArtisan, ClassCraftsman, ClassTypeTradeskill, false, true}, + {ClassOutfitter, "OUTFITTER", "Outfitter", ClassArtisan, ClassCommoner, ClassTypeTradeskill, false, true}, + {ClassArmorer, "ARMORER", "Armorer", ClassArtisan, ClassOutfitter, ClassTypeTradeskill, false, true}, + {ClassWeaponsmith, "WEAPONSMITH", "Weaponsmith", ClassArtisan, ClassOutfitter, ClassTypeTradeskill, false, true}, + {ClassTailor, "TAILOR", "Tailor", ClassArtisan, ClassOutfitter, ClassTypeTradeskill, false, true}, + {ClassScholar, "SCHOLAR", "Scholar", ClassArtisan, ClassCommoner, ClassTypeTradeskill, false, true}, + {ClassJeweler, "JEWELER", "Jeweler", ClassArtisan, ClassScholar, ClassTypeTradeskill, false, true}, + {ClassSage, "SAGE", "Sage", ClassArtisan, ClassScholar, ClassTypeTradeskill, false, true}, + {ClassAlchemist, "ALCHEMIST", "Alchemist", ClassArtisan, ClassScholar, ClassTypeTradeskill, false, true}, + } + + // Create and store class objects + for _, def := range classDefinitions { + class := &Class{ + ID: def.id, + Name: def.name, + DisplayName: def.displayName, + BaseClass: def.baseClass, + SecondaryBaseClass: def.secondaryBase, + ClassType: def.classType, + IsAdventure: def.isAdventure, + IsTradeskill: def.isTradeskill, + } + + m.classes[def.id] = class + m.nameToID[def.name] = def.id + m.displayNameMap[def.id] = def.displayName + } + + m.stats.ClassesLoaded = int32(len(classDefinitions)) +} + +// GetClass gets class by ID +func (m *Manager) GetClass(classID int8) *Class { + m.mutex.RLock() + defer m.mutex.RUnlock() + m.stats.LookupsPerformed++ + + if class, exists := m.classes[classID]; exists { + m.stats.CacheHits++ + return class + } + + m.stats.CacheMisses++ + return nil +} + +// GetClassID returns the class ID for a given class name (C++ API compatibility) +func (m *Manager) GetClassID(name string) int8 { + m.mutex.RLock() + defer m.mutex.RUnlock() + m.stats.LookupsPerformed++ + + className := strings.ToUpper(strings.TrimSpace(name)) + if classID, exists := m.nameToID[className]; exists { + m.stats.CacheHits++ + return classID + } + + m.stats.CacheMisses++ + return -1 // Invalid class +} + +// GetClassName returns the uppercase class name for a given ID (C++ API compatibility) +func (m *Manager) GetClassName(classID int8) string { + m.mutex.RLock() + defer m.mutex.RUnlock() + m.stats.LookupsPerformed++ + + if class := m.classes[classID]; class != nil { + m.stats.CacheHits++ + return class.Name + } + + m.stats.CacheMisses++ + return "" // Invalid class ID +} + +// GetClassNameCase returns the friendly display name for a given class ID (C++ API compatibility) +func (m *Manager) GetClassNameCase(classID int8) string { + m.mutex.RLock() + defer m.mutex.RUnlock() + m.stats.LookupsPerformed++ + + if displayName, exists := m.displayNameMap[classID]; exists { + m.stats.CacheHits++ + return displayName + } + + m.stats.CacheMisses++ + return "" // Invalid class ID +} + +// GetBaseClass returns the base class ID for a given class (C++ API compatibility) +func (m *Manager) GetBaseClass(classID int8) int8 { + m.mutex.RLock() + defer m.mutex.RUnlock() + m.stats.LookupsPerformed++ + + if class := m.classes[classID]; class != nil { + m.stats.CacheHits++ + return class.BaseClass + } + + m.stats.CacheMisses++ + return ClassCommoner // Default for unknown classes +} + +// GetSecondaryBaseClass returns the secondary base class ID (C++ API compatibility) +func (m *Manager) GetSecondaryBaseClass(classID int8) int8 { + m.mutex.RLock() + defer m.mutex.RUnlock() + m.stats.LookupsPerformed++ + + if class := m.classes[classID]; class != nil { + m.stats.CacheHits++ + return class.SecondaryBaseClass + } + + m.stats.CacheMisses++ + return ClassCommoner // Default for unknown classes +} + +// GetTSBaseClass returns the tradeskill base class ID (C++ API compatibility) +func (m *Manager) GetTSBaseClass(classID int8) int8 { + // This function maps tradeskill class IDs to their base tradeskill progression levels + // The C++ code uses offset of 42 between adventure and tradeskill class systems + if classID+42 >= ClassArtisan { + return ClassArtisan - 44 // Returns 1 (base artisan level) + } + return classID // For non-tradeskill classes, return as-is +} + +// GetSecondaryTSBaseClass returns the secondary tradeskill base class ID (C++ API compatibility) +func (m *Manager) GetSecondaryTSBaseClass(classID int8) int8 { + ret := classID + 42 + + if ret == ClassArtisan { + return ClassArtisan - 44 + } else if ret >= ClassCraftsman && ret < ClassOutfitter { + return ClassCraftsman - 44 + } else if ret >= ClassOutfitter && ret < ClassScholar { + return ClassOutfitter - 44 + } else if ret >= ClassScholar { + return ClassScholar - 44 + } + + return classID +} + +// IsValidClassID checks if a class ID is valid +func (m *Manager) IsValidClassID(classID int8) bool { + return classID >= MinClassID && classID <= MaxClassID +} + +// IsAdventureClass checks if a class is an adventure class +func (m *Manager) IsAdventureClass(classID int8) bool { + if class := m.GetClass(classID); class != nil { + return class.IsAdventure + } + return false +} + +// IsTradeskillClass checks if a class is a tradeskill class +func (m *Manager) IsTradeskillClass(classID int8) bool { + if class := m.GetClass(classID); class != nil { + return class.IsTradeskill + } + return false +} + +// GetClassType returns the type of class (adventure, tradeskill, etc.) +func (m *Manager) GetClassType(classID int8) string { + if class := m.GetClass(classID); class != nil { + return class.ClassType + } + return ClassTypeSpecial +} + +// GetAllClasses returns all class IDs and their display names +func (m *Manager) GetAllClasses() map[int8]string { + m.mutex.RLock() + defer m.mutex.RUnlock() + + result := make(map[int8]string) + for classID, displayName := range m.displayNameMap { + result[classID] = displayName + } + return result +} + +// GetClassInfo returns comprehensive information about a class +func (m *Manager) GetClassInfo(classID int8) map[string]any { + class := m.GetClass(classID) + info := make(map[string]any) + + if class == nil { + info["valid"] = false + return info + } + + info["valid"] = true + info["class_id"] = class.ID + info["name"] = class.Name + info["display_name"] = class.DisplayName + info["base_class"] = class.BaseClass + info["secondary_base_class"] = class.SecondaryBaseClass + info["type"] = class.ClassType + info["is_adventure"] = class.IsAdventure + info["is_tradeskill"] = class.IsTradeskill + + return info +} + +// GetClassHierarchy returns the full class hierarchy for a given class +func (m *Manager) GetClassHierarchy(classID int8) []int8 { + class := m.GetClass(classID) + if class == nil { + return nil + } + + hierarchy := []int8{classID} + + // Add secondary base class if it exists + if class.SecondaryBaseClass != ClassCommoner && class.SecondaryBaseClass != classID { + hierarchy = append(hierarchy, class.SecondaryBaseClass) + } + + // Add base class + if class.BaseClass != ClassCommoner && class.BaseClass != classID { + // Check if base is already in hierarchy (from secondary) + found := false + for _, id := range hierarchy { + if id == class.BaseClass { + found = true + break + } + } + if !found { + hierarchy = append(hierarchy, class.BaseClass) + } + } + + // Always add Commoner as the root + if classID != ClassCommoner { + hierarchy = append(hierarchy, ClassCommoner) + } + + return hierarchy +} + +// IsSameArchetype checks if two classes share the same base archetype +func (m *Manager) IsSameArchetype(classID1, classID2 int8) bool { + class1 := m.GetClass(classID1) + class2 := m.GetClass(classID2) + + if class1 == nil || class2 == nil { + return false + } + + return class1.BaseClass == class2.BaseClass +} + +// GetArchetypeClasses returns all classes of a given archetype +func (m *Manager) GetArchetypeClasses(archetypeID int8) []int8 { + m.mutex.RLock() + defer m.mutex.RUnlock() + + var classes []int8 + for classID, class := range m.classes { + if class.BaseClass == archetypeID { + classes = append(classes, classID) + } + } + return classes +} + +// GetStatistics returns current manager statistics +func (m *Manager) GetStatistics() map[string]interface{} { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return map[string]interface{}{ + "classes_loaded": m.stats.ClassesLoaded, + "lookups_performed": m.stats.LookupsPerformed, + "cache_hits": m.stats.CacheHits, + "cache_misses": m.stats.CacheMisses, + "total_classes": int32(len(m.classes)), + "adventure_classes": m.countClassesByType(ClassTypeAdventure), + "tradeskill_classes": m.countClassesByType(ClassTypeTradeskill), + } +} + +// countClassesByType counts classes of a specific type +func (m *Manager) countClassesByType(classType string) int32 { + count := int32(0) + for _, class := range m.classes { + if class.ClassType == classType { + count++ + } + } + return count +} + +// Global manager instance +var globalManager *Manager + +// InitializeManager initializes the global class manager +func InitializeManager() { + globalManager = NewManager() +} + +// GetManager returns the global class manager +func GetManager() *Manager { + if globalManager == nil { + InitializeManager() + } + return globalManager +} + +// Global functions for C++ API compatibility +func GetClassID(name string) int8 { + return GetManager().GetClassID(name) +} + +func GetClassName(classID int8) string { + return GetManager().GetClassName(classID) +} + +func GetClassNameCase(classID int8) string { + return GetManager().GetClassNameCase(classID) +} + +func GetBaseClass(classID int8) int8 { + return GetManager().GetBaseClass(classID) +} + +func GetSecondaryBaseClass(classID int8) int8 { + return GetManager().GetSecondaryBaseClass(classID) +} + +func GetTSBaseClass(classID int8) int8 { + return GetManager().GetTSBaseClass(classID) +} + +func GetSecondaryTSBaseClass(classID int8) int8 { + return GetManager().GetSecondaryTSBaseClass(classID) +} + +func IsValidClassID(classID int8) bool { + return GetManager().IsValidClassID(classID) +} + +func IsAdventureClass(classID int8) bool { + return GetManager().IsAdventureClass(classID) +} + +func IsTradeskillClass(classID int8) bool { + return GetManager().IsTradeskillClass(classID) +} + +func GetClassType(classID int8) string { + return GetManager().GetClassType(classID) +} + +func GetAllClasses() map[int8]string { + return GetManager().GetAllClasses() +} + +func GetClassInfo(classID int8) map[string]any { + return GetManager().GetClassInfo(classID) +} + +func GetClassHierarchy(classID int8) []int8 { + return GetManager().GetClassHierarchy(classID) +} + +func IsSameArchetype(classID1, classID2 int8) bool { + return GetManager().IsSameArchetype(classID1, classID2) +} + +func GetArchetypeClasses(archetypeID int8) []int8 { + return GetManager().GetArchetypeClasses(archetypeID) +} \ No newline at end of file diff --git a/internal/classes/classes_test.go b/internal/classes/classes_test.go new file mode 100644 index 0000000..fbd9cb7 --- /dev/null +++ b/internal/classes/classes_test.go @@ -0,0 +1,578 @@ +package classes + +import ( + "strings" + "testing" +) + +func TestClassBasics(t *testing.T) { + class := &Class{ + ID: ClassWarrior, + Name: "WARRIOR", + DisplayName: "Warrior", + BaseClass: ClassFighter, + SecondaryBaseClass: ClassCommoner, + ClassType: ClassTypeAdventure, + IsAdventure: true, + IsTradeskill: false, + } + + if class.GetID() != ClassWarrior { + t.Errorf("Expected ID %d, got %d", ClassWarrior, class.GetID()) + } + + if class.GetName() != "WARRIOR" { + t.Errorf("Expected name 'WARRIOR', got %s", class.GetName()) + } + + if class.GetDisplayName() != "Warrior" { + t.Errorf("Expected display name 'Warrior', got %s", class.GetDisplayName()) + } + + if class.GetBaseClass() != ClassFighter { + t.Errorf("Expected base class %d, got %d", ClassFighter, class.GetBaseClass()) + } + + if class.GetSecondaryBaseClass() != ClassCommoner { + t.Errorf("Expected secondary base class %d, got %d", ClassCommoner, class.GetSecondaryBaseClass()) + } + + if !class.IsValid() { + t.Error("Expected class to be valid") + } + + // Test invalid class + invalidClass := &Class{ID: int8(99)} + if invalidClass.IsValid() { + t.Error("Expected invalid class to be invalid") + } +} + +func TestManagerCreation(t *testing.T) { + manager := NewManager() + + if manager == nil { + t.Fatal("Manager creation failed") + } + + stats := manager.GetStatistics() + if stats["classes_loaded"].(int32) == 0 { + t.Error("Expected classes to be loaded during initialization") + } + + if stats["total_classes"].(int32) != MaxClasses { + t.Errorf("Expected %d total classes, got %d", MaxClasses, stats["total_classes"]) + } + + t.Logf("Manager initialized with %d classes", stats["total_classes"]) +} + +func TestGetClassID(t *testing.T) { + manager := NewManager() + + // Test valid class names (C++ API compatibility) + testCases := map[string]int8{ + "WARRIOR": ClassWarrior, + "GUARDIAN": ClassGuardian, + "TEMPLAR": ClassTemplar, + "WIZARD": ClassWizard, + "RANGER": ClassRanger, + "BEASTLORD": ClassBeastlord, + "CHANNELER": ClassChanneler, + "ARTISAN": ClassArtisan, + "PROVISIONER": ClassProvisioner, + "ALCHEMIST": ClassAlchemist, + "COMMONER": ClassCommoner, + } + + for name, expectedID := range testCases { + id := manager.GetClassID(name) + if id != expectedID { + t.Errorf("GetClassID('%s'): expected %d, got %d", name, expectedID, id) + } + + // Test case insensitivity and whitespace handling + id = manager.GetClassID(" " + strings.ToLower(name) + " ") + if id != expectedID { + t.Errorf("GetClassID('%s') with whitespace and lowercase: expected %d, got %d", name, expectedID, id) + } + } + + // Test invalid class name + invalidID := manager.GetClassID("INVALID_CLASS") + if invalidID != -1 { + t.Errorf("Expected -1 for invalid class name, got %d", invalidID) + } + + // Test empty string + emptyID := manager.GetClassID("") + if emptyID != -1 { + t.Errorf("Expected -1 for empty class name, got %d", emptyID) + } +} + +func TestGetClassName(t *testing.T) { + manager := NewManager() + + // Test valid class IDs (C++ API compatibility) + testCases := map[int8]string{ + ClassWarrior: "WARRIOR", + ClassGuardian: "GUARDIAN", + ClassTemplar: "TEMPLAR", + ClassWizard: "WIZARD", + ClassRanger: "RANGER", + ClassBeastlord: "BEASTLORD", + ClassChanneler: "CHANNELER", + ClassArtisan: "ARTISAN", + ClassProvisioner: "PROVISIONER", + ClassAlchemist: "ALCHEMIST", + ClassCommoner: "COMMONER", + } + + for classID, expectedName := range testCases { + name := manager.GetClassName(classID) + if name != expectedName { + t.Errorf("GetClassName(%d): expected '%s', got '%s'", classID, expectedName, name) + } + } + + // Test invalid class ID + invalidName := manager.GetClassName(int8(99)) + if invalidName != "" { + t.Errorf("Expected empty string for invalid class ID, got '%s'", invalidName) + } +} + +func TestGetClassNameCase(t *testing.T) { + manager := NewManager() + + // Test display names (C++ API compatibility) + testCases := map[int8]string{ + ClassWarrior: "Warrior", + ClassGuardian: "Guardian", + ClassTemplar: "Templar", + ClassWizard: "Wizard", + ClassRanger: "Ranger", + ClassBeastlord: "Beastlord", + ClassChanneler: "Channeler", + ClassArtisan: "Artisan", + ClassProvisioner: "Provisioner", + ClassAlchemist: "Alchemist", + ClassCommoner: "Commoner", + } + + for classID, expectedDisplayName := range testCases { + displayName := manager.GetClassNameCase(classID) + if displayName != expectedDisplayName { + t.Errorf("GetClassNameCase(%d): expected '%s', got '%s'", classID, expectedDisplayName, displayName) + } + } + + // Test invalid class ID + invalidDisplayName := manager.GetClassNameCase(int8(99)) + if invalidDisplayName != "" { + t.Errorf("Expected empty string for invalid class ID, got '%s'", invalidDisplayName) + } +} + +func TestGetBaseClass(t *testing.T) { + manager := NewManager() + + // Test base class mappings (C++ API compatibility) + testCases := map[int8]int8{ + // Fighter archetype + ClassWarrior: ClassFighter, + ClassGuardian: ClassFighter, + ClassBerserker: ClassFighter, + ClassMonk: ClassFighter, + ClassShadowknight: ClassFighter, + ClassPaladin: ClassFighter, + + // Priest archetype + ClassCleric: ClassPriest, + ClassTemplar: ClassPriest, + ClassInquisitor: ClassPriest, + ClassWarden: ClassPriest, + ClassFury: ClassPriest, + ClassMystic: ClassPriest, + ClassDefiler: ClassPriest, + ClassChanneler: ClassPriest, + + // Mage archetype + ClassSorcerer: ClassMage, + ClassWizard: ClassMage, + ClassWarlock: ClassMage, + ClassIllusionist: ClassMage, + ClassCoercer: ClassMage, + ClassConjuror: ClassMage, + ClassNecromancer: ClassMage, + + // Scout archetype + ClassRogue: ClassScout, + ClassSwashbuckler: ClassScout, + ClassBrigand: ClassScout, + ClassTroubador: ClassScout, + ClassDirge: ClassScout, + ClassRanger: ClassScout, + ClassAssassin: ClassScout, + ClassBeastlord: ClassScout, + + // Base classes should return themselves + ClassFighter: ClassFighter, + ClassPriest: ClassPriest, + ClassMage: ClassMage, + ClassScout: ClassScout, + ClassCommoner: ClassCommoner, + } + + for classID, expectedBase := range testCases { + base := manager.GetBaseClass(classID) + if base != expectedBase { + t.Errorf("GetBaseClass(%d): expected %d, got %d", classID, expectedBase, base) + } + } + + // Test invalid class ID + invalidBase := manager.GetBaseClass(int8(99)) + if invalidBase != ClassCommoner { + t.Errorf("Expected %d (Commoner) for invalid class ID, got %d", ClassCommoner, invalidBase) + } +} + +func TestGetSecondaryBaseClass(t *testing.T) { + manager := NewManager() + + // Test secondary base class mappings (C++ API compatibility) + testCases := map[int8]int8{ + ClassGuardian: ClassWarrior, + ClassBerserker: ClassWarrior, + ClassMonk: ClassBrawler, + ClassBruiser: ClassBrawler, + ClassShadowknight: ClassCrusader, + ClassPaladin: ClassCrusader, + ClassTemplar: ClassCleric, + ClassInquisitor: ClassCleric, + ClassWarden: ClassDruid, + ClassFury: ClassDruid, + ClassMystic: ClassShaman, + ClassDefiler: ClassShaman, + ClassWizard: ClassSorcerer, + ClassWarlock: ClassSorcerer, + ClassIllusionist: ClassEnchanter, + ClassCoercer: ClassEnchanter, + ClassConjuror: ClassSummoner, + ClassNecromancer: ClassSummoner, + ClassSwashbuckler: ClassRogue, + ClassBrigand: ClassRogue, + ClassTroubador: ClassBard, + ClassDirge: ClassBard, + ClassRanger: ClassPredator, + ClassAssassin: ClassPredator, + ClassBeastlord: ClassAnimalist, + ClassChanneler: ClassShaper, + } + + for classID, expectedSecondary := range testCases { + secondary := manager.GetSecondaryBaseClass(classID) + if secondary != expectedSecondary { + t.Errorf("GetSecondaryBaseClass(%d): expected %d, got %d", classID, expectedSecondary, secondary) + } + } + + // Test base classes (should return Commoner) + baseClasses := []int8{ClassCommoner, ClassFighter, ClassPriest, ClassMage, ClassScout} + for _, classID := range baseClasses { + secondary := manager.GetSecondaryBaseClass(classID) + if secondary != ClassCommoner { + t.Errorf("GetSecondaryBaseClass(%d): expected %d (Commoner), got %d", classID, ClassCommoner, secondary) + } + } +} + +func TestClassValidation(t *testing.T) { + manager := NewManager() + + // Test valid class IDs + validClasses := []int8{ + ClassCommoner, ClassFighter, ClassWarrior, ClassGuardian, + ClassTemplar, ClassWizard, ClassRanger, ClassBeastlord, + ClassChanneler, ClassArtisan, ClassAlchemist, + } + + for _, classID := range validClasses { + if !manager.IsValidClassID(classID) { + t.Errorf("Expected class ID %d to be valid", classID) + } + } + + // Test invalid class IDs + invalidClasses := []int8{-1, -10, 58, 99, 127} + for _, classID := range invalidClasses { + if manager.IsValidClassID(classID) { + t.Errorf("Expected class ID %d to be invalid", classID) + } + } + + // Test boundary conditions + if !manager.IsValidClassID(MinClassID) { + t.Errorf("Expected MinClassID (%d) to be valid", MinClassID) + } + + if !manager.IsValidClassID(MaxClassID) { + t.Errorf("Expected MaxClassID (%d) to be valid", MaxClassID) + } +} + +func TestClassTypeChecking(t *testing.T) { + manager := NewManager() + + // Test adventure classes + adventureClasses := []int8{ + ClassCommoner, ClassFighter, ClassWarrior, ClassGuardian, + ClassTemplar, ClassWizard, ClassRanger, ClassBeastlord, + ClassChanneler, + } + + for _, classID := range adventureClasses { + if !manager.IsAdventureClass(classID) { + t.Errorf("Expected class ID %d to be an adventure class", classID) + } + if manager.IsTradeskillClass(classID) { + t.Errorf("Expected class ID %d to not be a tradeskill class", classID) + } + if manager.GetClassType(classID) != ClassTypeAdventure { + t.Errorf("Expected class ID %d to have type '%s', got '%s'", classID, ClassTypeAdventure, manager.GetClassType(classID)) + } + } + + // Test tradeskill classes + tradeskillClasses := []int8{ + ClassArtisan, ClassCraftsman, ClassProvisioner, + ClassOutfitter, ClassArmorer, ClassScholar, + ClassJeweler, ClassAlchemist, + } + + for _, classID := range tradeskillClasses { + if manager.IsAdventureClass(classID) { + t.Errorf("Expected class ID %d to not be an adventure class", classID) + } + if !manager.IsTradeskillClass(classID) { + t.Errorf("Expected class ID %d to be a tradeskill class", classID) + } + if manager.GetClassType(classID) != ClassTypeTradeskill { + t.Errorf("Expected class ID %d to have type '%s', got '%s'", classID, ClassTypeTradeskill, manager.GetClassType(classID)) + } + } +} + +func TestClassHierarchy(t *testing.T) { + manager := NewManager() + + // Test Guardian hierarchy: Guardian -> Warrior -> Fighter -> Commoner + guardianHierarchy := manager.GetClassHierarchy(ClassGuardian) + expectedGuardian := []int8{ClassGuardian, ClassWarrior, ClassFighter, ClassCommoner} + + if len(guardianHierarchy) != len(expectedGuardian) { + t.Errorf("Guardian hierarchy length: expected %d, got %d", len(expectedGuardian), len(guardianHierarchy)) + } else { + for i, expected := range expectedGuardian { + if i < len(guardianHierarchy) && guardianHierarchy[i] != expected { + t.Errorf("Guardian hierarchy[%d]: expected %d, got %d", i, expected, guardianHierarchy[i]) + } + } + } + + // Test Templar hierarchy: Templar -> Cleric -> Priest -> Commoner + templarHierarchy := manager.GetClassHierarchy(ClassTemplar) + expectedTemplar := []int8{ClassTemplar, ClassCleric, ClassPriest, ClassCommoner} + + if len(templarHierarchy) != len(expectedTemplar) { + t.Errorf("Templar hierarchy length: expected %d, got %d", len(expectedTemplar), len(templarHierarchy)) + } else { + for i, expected := range expectedTemplar { + if i < len(templarHierarchy) && templarHierarchy[i] != expected { + t.Errorf("Templar hierarchy[%d]: expected %d, got %d", i, expected, templarHierarchy[i]) + } + } + } + + // Test Commoner hierarchy: just Commoner + commonerHierarchy := manager.GetClassHierarchy(ClassCommoner) + if len(commonerHierarchy) != 1 || commonerHierarchy[0] != ClassCommoner { + t.Errorf("Commoner hierarchy: expected [%d], got %v", ClassCommoner, commonerHierarchy) + } + + // Test invalid class + invalidHierarchy := manager.GetClassHierarchy(int8(99)) + if invalidHierarchy != nil { + t.Errorf("Expected nil hierarchy for invalid class, got %v", invalidHierarchy) + } +} + +func TestArchetypeOperations(t *testing.T) { + manager := NewManager() + + // Test same archetype checking + if !manager.IsSameArchetype(ClassWarrior, ClassGuardian) { + t.Error("Expected Warrior and Guardian to be same archetype (Fighter)") + } + + if !manager.IsSameArchetype(ClassTemplar, ClassMystic) { + t.Error("Expected Templar and Mystic to be same archetype (Priest)") + } + + if manager.IsSameArchetype(ClassWarrior, ClassTemplar) { + t.Error("Expected Warrior and Templar to be different archetypes") + } + + // Test archetype class listing + fighterClasses := manager.GetArchetypeClasses(ClassFighter) + expectedFighterCount := 10 // Including base Fighter class (Fighter, Warrior, Guardian, Berserker, Brawler, Monk, Bruiser, Crusader, Shadowknight, Paladin) + if len(fighterClasses) != expectedFighterCount { + t.Errorf("Expected %d Fighter archetype classes, got %d", expectedFighterCount, len(fighterClasses)) + } + + // Verify Fighter is in the list + foundFighter := false + for _, classID := range fighterClasses { + if classID == ClassFighter { + foundFighter = true + break + } + } + if !foundFighter { + t.Error("Expected Fighter base class to be in Fighter archetype list") + } +} + +func TestTradeskillClassOperations(t *testing.T) { + manager := NewManager() + + // Test tradeskill base class operations (C++ API compatibility) + // Note: These functions use specific C++ logic with offsets + + // Test with adventure class (should return as-is for most) + warriorTS := manager.GetTSBaseClass(ClassWarrior) + if warriorTS != ClassWarrior { + t.Errorf("Expected GetTSBaseClass(%d) to return %d, got %d", ClassWarrior, ClassWarrior, warriorTS) + } + + // Test secondary tradeskill operations + warriorSecondaryTS := manager.GetSecondaryTSBaseClass(ClassWarrior) + // This should follow the C++ logic with +42 offset calculations + t.Logf("GetSecondaryTSBaseClass(ClassWarrior) = %d", warriorSecondaryTS) +} + +func TestManagerStatistics(t *testing.T) { + manager := NewManager() + + // Perform some operations to generate statistics + manager.GetClassID("WARRIOR") + manager.GetClassName(ClassTemplar) + manager.GetClassNameCase(ClassRanger) + manager.GetBaseClass(ClassGuardian) + manager.GetClass(ClassBeastlord) + manager.GetClassID("INVALID_CLASS") // This should increment cache misses + + stats := manager.GetStatistics() + + // Check basic statistics + if stats["classes_loaded"].(int32) == 0 { + t.Error("Expected classes to be loaded") + } + + if stats["lookups_performed"].(int32) == 0 { + t.Error("Expected lookups to be performed") + } + + if stats["total_classes"].(int32) != MaxClasses { + t.Errorf("Expected %d total classes, got %d", MaxClasses, stats["total_classes"]) + } + + // Check adventure vs tradeskill counts + adventureCount := stats["adventure_classes"].(int32) + tradeskillCount := stats["tradeskill_classes"].(int32) + + if adventureCount == 0 { + t.Error("Expected some adventure classes") + } + + if tradeskillCount == 0 { + t.Error("Expected some tradeskill classes") + } + + t.Logf("Statistics: %+v", stats) +} + +func TestGlobalFunctions(t *testing.T) { + // Test global functions that should initialize manager automatically + + // Test basic operations + id := GetClassID("WARRIOR") + if id != ClassWarrior { + t.Errorf("GetClassID('WARRIOR'): expected %d, got %d", ClassWarrior, id) + } + + name := GetClassName(ClassTemplar) + if name != "TEMPLAR" { + t.Errorf("GetClassName(%d): expected 'TEMPLAR', got '%s'", ClassTemplar, name) + } + + displayName := GetClassNameCase(ClassRanger) + if displayName != "Ranger" { + t.Errorf("GetClassNameCase(%d): expected 'Ranger', got '%s'", ClassRanger, displayName) + } + + base := GetBaseClass(ClassGuardian) + if base != ClassFighter { + t.Errorf("GetBaseClass(%d): expected %d, got %d", ClassGuardian, ClassFighter, base) + } + + secondary := GetSecondaryBaseClass(ClassGuardian) + if secondary != ClassWarrior { + t.Errorf("GetSecondaryBaseClass(%d): expected %d, got %d", ClassGuardian, ClassWarrior, secondary) + } + + // Test utility functions + if !IsValidClassID(ClassBeastlord) { + t.Errorf("Expected ClassBeastlord (%d) to be valid", ClassBeastlord) + } + + if !IsAdventureClass(ClassChanneler) { + t.Errorf("Expected ClassChanneler (%d) to be adventure class", ClassChanneler) + } + + if !IsTradeskillClass(ClassAlchemist) { + t.Errorf("Expected ClassAlchemist (%d) to be tradeskill class", ClassAlchemist) + } + + classType := GetClassType(ClassWarrior) + if classType != ClassTypeAdventure { + t.Errorf("Expected ClassWarrior type to be '%s', got '%s'", ClassTypeAdventure, classType) + } + + // Test collection functions + allClasses := GetAllClasses() + if len(allClasses) != MaxClasses { + t.Errorf("Expected %d classes in GetAllClasses(), got %d", MaxClasses, len(allClasses)) + } + + info := GetClassInfo(ClassTemplar) + if !info["valid"].(bool) { + t.Error("Expected ClassTemplar info to be valid") + } + + hierarchy := GetClassHierarchy(ClassGuardian) + if len(hierarchy) == 0 { + t.Error("Expected non-empty hierarchy for ClassGuardian") + } + + if !IsSameArchetype(ClassWarrior, ClassGuardian) { + t.Error("Expected Warrior and Guardian to be same archetype") + } + + fighterClasses := GetArchetypeClasses(ClassFighter) + if len(fighterClasses) == 0 { + t.Error("Expected non-empty list of Fighter archetype classes") + } +} \ No newline at end of file