diff --git a/internal/classes/class.go b/internal/classes/class.go new file mode 100644 index 0000000..61a7e3a --- /dev/null +++ b/internal/classes/class.go @@ -0,0 +1,359 @@ +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/class_test.go b/internal/classes/class_test.go new file mode 100644 index 0000000..9825a17 --- /dev/null +++ b/internal/classes/class_test.go @@ -0,0 +1,372 @@ +package classes + +import ( + "testing" +) + +func TestGetClassID(t *testing.T) { + tests := []struct { + name string + input string + expected int8 + }{ + {"Uppercase", "WARRIOR", ClassWarrior}, + {"Lowercase", "warrior", ClassWarrior}, + {"Mixed case", "WaRrIoR", ClassWarrior}, + {"With spaces", " WARRIOR ", ClassWarrior}, + {"Invalid", "INVALID_CLASS", -1}, + {"Empty", "", -1}, + {"Tradeskill", "CARPENTER", ClassCarpenter}, + {"Special", "CHANNELER", ClassChanneler}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetClassID(tt.input) + if result != tt.expected { + t.Errorf("GetClassID(%q) = %d, want %d", tt.input, result, tt.expected) + } + }) + } +} + +func TestGetClassName(t *testing.T) { + tests := []struct { + classID int8 + expected string + }{ + {ClassWarrior, "WARRIOR"}, + {ClassPriest, "PRIEST"}, + {ClassCarpenter, "CARPENTER"}, + {ClassChanneler, "CHANNELER"}, + {-1, ""}, + {100, ""}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetClassName(tt.classID) + if result != tt.expected { + t.Errorf("GetClassName(%d) = %q, want %q", tt.classID, result, tt.expected) + } + }) + } +} + +func TestGetClassNameCase(t *testing.T) { + tests := []struct { + classID int8 + expected string + }{ + {ClassWarrior, "Warrior"}, + {ClassShadowknight, "Shadowknight"}, + {ClassTroubador, "Troubador"}, + {ClassCarpenter, "Carpenter"}, + {-1, ""}, + {100, ""}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetClassNameCase(tt.classID) + if result != tt.expected { + t.Errorf("GetClassNameCase(%d) = %q, want %q", tt.classID, result, tt.expected) + } + }) + } +} + +func TestGetBaseClass(t *testing.T) { + tests := []struct { + classID int8 + expected int8 + }{ + {ClassGuardian, ClassFighter}, + {ClassBerserker, ClassFighter}, + {ClassTemplar, ClassPriest}, + {ClassWizard, ClassMage}, + {ClassRanger, ClassScout}, + {ClassChanneler, ClassPriest}, + {ClassCommoner, ClassCommoner}, + {ClassFighter, ClassCommoner}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetBaseClass(tt.classID) + if result != tt.expected { + t.Errorf("GetBaseClass(%d) = %d, want %d", tt.classID, result, tt.expected) + } + }) + } +} + +func TestGetSecondaryBaseClass(t *testing.T) { + tests := []struct { + classID int8 + expected int8 + }{ + {ClassGuardian, ClassWarrior}, + {ClassBerserker, ClassWarrior}, + {ClassMonk, ClassBrawler}, + {ClassTemplar, ClassCleric}, + {ClassWizard, ClassSorcerer}, + {ClassRanger, ClassPredator}, + {ClassBeastlord, ClassAnimalist}, + {ClassChanneler, ClassShaper}, + {ClassWarrior, ClassCommoner}, // No secondary + {ClassFighter, ClassCommoner}, // No secondary + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetSecondaryBaseClass(tt.classID) + if result != tt.expected { + t.Errorf("GetSecondaryBaseClass(%d) = %d, want %d", tt.classID, result, tt.expected) + } + }) + } +} + +func TestGetTSBaseClass(t *testing.T) { + tests := []struct { + classID int8 + expected int8 + }{ + {3, 1}, // Guardian (3+42=45 >= ClassArtisan) returns ClassArtisan-44 = 1 + {10, 1}, // Paladin (10+42=52 >= ClassArtisan) returns ClassArtisan-44 = 1 + {0, 0}, // Commoner (0+42=42 < ClassArtisan) returns 0 + {1, 1}, // Fighter (1+42=43 < ClassArtisan) returns 1 + {2, 2}, // Warrior (2+42=44 < ClassArtisan) returns 2 + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetTSBaseClass(tt.classID) + if result != tt.expected { + t.Errorf("GetTSBaseClass(%d) = %d, want %d", tt.classID, result, tt.expected) + } + }) + } +} + +func TestIsValidClassID(t *testing.T) { + tests := []struct { + classID int8 + expected bool + }{ + {ClassCommoner, true}, + {ClassWarrior, true}, + {ClassAlchemist, true}, + {-1, false}, + {100, false}, + {58, false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := IsValidClassID(tt.classID) + if result != tt.expected { + t.Errorf("IsValidClassID(%d) = %v, want %v", tt.classID, result, tt.expected) + } + }) + } +} + +func TestIsAdventureClass(t *testing.T) { + tests := []struct { + classID int8 + expected bool + }{ + {ClassCommoner, true}, + {ClassWarrior, true}, + {ClassChanneler, true}, + {ClassArtisan, false}, + {ClassCarpenter, false}, + {-1, false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := IsAdventureClass(tt.classID) + if result != tt.expected { + t.Errorf("IsAdventureClass(%d) = %v, want %v", tt.classID, result, tt.expected) + } + }) + } +} + +func TestIsTradeskillClass(t *testing.T) { + tests := []struct { + classID int8 + expected bool + }{ + {ClassArtisan, true}, + {ClassCarpenter, true}, + {ClassAlchemist, true}, + {ClassWarrior, false}, + {ClassCommoner, false}, + {-1, false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := IsTradeskillClass(tt.classID) + if result != tt.expected { + t.Errorf("IsTradeskillClass(%d) = %v, want %v", tt.classID, result, tt.expected) + } + }) + } +} + +func TestGetClassType(t *testing.T) { + tests := []struct { + classID int8 + expected string + }{ + {ClassWarrior, ClassTypeAdventure}, + {ClassChanneler, ClassTypeAdventure}, + {ClassArtisan, ClassTypeTradeskill}, + {ClassCarpenter, ClassTypeTradeskill}, + {-1, ClassTypeSpecial}, + {100, ClassTypeSpecial}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetClassType(tt.classID) + if result != tt.expected { + t.Errorf("GetClassType(%d) = %q, want %q", tt.classID, result, tt.expected) + } + }) + } +} + +func TestGetAllClasses(t *testing.T) { + classes := GetAllClasses() + + // Check we have the right number of classes + if len(classes) != 58 { + t.Errorf("GetAllClasses() returned %d classes, want 58", len(classes)) + } + + // Check a few specific classes + if name, ok := classes[ClassWarrior]; !ok || name != "Warrior" { + t.Errorf("GetAllClasses()[ClassWarrior] = %q, %v; want 'Warrior', true", name, ok) + } + + if name, ok := classes[ClassCarpenter]; !ok || name != "Carpenter" { + t.Errorf("GetAllClasses()[ClassCarpenter] = %q, %v; want 'Carpenter', true", name, ok) + } +} + +func TestGetClassInfo(t *testing.T) { + // Test valid class + info := GetClassInfo(ClassGuardian) + if !info["valid"].(bool) { + t.Error("GetClassInfo(ClassGuardian) should be valid") + } + if info["class_id"].(int8) != ClassGuardian { + t.Errorf("GetClassInfo(ClassGuardian).class_id = %v, want %d", info["class_id"], ClassGuardian) + } + if info["name"].(string) != "GUARDIAN" { + t.Errorf("GetClassInfo(ClassGuardian).name = %v, want 'GUARDIAN'", info["name"]) + } + if info["display_name"].(string) != "Guardian" { + t.Errorf("GetClassInfo(ClassGuardian).display_name = %v, want 'Guardian'", info["display_name"]) + } + if info["base_class"].(int8) != ClassFighter { + t.Errorf("GetClassInfo(ClassGuardian).base_class = %v, want %d", info["base_class"], ClassFighter) + } + if info["secondary_base_class"].(int8) != ClassWarrior { + t.Errorf("GetClassInfo(ClassGuardian).secondary_base_class = %v, want %d", info["secondary_base_class"], ClassWarrior) + } + if !info["is_adventure"].(bool) { + t.Error("GetClassInfo(ClassGuardian).is_adventure should be true") + } + if info["is_tradeskill"].(bool) { + t.Error("GetClassInfo(ClassGuardian).is_tradeskill should be false") + } + + // Test invalid class + info = GetClassInfo(-1) + if info["valid"].(bool) { + t.Error("GetClassInfo(-1) should be invalid") + } +} + +func TestGetClassHierarchy(t *testing.T) { + tests := []struct { + classID int8 + expected []int8 + }{ + {ClassGuardian, []int8{ClassGuardian, ClassWarrior, ClassFighter, ClassCommoner}}, + {ClassWizard, []int8{ClassWizard, ClassSorcerer, ClassMage, ClassCommoner}}, + {ClassFighter, []int8{ClassFighter, ClassCommoner}}, + {ClassCommoner, []int8{ClassCommoner}}, + {-1, nil}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetClassHierarchy(tt.classID) + if len(result) != len(tt.expected) { + t.Errorf("GetClassHierarchy(%d) = %v, want %v", tt.classID, result, tt.expected) + return + } + for i, id := range result { + if id != tt.expected[i] { + t.Errorf("GetClassHierarchy(%d)[%d] = %d, want %d", tt.classID, i, id, tt.expected[i]) + } + } + }) + } +} + +func TestIsSameArchetype(t *testing.T) { + tests := []struct { + classID1 int8 + classID2 int8 + expected bool + }{ + {ClassGuardian, ClassBerserker, true}, // Both Fighter archetype + {ClassGuardian, ClassMonk, true}, // Both Fighter archetype + {ClassWizard, ClassWarlock, true}, // Both Mage archetype + {ClassGuardian, ClassWizard, false}, // Different archetypes + {ClassCommoner, ClassCommoner, true}, // Same class + {-1, ClassGuardian, false}, // Invalid class + {ClassGuardian, -1, false}, // Invalid class + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := IsSameArchetype(tt.classID1, tt.classID2) + if result != tt.expected { + t.Errorf("IsSameArchetype(%d, %d) = %v, want %v", tt.classID1, tt.classID2, result, tt.expected) + } + }) + } +} + +func TestGetArchetypeClasses(t *testing.T) { + // Test Fighter archetype + fighterClasses := GetArchetypeClasses(ClassFighter) + expectedFighterCount := 9 // Warrior through Paladin (2-10) + if len(fighterClasses) != expectedFighterCount { + t.Errorf("GetArchetypeClasses(ClassFighter) returned %d classes, want %d", len(fighterClasses), expectedFighterCount) + } + + // Verify all returned classes are fighters + for _, classID := range fighterClasses { + if GetBaseClass(classID) != ClassFighter { + t.Errorf("GetArchetypeClasses(ClassFighter) returned non-fighter class %d", classID) + } + } + + // Test Mage archetype + mageClasses := GetArchetypeClasses(ClassMage) + expectedMageCount := 9 // Sorcerer through Necromancer (22-30) + if len(mageClasses) != expectedMageCount { + t.Errorf("GetArchetypeClasses(ClassMage) returned %d classes, want %d", len(mageClasses), expectedMageCount) + } +} \ No newline at end of file diff --git a/internal/classes/classes.go b/internal/classes/classes.go deleted file mode 100644 index 50a492b..0000000 --- a/internal/classes/classes.go +++ /dev/null @@ -1,366 +0,0 @@ -package classes - -import ( - "strings" - "sync" -) - -// Classes manages class information and lookups -// Converted from C++ Classes class -type Classes struct { - // Class name to ID mapping (uppercase keys) - classMap map[string]int8 - - // ID to display name mapping for friendly names - displayNameMap map[int8]string - - // Thread safety - mutex sync.RWMutex -} - -// NewClasses creates a new classes manager with all EQ2 classes -// Converted from C++ Classes::Classes constructor -func NewClasses() *Classes { - classes := &Classes{ - classMap: make(map[string]int8), - displayNameMap: make(map[int8]string), - } - - classes.initializeClasses() - return classes -} - -// initializeClasses sets up all class mappings -func (c *Classes) initializeClasses() { - // Initialize class name to ID mappings (from C++ constructor) - c.classMap[ClassNameCommoner] = ClassCommoner - c.classMap[ClassNameFighter] = ClassFighter - c.classMap[ClassNameWarrior] = ClassWarrior - c.classMap[ClassNameGuardian] = ClassGuardian - c.classMap[ClassNameBerserker] = ClassBerserker - c.classMap[ClassNameBrawler] = ClassBrawler - c.classMap[ClassNameMonk] = ClassMonk - c.classMap[ClassNameBruiser] = ClassBruiser - c.classMap[ClassNameCrusader] = ClassCrusader - c.classMap[ClassNameShadowknight] = ClassShadowknight - c.classMap[ClassNamePaladin] = ClassPaladin - c.classMap[ClassNamePriest] = ClassPriest - c.classMap[ClassNameCleric] = ClassCleric - c.classMap[ClassNameTemplar] = ClassTemplar - c.classMap[ClassNameInquisitor] = ClassInquisitor - c.classMap[ClassNameDruid] = ClassDruid - c.classMap[ClassNameWarden] = ClassWarden - c.classMap[ClassNameFury] = ClassFury - c.classMap[ClassNameShaman] = ClassShaman - c.classMap[ClassNameMystic] = ClassMystic - c.classMap[ClassNameDefiler] = ClassDefiler - c.classMap[ClassNameMage] = ClassMage - c.classMap[ClassNameSorcerer] = ClassSorcerer - c.classMap[ClassNameWizard] = ClassWizard - c.classMap[ClassNameWarlock] = ClassWarlock - c.classMap[ClassNameEnchanter] = ClassEnchanter - c.classMap[ClassNameIllusionist] = ClassIllusionist - c.classMap[ClassNameCoercer] = ClassCoercer - c.classMap[ClassNameSummoner] = ClassSummoner - c.classMap[ClassNameConjuror] = ClassConjuror - c.classMap[ClassNameNecromancer] = ClassNecromancer - c.classMap[ClassNameScout] = ClassScout - c.classMap[ClassNameRogue] = ClassRogue - c.classMap[ClassNameSwashbuckler] = ClassSwashbuckler - c.classMap[ClassNameBrigand] = ClassBrigand - c.classMap[ClassNameBard] = ClassBard - c.classMap[ClassNameTroubador] = ClassTroubador - c.classMap[ClassNameDirge] = ClassDirge - c.classMap[ClassNamePredator] = ClassPredator - c.classMap[ClassNameRanger] = ClassRanger - c.classMap[ClassNameAssassin] = ClassAssassin - c.classMap[ClassNameAnimalist] = ClassAnimalist - c.classMap[ClassNameBeastlord] = ClassBeastlord - c.classMap[ClassNameShaper] = ClassShaper - c.classMap[ClassNameChanneler] = ClassChanneler - c.classMap[ClassNameArtisan] = ClassArtisan - c.classMap[ClassNameCraftsman] = ClassCraftsman - c.classMap[ClassNameProvisioner] = ClassProvisioner - c.classMap[ClassNameWoodworker] = ClassWoodworker - c.classMap[ClassNameCarpenter] = ClassCarpenter - c.classMap[ClassNameOutfitter] = ClassOutfitter - c.classMap[ClassNameArmorer] = ClassArmorer - c.classMap[ClassNameWeaponsmith] = ClassWeaponsmith - c.classMap[ClassNameTailor] = ClassTailor - c.classMap[ClassNameScholar] = ClassScholar - c.classMap[ClassNameJeweler] = ClassJeweler - c.classMap[ClassNameSage] = ClassSage - c.classMap[ClassNameAlchemist] = ClassAlchemist - - // Initialize display names - c.displayNameMap[ClassCommoner] = DisplayNameCommoner - c.displayNameMap[ClassFighter] = DisplayNameFighter - c.displayNameMap[ClassWarrior] = DisplayNameWarrior - c.displayNameMap[ClassGuardian] = DisplayNameGuardian - c.displayNameMap[ClassBerserker] = DisplayNameBerserker - c.displayNameMap[ClassBrawler] = DisplayNameBrawler - c.displayNameMap[ClassMonk] = DisplayNameMonk - c.displayNameMap[ClassBruiser] = DisplayNameBruiser - c.displayNameMap[ClassCrusader] = DisplayNameCrusader - c.displayNameMap[ClassShadowknight] = DisplayNameShadowknight - c.displayNameMap[ClassPaladin] = DisplayNamePaladin - c.displayNameMap[ClassPriest] = DisplayNamePriest - c.displayNameMap[ClassCleric] = DisplayNameCleric - c.displayNameMap[ClassTemplar] = DisplayNameTemplar - c.displayNameMap[ClassInquisitor] = DisplayNameInquisitor - c.displayNameMap[ClassDruid] = DisplayNameDruid - c.displayNameMap[ClassWarden] = DisplayNameWarden - c.displayNameMap[ClassFury] = DisplayNameFury - c.displayNameMap[ClassShaman] = DisplayNameShaman - c.displayNameMap[ClassMystic] = DisplayNameMystic - c.displayNameMap[ClassDefiler] = DisplayNameDefiler - c.displayNameMap[ClassMage] = DisplayNameMage - c.displayNameMap[ClassSorcerer] = DisplayNameSorcerer - c.displayNameMap[ClassWizard] = DisplayNameWizard - c.displayNameMap[ClassWarlock] = DisplayNameWarlock - c.displayNameMap[ClassEnchanter] = DisplayNameEnchanter - c.displayNameMap[ClassIllusionist] = DisplayNameIllusionist - c.displayNameMap[ClassCoercer] = DisplayNameCoercer - c.displayNameMap[ClassSummoner] = DisplayNameSummoner - c.displayNameMap[ClassConjuror] = DisplayNameConjuror - c.displayNameMap[ClassNecromancer] = DisplayNameNecromancer - c.displayNameMap[ClassScout] = DisplayNameScout - c.displayNameMap[ClassRogue] = DisplayNameRogue - c.displayNameMap[ClassSwashbuckler] = DisplayNameSwashbuckler - c.displayNameMap[ClassBrigand] = DisplayNameBrigand - c.displayNameMap[ClassBard] = DisplayNameBard - c.displayNameMap[ClassTroubador] = DisplayNameTroubador - c.displayNameMap[ClassDirge] = DisplayNameDirge - c.displayNameMap[ClassPredator] = DisplayNamePredator - c.displayNameMap[ClassRanger] = DisplayNameRanger - c.displayNameMap[ClassAssassin] = DisplayNameAssassin - c.displayNameMap[ClassAnimalist] = DisplayNameAnimalist - c.displayNameMap[ClassBeastlord] = DisplayNameBeastlord - c.displayNameMap[ClassShaper] = DisplayNameShaper - c.displayNameMap[ClassChanneler] = DisplayNameChanneler - c.displayNameMap[ClassArtisan] = DisplayNameArtisan - c.displayNameMap[ClassCraftsman] = DisplayNameCraftsman - c.displayNameMap[ClassProvisioner] = DisplayNameProvisioner - c.displayNameMap[ClassWoodworker] = DisplayNameWoodworker - c.displayNameMap[ClassCarpenter] = DisplayNameCarpenter - c.displayNameMap[ClassOutfitter] = DisplayNameOutfitter - c.displayNameMap[ClassArmorer] = DisplayNameArmorer - c.displayNameMap[ClassWeaponsmith] = DisplayNameWeaponsmith - c.displayNameMap[ClassTailor] = DisplayNameTailor - c.displayNameMap[ClassScholar] = DisplayNameScholar - c.displayNameMap[ClassJeweler] = DisplayNameJeweler - c.displayNameMap[ClassSage] = DisplayNameSage - c.displayNameMap[ClassAlchemist] = DisplayNameAlchemist -} - -// GetClassID returns the class ID for a given class name -// Converted from C++ Classes::GetClassID -func (c *Classes) GetClassID(name string) int8 { - c.mutex.RLock() - defer c.mutex.RUnlock() - - className := strings.ToUpper(strings.TrimSpace(name)) - if classID, exists := c.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 (c *Classes) GetClassName(classID int8) string { - c.mutex.RLock() - defer c.mutex.RUnlock() - - // Search through class map to find the name - for name, id := range c.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 (c *Classes) GetClassNameCase(classID int8) string { - c.mutex.RLock() - defer c.mutex.RUnlock() - - if displayName, exists := c.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 (c *Classes) 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 (c *Classes) 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 (c *Classes) GetTSBaseClass(classID int8) int8 { - if classID+42 >= ClassArtisan { - return ClassArtisan - 44 - } - - return classID -} - -// GetSecondaryTSBaseClass returns the secondary tradeskill base class ID -// Converted from C++ Classes::GetSecondaryTSBaseClass -func (c *Classes) 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 (c *Classes) IsValidClassID(classID int8) bool { - return classID >= MinClassID && classID <= MaxClassID -} - -// GetAllClasses returns all class IDs and their display names -func (c *Classes) GetAllClasses() map[int8]string { - c.mutex.RLock() - defer c.mutex.RUnlock() - - result := make(map[int8]string) - for classID, displayName := range c.displayNameMap { - result[classID] = displayName - } - - return result -} - -// IsAdventureClass checks if a class is an adventure class -func (c *Classes) IsAdventureClass(classID int8) bool { - return classID >= ClassCommoner && classID <= ClassChanneler -} - -// IsTradeskillClass checks if a class is a tradeskill class -func (c *Classes) IsTradeskillClass(classID int8) bool { - return classID >= ClassArtisan && classID <= ClassAlchemist -} - -// GetClassType returns the type of class (adventure, tradeskill, etc.) -func (c *Classes) GetClassType(classID int8) string { - if c.IsAdventureClass(classID) { - return ClassTypeAdventure - } - if c.IsTradeskillClass(classID) { - return ClassTypeTradeskill - } - - return ClassTypeSpecial -} - -// GetClassCount returns the total number of classes -func (c *Classes) GetClassCount() int { - c.mutex.RLock() - defer c.mutex.RUnlock() - - return len(c.displayNameMap) -} - -// GetClassInfo returns comprehensive information about a class -func (c *Classes) GetClassInfo(classID int8) map[string]any { - c.mutex.RLock() - defer c.mutex.RUnlock() - - info := make(map[string]any) - - if !c.IsValidClassID(classID) { - info["valid"] = false - return info - } - - info["valid"] = true - info["class_id"] = classID - info["name"] = c.GetClassName(classID) - info["display_name"] = c.GetClassNameCase(classID) - info["base_class"] = c.GetBaseClass(classID) - info["secondary_base_class"] = c.GetSecondaryBaseClass(classID) - info["type"] = c.GetClassType(classID) - info["is_adventure"] = c.IsAdventureClass(classID) - info["is_tradeskill"] = c.IsTradeskillClass(classID) - - return info -} - -// Global classes instance -var globalClasses *Classes -var initClassesOnce sync.Once - -// GetGlobalClasses returns the global classes manager (singleton) -func GetGlobalClasses() *Classes { - initClassesOnce.Do(func() { - globalClasses = NewClasses() - }) - return globalClasses -} diff --git a/internal/classes/classes_test.go b/internal/classes/classes_test.go deleted file mode 100644 index 29d1b45..0000000 --- a/internal/classes/classes_test.go +++ /dev/null @@ -1,1702 +0,0 @@ -package classes - -import ( - "strings" - "sync" - "testing" -) - -// Mock entity implementations for testing -type MockClassAware struct { - classID int8 - mutex sync.RWMutex -} - -func (m *MockClassAware) GetClass() int8 { - m.mutex.RLock() - defer m.mutex.RUnlock() - return m.classID -} - -func (m *MockClassAware) SetClass(classID int8) { - m.mutex.Lock() - defer m.mutex.Unlock() - m.classID = classID -} - -type MockEntityWithClass struct { - *MockClassAware - id int32 - name string - level int8 -} - -func (m *MockEntityWithClass) GetID() int32 { - return m.id -} - -func (m *MockEntityWithClass) GetName() string { - return m.name -} - -func (m *MockEntityWithClass) GetLevel() int8 { - return m.level -} - -func NewMockClassAware(classID int8) *MockClassAware { - return &MockClassAware{classID: classID} -} - -func NewMockEntityWithClass(id int32, name string, level int8, classID int8) *MockEntityWithClass { - return &MockEntityWithClass{ - MockClassAware: NewMockClassAware(classID), - id: id, - name: name, - level: level, - } -} - -// Core Classes tests -func TestNewClasses(t *testing.T) { - classes := NewClasses() - if classes == nil { - t.Fatal("NewClasses returned nil") - } - - if len(classes.classMap) == 0 { - t.Error("classMap should not be empty after initialization") - } - - if len(classes.displayNameMap) == 0 { - t.Error("displayNameMap should not be empty after initialization") - } -} - -func TestGetClassID(t *testing.T) { - classes := NewClasses() - - tests := []struct { - name string - input string - expectedID int8 - shouldBeValid bool - }{ - {"Valid class name", "WARRIOR", ClassWarrior, true}, - {"Valid class name lowercase", "warrior", ClassWarrior, true}, - {"Valid class name with spaces", " WARRIOR ", ClassWarrior, true}, - {"Invalid class name", "INVALID", -1, false}, - {"Empty string", "", -1, false}, - {"Valid priest class", "CLERIC", ClassCleric, true}, - {"Valid mage class", "WIZARD", ClassWizard, true}, - {"Valid scout class", "RANGER", ClassRanger, true}, - {"Valid tradeskill class", "ARMORER", ClassArmorer, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := classes.GetClassID(tt.input) - if tt.shouldBeValid { - if result != tt.expectedID { - t.Errorf("GetClassID(%s) = %d, want %d", tt.input, result, tt.expectedID) - } - } else { - if result != -1 { - t.Errorf("GetClassID(%s) should return -1 for invalid input, got %d", tt.input, result) - } - } - }) - } -} - -func TestGetClassName(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - expectedName string - }{ - {ClassWarrior, ClassNameWarrior}, - {ClassCleric, ClassNameCleric}, - {ClassWizard, ClassNameWizard}, - {ClassRanger, ClassNameRanger}, - {ClassArmorer, ClassNameArmorer}, - {-1, ""}, // Invalid class ID - {100, ""}, // Invalid class ID - } - - for _, tt := range tests { - t.Run("GetClassName", func(t *testing.T) { - result := classes.GetClassName(tt.classID) - if result != tt.expectedName { - t.Errorf("GetClassName(%d) = %s, want %s", tt.classID, result, tt.expectedName) - } - }) - } -} - -func TestGetClassNameCase(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - expectedDisplay string - }{ - {ClassWarrior, DisplayNameWarrior}, - {ClassCleric, DisplayNameCleric}, - {ClassWizard, DisplayNameWizard}, - {ClassRanger, DisplayNameRanger}, - {ClassArmorer, DisplayNameArmorer}, - {-1, ""}, // Invalid class ID - {100, ""}, // Invalid class ID - } - - for _, tt := range tests { - t.Run("GetClassNameCase", func(t *testing.T) { - result := classes.GetClassNameCase(tt.classID) - if result != tt.expectedDisplay { - t.Errorf("GetClassNameCase(%d) = %s, want %s", tt.classID, result, tt.expectedDisplay) - } - }) - } -} - -func TestGetBaseClass(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - expectedBase int8 - }{ - {ClassWarrior, ClassFighter}, - {ClassGuardian, ClassFighter}, - {ClassBerserker, ClassFighter}, - {ClassCleric, ClassPriest}, - {ClassTemplar, ClassPriest}, - {ClassWizard, ClassMage}, - {ClassWarlock, ClassMage}, - {ClassRanger, ClassScout}, - {ClassAssassin, ClassScout}, - {ClassShaper, ClassPriest}, - {ClassChanneler, ClassPriest}, - {ClassCommoner, ClassCommoner}, - {-1, ClassCommoner}, // Invalid defaults to Commoner - } - - for _, tt := range tests { - t.Run("GetBaseClass", func(t *testing.T) { - result := classes.GetBaseClass(tt.classID) - if result != tt.expectedBase { - t.Errorf("GetBaseClass(%d) = %d, want %d", tt.classID, result, tt.expectedBase) - } - }) - } -} - -func TestGetSecondaryBaseClass(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - expectedSecondaryBase 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}, - {ClassCommoner, ClassCommoner}, // Base classes return Commoner - {ClassFighter, ClassCommoner}, - } - - for _, tt := range tests { - t.Run("GetSecondaryBaseClass", func(t *testing.T) { - result := classes.GetSecondaryBaseClass(tt.classID) - if result != tt.expectedSecondaryBase { - t.Errorf("GetSecondaryBaseClass(%d) = %d, want %d", tt.classID, result, tt.expectedSecondaryBase) - } - }) - } -} - -func TestIsValidClassID(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - valid bool - }{ - {ClassCommoner, true}, - {ClassWarrior, true}, - {ClassAlchemist, true}, - {MinClassID, true}, - {MaxClassID, true}, - {-1, false}, - {MaxClassID + 1, false}, - {100, false}, - } - - for _, tt := range tests { - t.Run("IsValidClassID", func(t *testing.T) { - result := classes.IsValidClassID(tt.classID) - if result != tt.valid { - t.Errorf("IsValidClassID(%d) = %t, want %t", tt.classID, result, tt.valid) - } - }) - } -} - -func TestGetAllClasses(t *testing.T) { - classes := NewClasses() - allClasses := classes.GetAllClasses() - - if len(allClasses) == 0 { - t.Error("GetAllClasses should not return empty map") - } - - // Check that all classes have valid display names - for classID, displayName := range allClasses { - if displayName == "" { - t.Errorf("Class %d has empty display name", classID) - } - - if !classes.IsValidClassID(classID) { - t.Errorf("Invalid class ID in GetAllClasses: %d", classID) - } - } - - // Verify we have the expected number of classes - expectedCount := MaxClasses - if len(allClasses) != expectedCount { - t.Errorf("GetAllClasses returned %d classes, expected %d", len(allClasses), expectedCount) - } -} - -func TestIsAdventureClass(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - adventure bool - }{ - {ClassCommoner, true}, - {ClassFighter, true}, - {ClassWarrior, true}, - {ClassChanneler, true}, - {ClassArtisan, false}, - {ClassCraftsman, false}, - {ClassAlchemist, false}, - } - - for _, tt := range tests { - t.Run("IsAdventureClass", func(t *testing.T) { - result := classes.IsAdventureClass(tt.classID) - if result != tt.adventure { - t.Errorf("IsAdventureClass(%d) = %t, want %t", tt.classID, result, tt.adventure) - } - }) - } -} - -func TestIsTradeskillClass(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - tradeskill bool - }{ - {ClassCommoner, false}, - {ClassWarrior, false}, - {ClassChanneler, false}, - {ClassArtisan, true}, - {ClassCraftsman, true}, - {ClassAlchemist, true}, - } - - for _, tt := range tests { - t.Run("IsTradeskillClass", func(t *testing.T) { - result := classes.IsTradeskillClass(tt.classID) - if result != tt.tradeskill { - t.Errorf("IsTradeskillClass(%d) = %t, want %t", tt.classID, result, tt.tradeskill) - } - }) - } -} - -func TestGetClassType(t *testing.T) { - classes := NewClasses() - - tests := []struct { - classID int8 - expectedType string - }{ - {ClassWarrior, ClassTypeAdventure}, - {ClassCleric, ClassTypeAdventure}, - {ClassChanneler, ClassTypeAdventure}, - {ClassArtisan, ClassTypeTradeskill}, - {ClassCraftsman, ClassTypeTradeskill}, - {ClassAlchemist, ClassTypeTradeskill}, - {-1, ClassTypeSpecial}, // Invalid class - {100, ClassTypeSpecial}, // Invalid class - } - - for _, tt := range tests { - t.Run("GetClassType", func(t *testing.T) { - result := classes.GetClassType(tt.classID) - if result != tt.expectedType { - t.Errorf("GetClassType(%d) = %s, want %s", tt.classID, result, tt.expectedType) - } - }) - } -} - -func TestGetClassCount(t *testing.T) { - classes := NewClasses() - count := classes.GetClassCount() - - if count != MaxClasses { - t.Errorf("GetClassCount() = %d, want %d", count, MaxClasses) - } -} - -func TestGetClassInfo(t *testing.T) { - classes := NewClasses() - - // Test valid class - info := classes.GetClassInfo(ClassWarrior) - if !info["valid"].(bool) { - t.Error("GetClassInfo should indicate valid class") - } - - if info["class_id"] != ClassWarrior { - t.Error("GetClassInfo should return correct class_id") - } - - if info["name"] != ClassNameWarrior { - t.Error("GetClassInfo should return correct name") - } - - if info["display_name"] != DisplayNameWarrior { - t.Error("GetClassInfo should return correct display_name") - } - - // Test invalid class - invalidInfo := classes.GetClassInfo(-1) - if invalidInfo["valid"].(bool) { - t.Error("GetClassInfo should indicate invalid class for -1") - } -} - -func TestGetGlobalClasses(t *testing.T) { - classes1 := GetGlobalClasses() - classes2 := GetGlobalClasses() - - if classes1 != classes2 { - t.Error("GetGlobalClasses should return the same instance (singleton)") - } - - if classes1 == nil { - t.Error("GetGlobalClasses should not return nil") - } -} - -// ClassUtils tests -func TestNewClassUtils(t *testing.T) { - utils := NewClassUtils() - if utils == nil { - t.Fatal("NewClassUtils returned nil") - } - - if utils.classes == nil { - t.Error("ClassUtils should have classes instance") - } -} - -func TestParseClassName(t *testing.T) { - utils := NewClassUtils() - - tests := []struct { - name string - input string - expected int8 - }{ - {"Exact match", "WARRIOR", ClassWarrior}, - {"Case insensitive", "warrior", ClassWarrior}, - {"With spaces", " Warrior ", ClassWarrior}, - {"With underscores", "SHADOW_KNIGHT", ClassShadowknight}, - {"With dashes", "SHADOW-KNIGHT", ClassShadowknight}, - {"Friendly name", "Warrior", ClassWarrior}, - {"Empty string", "", -1}, - {"Invalid name", "INVALID", -1}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := utils.ParseClassName(tt.input) - if result != tt.expected { - t.Errorf("ParseClassName(%s) = %d, want %d", tt.input, result, tt.expected) - } - }) - } -} - -func TestFormatClassName(t *testing.T) { - utils := NewClassUtils() - - tests := []struct { - classID int8 - format string - expected string - }{ - {ClassWarrior, "display", DisplayNameWarrior}, - {ClassWarrior, "friendly", DisplayNameWarrior}, - {ClassWarrior, "upper", ClassNameWarrior}, - {ClassWarrior, "lowercase", strings.ToLower(ClassNameWarrior)}, - {ClassWarrior, "default", DisplayNameWarrior}, - {ClassWarrior, "", DisplayNameWarrior}, - } - - for _, tt := range tests { - t.Run("FormatClassName", func(t *testing.T) { - result := utils.FormatClassName(tt.classID, tt.format) - if result != tt.expected { - t.Errorf("FormatClassName(%d, %s) = %s, want %s", tt.classID, tt.format, result, tt.expected) - } - }) - } -} - -func TestGetRandomClassByType(t *testing.T) { - utils := NewClassUtils() - - // Test adventure classes - adventureClass := utils.GetRandomClassByType(ClassTypeAdventure) - classes := GetGlobalClasses() - if !classes.IsAdventureClass(adventureClass) { - t.Errorf("GetRandomClassByType(adventure) returned non-adventure class: %d", adventureClass) - } - - // Test tradeskill classes - tradeskillClass := utils.GetRandomClassByType(ClassTypeTradeskill) - if !classes.IsTradeskillClass(tradeskillClass) { - t.Errorf("GetRandomClassByType(tradeskill) returned non-tradeskill class: %d", tradeskillClass) - } - - // Test invalid type - invalidTypeClass := utils.GetRandomClassByType("invalid") - if invalidTypeClass != DefaultClassID { - t.Errorf("GetRandomClassByType(invalid) should return DefaultClassID, got %d", invalidTypeClass) - } -} - -func TestGetRandomAdventureClass(t *testing.T) { - utils := NewClassUtils() - classes := GetGlobalClasses() - - for i := 0; i < 10; i++ { - adventureClass := utils.GetRandomAdventureClass() - if !classes.IsAdventureClass(adventureClass) { - t.Errorf("GetRandomAdventureClass() returned non-adventure class: %d", adventureClass) - } - } -} - -func TestGetRandomTradeskillClass(t *testing.T) { - utils := NewClassUtils() - classes := GetGlobalClasses() - - for i := 0; i < 10; i++ { - tradeskillClass := utils.GetRandomTradeskillClass() - if !classes.IsTradeskillClass(tradeskillClass) { - t.Errorf("GetRandomTradeskillClass() returned non-tradeskill class: %d", tradeskillClass) - } - } -} - -func TestValidateClassForRace(t *testing.T) { - utils := NewClassUtils() - - // Currently all classes are valid for all races - if !utils.ValidateClassForRace(ClassWarrior, 1) { - t.Error("ValidateClassForRace should return true for valid class") - } - - if utils.ValidateClassForRace(-1, 1) { - t.Error("ValidateClassForRace should return false for invalid class") - } -} - -func TestGetClassDescription(t *testing.T) { - utils := NewClassUtils() - - description := utils.GetClassDescription(ClassWarrior) - if description == "" { - t.Error("GetClassDescription should return non-empty description for Warrior") - } - - if !strings.Contains(strings.ToLower(description), "weapon") { - t.Error("Warrior description should contain 'weapon'") - } - - unknownDescription := utils.GetClassDescription(-1) - if unknownDescription == "" { - t.Error("GetClassDescription should return default description for invalid class") - } -} - -func TestGetClassProgression(t *testing.T) { - utils := NewClassUtils() - - // Test final class progression - guardianProgression := utils.GetClassProgression(ClassGuardian) - expected := []int8{ClassCommoner, ClassFighter, ClassWarrior, ClassGuardian} - - if len(guardianProgression) != len(expected) { - t.Errorf("Guardian progression length = %d, want %d", len(guardianProgression), len(expected)) - } - - for i, classID := range expected { - if i < len(guardianProgression) && guardianProgression[i] != classID { - t.Errorf("Guardian progression[%d] = %d, want %d", i, guardianProgression[i], classID) - } - } - - // Test base class progression (should be shorter) - fighterProgression := utils.GetClassProgression(ClassFighter) - if len(fighterProgression) >= len(guardianProgression) { - t.Error("Fighter progression should be shorter than Guardian progression") - } - - // Test commoner progression - commonerProgression := utils.GetClassProgression(ClassCommoner) - if len(commonerProgression) != 1 || commonerProgression[0] != ClassCommoner { - t.Error("Commoner progression should only contain Commoner") - } -} - -func TestGetClassesForBaseClass(t *testing.T) { - utils := NewClassUtils() - - fighterClasses := utils.GetClassesForBaseClass(ClassFighter) - if len(fighterClasses) == 0 { - t.Error("Should find fighter classes") - } - - // Verify all returned classes have Fighter as base class - classes := GetGlobalClasses() - for _, classID := range fighterClasses { - if classes.GetBaseClass(classID) != ClassFighter { - t.Errorf("Class %d should have Fighter as base class", classID) - } - } -} - -func TestGetClassesBySecondaryBase(t *testing.T) { - utils := NewClassUtils() - - warriorClasses := utils.GetClassesBySecondaryBase(ClassWarrior) - if len(warriorClasses) == 0 { - t.Error("Should find classes with Warrior as secondary base") - } - - // Verify all returned classes have Warrior as secondary base class - classes := GetGlobalClasses() - for _, classID := range warriorClasses { - if classes.GetSecondaryBaseClass(classID) != ClassWarrior { - t.Errorf("Class %d should have Warrior as secondary base class", classID) - } - } -} - -func TestGetClassesByPattern(t *testing.T) { - utils := NewClassUtils() - - // Search for classes containing "war" - warClasses := utils.GetClassesByPattern("war") - if len(warClasses) == 0 { - t.Error("Should find classes matching 'war'") - } - - // Verify matches - classes := GetGlobalClasses() - for _, classID := range warClasses { - displayName := classes.GetClassNameCase(classID) - if !strings.Contains(strings.ToLower(displayName), "war") { - t.Errorf("Class %s should contain 'war'", displayName) - } - } - - // Test case insensitive search - warClassesUpper := utils.GetClassesByPattern("WAR") - if len(warClassesUpper) != len(warClasses) { - t.Error("Pattern search should be case insensitive") - } -} - -func TestValidateClassTransition(t *testing.T) { - utils := NewClassUtils() - - tests := []struct { - name string - fromClass int8 - toClass int8 - shouldAllow bool - }{ - {"Commoner to Fighter", ClassCommoner, ClassFighter, true}, - {"Fighter to Warrior", ClassFighter, ClassWarrior, true}, - {"Warrior to Guardian", ClassWarrior, ClassGuardian, true}, - {"Same class", ClassWarrior, ClassWarrior, false}, - {"Regression", ClassGuardian, ClassWarrior, false}, - {"Invalid from", -1, ClassWarrior, false}, - {"Invalid to", ClassWarrior, -1, false}, - {"Incompatible paths", ClassWarrior, ClassCleric, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - valid, _ := utils.ValidateClassTransition(tt.fromClass, tt.toClass) - if valid != tt.shouldAllow { - t.Errorf("ValidateClassTransition(%d, %d) = %t, want %t", tt.fromClass, tt.toClass, valid, tt.shouldAllow) - } - }) - } -} - -func TestGetClassAliases(t *testing.T) { - utils := NewClassUtils() - - // Test class with known aliases - skAliases := utils.GetClassAliases(ClassShadowknight) - if len(skAliases) == 0 { - t.Error("Shadowknight should have aliases") - } - - // Should contain the alias "SK" - foundSK := false - for _, alias := range skAliases { - if alias == "SK" { - foundSK = true - break - } - } - if !foundSK { - t.Error("Shadowknight aliases should include 'SK'") - } - - // Test class without specific aliases - commonerAliases := utils.GetClassAliases(ClassCommoner) - if len(commonerAliases) == 0 { - t.Error("All classes should have at least their official names as aliases") - } -} - -func TestGetClassStatistics(t *testing.T) { - utils := NewClassUtils() - - stats := utils.GetClassStatistics() - - if stats["total_classes"] == nil { - t.Error("Statistics should include total_classes") - } - - if stats["adventure_classes"] == nil { - t.Error("Statistics should include adventure_classes") - } - - if stats["tradeskill_classes"] == nil { - t.Error("Statistics should include tradeskill_classes") - } - - totalClasses := stats["total_classes"].(int) - adventureClasses := stats["adventure_classes"].(int) - tradeskillClasses := stats["tradeskill_classes"].(int) - - if totalClasses != adventureClasses+tradeskillClasses { - t.Errorf("Total classes (%d) should equal adventure (%d) + tradeskill (%d)", - totalClasses, adventureClasses, tradeskillClasses) - } -} - -func TestFormatClassList(t *testing.T) { - utils := NewClassUtils() - - classIDs := []int8{ClassWarrior, ClassCleric, ClassWizard} - result := utils.FormatClassList(classIDs, ", ") - - expected := "Warrior, Cleric, Wizard" - if result != expected { - t.Errorf("FormatClassList() = %s, want %s", result, expected) - } - - // Test empty list - emptyResult := utils.FormatClassList([]int8{}, ", ") - if emptyResult != "" { - t.Error("FormatClassList with empty list should return empty string") - } -} - -func TestGetEQClassName(t *testing.T) { - utils := NewClassUtils() - - // Currently just returns display name - result := utils.GetEQClassName(ClassWarrior, 50) - expected := DisplayNameWarrior - if result != expected { - t.Errorf("GetEQClassName() = %s, want %s", result, expected) - } -} - -func TestGetStartingClass(t *testing.T) { - utils := NewClassUtils() - - startingClass := utils.GetStartingClass() - if startingClass != ClassCommoner { - t.Errorf("GetStartingClass() = %d, want %d", startingClass, ClassCommoner) - } -} - -func TestIsBaseClass(t *testing.T) { - utils := NewClassUtils() - - tests := []struct { - classID int8 - isBase bool - }{ - {ClassFighter, true}, - {ClassPriest, true}, - {ClassMage, true}, - {ClassScout, true}, - {ClassWarrior, false}, - {ClassCleric, false}, - {ClassCommoner, false}, - } - - for _, tt := range tests { - t.Run("IsBaseClass", func(t *testing.T) { - result := utils.IsBaseClass(tt.classID) - if result != tt.isBase { - t.Errorf("IsBaseClass(%d) = %t, want %t", tt.classID, result, tt.isBase) - } - }) - } -} - -func TestIsSecondaryBaseClass(t *testing.T) { - utils := NewClassUtils() - - // Warrior is a secondary base class (for Guardian, Berserker) - if !utils.IsSecondaryBaseClass(ClassWarrior) { - t.Error("Warrior should be a secondary base class") - } - - // Guardian is not a secondary base class - if utils.IsSecondaryBaseClass(ClassGuardian) { - t.Error("Guardian should not be a secondary base class") - } -} - -// ClassIntegration tests -func TestNewClassIntegration(t *testing.T) { - integration := NewClassIntegration() - if integration == nil { - t.Fatal("NewClassIntegration returned nil") - } - - if integration.classes == nil { - t.Error("ClassIntegration should have classes instance") - } - - if integration.utils == nil { - t.Error("ClassIntegration should have utils instance") - } -} - -func TestValidateEntityClass(t *testing.T) { - integration := NewClassIntegration() - - // Test valid entity - validEntity := NewMockClassAware(ClassWarrior) - valid, message, info := integration.ValidateEntityClass(validEntity) - - if !valid { - t.Error("ValidateEntityClass should return true for valid class") - } - - if message != "Valid class" { - t.Errorf("ValidateEntityClass message = %s, want 'Valid class'", message) - } - - if info == nil { - t.Error("ValidateEntityClass should return class info") - } - - // Test invalid entity - invalidEntity := NewMockClassAware(-1) - valid, message, info = integration.ValidateEntityClass(invalidEntity) - - if valid { - t.Error("ValidateEntityClass should return false for invalid class") - } - - if info != nil { - t.Error("ValidateEntityClass should return nil info for invalid class") - } -} - -func TestGetEntityClassInfo(t *testing.T) { - integration := NewClassIntegration() - - entity := NewMockEntityWithClass(100, "TestEntity", 25, ClassWarrior) - info := integration.GetEntityClassInfo(entity) - - if info["entity_id"] != int32(100) { - t.Error("GetEntityClassInfo should include entity_id") - } - - if info["entity_name"] != "TestEntity" { - t.Error("GetEntityClassInfo should include entity_name") - } - - if info["entity_level"] != int8(25) { - t.Error("GetEntityClassInfo should include entity_level") - } - - if info["class"] == nil { - t.Error("GetEntityClassInfo should include class information") - } - - if info["description"] == nil { - t.Error("GetEntityClassInfo should include description") - } -} - -func TestChangeEntityClass(t *testing.T) { - integration := NewClassIntegration() - - entity := NewMockClassAware(ClassCommoner) - - // Valid class change - err := integration.ChangeEntityClass(entity, ClassFighter) - if err != nil { - t.Errorf("ChangeEntityClass should allow valid transition: %v", err) - } - - if entity.GetClass() != ClassFighter { - t.Error("Entity class should be updated after successful change") - } - - // Invalid class change - err = integration.ChangeEntityClass(entity, -1) - if err == nil { - t.Error("ChangeEntityClass should reject invalid class") - } - - // Should not change class on error - if entity.GetClass() != ClassFighter { - t.Error("Entity class should not change on failed transition") - } -} - -func TestGetRandomClassForEntity(t *testing.T) { - integration := NewClassIntegration() - classes := GetGlobalClasses() - - adventureClass := integration.GetRandomClassForEntity(ClassTypeAdventure) - if !classes.IsAdventureClass(adventureClass) { - t.Error("GetRandomClassForEntity should return adventure class when requested") - } - - tradeskillClass := integration.GetRandomClassForEntity(ClassTypeTradeskill) - if !classes.IsTradeskillClass(tradeskillClass) { - t.Error("GetRandomClassForEntity should return tradeskill class when requested") - } -} - -func TestCheckClassCompatibility(t *testing.T) { - integration := NewClassIntegration() - - entity1 := NewMockClassAware(ClassWarrior) - entity2 := NewMockClassAware(ClassCleric) - - // Currently all valid classes are compatible - if !integration.CheckClassCompatibility(entity1, entity2) { - t.Error("CheckClassCompatibility should return true for valid classes") - } - - // Same class should be compatible - entity3 := NewMockClassAware(ClassWarrior) - if !integration.CheckClassCompatibility(entity1, entity3) { - t.Error("CheckClassCompatibility should return true for same classes") - } - - // Invalid class should not be compatible - invalidEntity := NewMockClassAware(-1) - if integration.CheckClassCompatibility(entity1, invalidEntity) { - t.Error("CheckClassCompatibility should return false for invalid class") - } -} - -func TestFormatEntityClass(t *testing.T) { - integration := NewClassIntegration() - - entity := NewMockEntityWithClass(100, "TestEntity", 25, ClassWarrior) - - // Test default format - result := integration.FormatEntityClass(entity, "display") - expected := DisplayNameWarrior - if result != expected { - t.Errorf("FormatEntityClass() = %s, want %s", result, expected) - } - - // Test EQ format - eqResult := integration.FormatEntityClass(entity, "eq") - if eqResult == "" { - t.Error("FormatEntityClass with eq format should not be empty") - } -} - -func TestGetEntityBaseClass(t *testing.T) { - integration := NewClassIntegration() - - entity := NewMockClassAware(ClassWarrior) - baseClass := integration.GetEntityBaseClass(entity) - - if baseClass != ClassFighter { - t.Errorf("GetEntityBaseClass() = %d, want %d", baseClass, ClassFighter) - } -} - -func TestGetEntitySecondaryBaseClass(t *testing.T) { - integration := NewClassIntegration() - - entity := NewMockClassAware(ClassGuardian) - secondaryBase := integration.GetEntitySecondaryBaseClass(entity) - - if secondaryBase != ClassWarrior { - t.Errorf("GetEntitySecondaryBaseClass() = %d, want %d", secondaryBase, ClassWarrior) - } -} - -func TestIsEntityAdventureClass(t *testing.T) { - integration := NewClassIntegration() - - adventureEntity := NewMockClassAware(ClassWarrior) - if !integration.IsEntityAdventureClass(adventureEntity) { - t.Error("IsEntityAdventureClass should return true for adventure class") - } - - tradeskillEntity := NewMockClassAware(ClassArmorer) - if integration.IsEntityAdventureClass(tradeskillEntity) { - t.Error("IsEntityAdventureClass should return false for tradeskill class") - } -} - -func TestIsEntityTradeskillClass(t *testing.T) { - integration := NewClassIntegration() - - tradeskillEntity := NewMockClassAware(ClassArmorer) - if !integration.IsEntityTradeskillClass(tradeskillEntity) { - t.Error("IsEntityTradeskillClass should return true for tradeskill class") - } - - adventureEntity := NewMockClassAware(ClassWarrior) - if integration.IsEntityTradeskillClass(adventureEntity) { - t.Error("IsEntityTradeskillClass should return false for adventure class") - } -} - -func TestGetEntitiesByClass(t *testing.T) { - integration := NewClassIntegration() - - entities := []ClassAware{ - NewMockClassAware(ClassWarrior), - NewMockClassAware(ClassCleric), - NewMockClassAware(ClassWarrior), - NewMockClassAware(ClassWizard), - } - - warriors := integration.GetEntitiesByClass(entities, ClassWarrior) - if len(warriors) != 2 { - t.Errorf("GetEntitiesByClass should find 2 warriors, found %d", len(warriors)) - } - - clerics := integration.GetEntitiesByClass(entities, ClassCleric) - if len(clerics) != 1 { - t.Errorf("GetEntitiesByClass should find 1 cleric, found %d", len(clerics)) - } -} - -func TestGetEntitiesByBaseClass(t *testing.T) { - integration := NewClassIntegration() - - entities := []ClassAware{ - NewMockClassAware(ClassWarrior), // Fighter base - NewMockClassAware(ClassGuardian), // Fighter base - NewMockClassAware(ClassCleric), // Priest base - NewMockClassAware(ClassWizard), // Mage base - } - - fighters := integration.GetEntitiesByBaseClass(entities, ClassFighter) - if len(fighters) != 2 { - t.Errorf("GetEntitiesByBaseClass should find 2 fighters, found %d", len(fighters)) - } -} - -func TestGetEntitiesByClassType(t *testing.T) { - integration := NewClassIntegration() - - entities := []ClassAware{ - NewMockClassAware(ClassWarrior), // Adventure - NewMockClassAware(ClassCleric), // Adventure - NewMockClassAware(ClassArmorer), // Tradeskill - NewMockClassAware(ClassAlchemist), // Tradeskill - } - - adventurers := integration.GetEntitiesByClassType(entities, ClassTypeAdventure) - if len(adventurers) != 2 { - t.Errorf("GetEntitiesByClassType should find 2 adventure classes, found %d", len(adventurers)) - } - - crafters := integration.GetEntitiesByClassType(entities, ClassTypeTradeskill) - if len(crafters) != 2 { - t.Errorf("GetEntitiesByClassType should find 2 tradeskill classes, found %d", len(crafters)) - } -} - -func TestIntegrationValidateClassForRace(t *testing.T) { - integration := NewClassIntegration() - - // Valid class and race - valid, message := integration.ValidateClassForRace(ClassWarrior, 1) - if !valid { - t.Error("ValidateClassForRace should return true for valid class") - } - if message != "" { - t.Error("ValidateClassForRace should return empty message for valid combination") - } - - // Invalid class - valid, message = integration.ValidateClassForRace(-1, 1) - if valid { - t.Error("ValidateClassForRace should return false for invalid class") - } - if message == "" { - t.Error("ValidateClassForRace should return error message for invalid class") - } -} - -func TestGetClassStartingStats(t *testing.T) { - integration := NewClassIntegration() - - // Test fighter class stats - fighterStats := integration.GetClassStartingStats(ClassWarrior) - if len(fighterStats) == 0 { - t.Error("GetClassStartingStats should return stats") - } - - // Fighters should have bonus strength - if fighterStats["strength"] <= 50 { - t.Error("Fighters should have strength bonus") - } - - // Test priest class stats - priestStats := integration.GetClassStartingStats(ClassCleric) - if priestStats["wisdom"] <= 50 { - t.Error("Priests should have wisdom bonus") - } - - // Test mage class stats - mageStats := integration.GetClassStartingStats(ClassWizard) - if mageStats["intelligence"] <= 50 { - t.Error("Mages should have intelligence bonus") - } - - // Test scout class stats - scoutStats := integration.GetClassStartingStats(ClassRanger) - if scoutStats["agility"] <= 50 { - t.Error("Scouts should have agility bonus") - } -} - -func TestCreateClassSpecificEntity(t *testing.T) { - integration := NewClassIntegration() - - entityData := integration.CreateClassSpecificEntity(ClassWarrior) - if entityData == nil { - t.Fatal("CreateClassSpecificEntity should not return nil for valid class") - } - - if entityData["class_id"] != ClassWarrior { - t.Errorf("Entity data class_id = %v, want %v", entityData["class_id"], ClassWarrior) - } - - if entityData["class_name"] != DisplayNameWarrior { - t.Error("Entity data should include correct class_name") - } - - if entityData["starting_stats"] == nil { - t.Error("Entity data should include starting_stats") - } - - if entityData["progression"] == nil { - t.Error("Entity data should include progression") - } - - // Test invalid class - invalidData := integration.CreateClassSpecificEntity(-1) - if invalidData != nil { - t.Error("CreateClassSpecificEntity should return nil for invalid class") - } -} - -func TestGetClassSelectionData(t *testing.T) { - integration := NewClassIntegration() - - selectionData := integration.GetClassSelectionData() - if selectionData == nil { - t.Fatal("GetClassSelectionData should not return nil") - } - - if selectionData["adventure_classes"] == nil { - t.Error("Selection data should include adventure_classes") - } - - if selectionData["statistics"] == nil { - t.Error("Selection data should include statistics") - } - - adventureClasses := selectionData["adventure_classes"].([]map[string]any) - if len(adventureClasses) == 0 { - t.Error("Selection data should include adventure classes") - } - - // Verify no tradeskill classes in selection - for _, classData := range adventureClasses { - classType := classData["type"].(string) - if classType != ClassTypeAdventure { - t.Error("Class selection should only include adventure classes") - } - } -} - -func TestGetGlobalClassIntegration(t *testing.T) { - integration1 := GetGlobalClassIntegration() - integration2 := GetGlobalClassIntegration() - - if integration1 != integration2 { - t.Error("GetGlobalClassIntegration should return the same instance") - } - - if integration1 == nil { - t.Error("GetGlobalClassIntegration should not return nil") - } -} - -// ClassManager tests -func TestNewClassManager(t *testing.T) { - manager := NewClassManager() - if manager == nil { - t.Fatal("NewClassManager returned nil") - } - - if manager.classes == nil { - t.Error("ClassManager should have classes instance") - } - - if manager.utils == nil { - t.Error("ClassManager should have utils instance") - } - - if manager.integration == nil { - t.Error("ClassManager should have integration instance") - } - - if manager.classUsageStats == nil { - t.Error("ClassManager should initialize usage stats") - } -} - -func TestRegisterClassUsage(t *testing.T) { - manager := NewClassManager() - - // Register usage for valid class - manager.RegisterClassUsage(ClassWarrior) - - stats := manager.GetClassUsageStats() - if stats[ClassWarrior] != 1 { - t.Errorf("Usage count for Warrior should be 1, got %d", stats[ClassWarrior]) - } - - // Register more usage - manager.RegisterClassUsage(ClassWarrior) - manager.RegisterClassUsage(ClassCleric) - - stats = manager.GetClassUsageStats() - if stats[ClassWarrior] != 2 { - t.Errorf("Usage count for Warrior should be 2, got %d", stats[ClassWarrior]) - } - if stats[ClassCleric] != 1 { - t.Errorf("Usage count for Cleric should be 1, got %d", stats[ClassCleric]) - } - - // Register usage for invalid class (should be ignored) - manager.RegisterClassUsage(-1) - stats = manager.GetClassUsageStats() - if _, exists := stats[-1]; exists { - t.Error("Invalid class usage should not be registered") - } -} - -func TestGetMostPopularClass(t *testing.T) { - manager := NewClassManager() - - // No usage yet - mostPopular, maxUsage := manager.GetMostPopularClass() - if mostPopular != -1 || maxUsage != 0 { - t.Error("Most popular class should be -1 with 0 usage when no usage recorded") - } - - // Register some usage - manager.RegisterClassUsage(ClassWarrior) - manager.RegisterClassUsage(ClassWarrior) - manager.RegisterClassUsage(ClassCleric) - - mostPopular, maxUsage = manager.GetMostPopularClass() - if mostPopular != ClassWarrior || maxUsage != 2 { - t.Errorf("Most popular should be Warrior with 2 usage, got class %d with %d usage", mostPopular, maxUsage) - } -} - -func TestGetLeastPopularClass(t *testing.T) { - manager := NewClassManager() - - // No usage yet - leastPopular, minUsage := manager.GetLeastPopularClass() - if leastPopular != -1 || minUsage != -1 { - t.Error("Least popular class should be -1 with -1 usage when no usage recorded") - } - - // Register some usage - manager.RegisterClassUsage(ClassWarrior) - manager.RegisterClassUsage(ClassWarrior) - manager.RegisterClassUsage(ClassCleric) - - leastPopular, minUsage = manager.GetLeastPopularClass() - if leastPopular != ClassCleric || minUsage != 1 { - t.Errorf("Least popular should be Cleric with 1 usage, got class %d with %d usage", leastPopular, minUsage) - } -} - -func TestResetUsageStats(t *testing.T) { - manager := NewClassManager() - - // Register some usage - manager.RegisterClassUsage(ClassWarrior) - manager.RegisterClassUsage(ClassCleric) - - stats := manager.GetClassUsageStats() - if len(stats) == 0 { - t.Error("Should have usage stats before reset") - } - - // Reset stats - manager.ResetUsageStats() - - stats = manager.GetClassUsageStats() - if len(stats) != 0 { - t.Error("Usage stats should be empty after reset") - } -} - -func TestProcessClassCommand(t *testing.T) { - manager := NewClassManager() - - // Test list command - result, err := manager.ProcessClassCommand("list", []string{}) - if err != nil { - t.Errorf("List command should not error: %v", err) - } - if result == "" { - t.Error("List command should return results") - } - - // Test info command with valid class - result, err = manager.ProcessClassCommand("info", []string{"WARRIOR"}) - if err != nil { - t.Errorf("Info command should not error: %v", err) - } - if result == "" { - t.Error("Info command should return results") - } - - // Test info command without args - _, err = manager.ProcessClassCommand("info", []string{}) - if err == nil { - t.Error("Info command without args should error") - } - - // Test random command - result, err = manager.ProcessClassCommand("random", []string{}) - if err != nil { - t.Errorf("Random command should not error: %v", err) - } - if result == "" { - t.Error("Random command should return results") - } - - // Test stats command - result, err = manager.ProcessClassCommand("stats", []string{}) - if err != nil { - t.Errorf("Stats command should not error: %v", err) - } - if result == "" { - t.Error("Stats command should return results") - } - - // Test search command with pattern - result, err = manager.ProcessClassCommand("search", []string{"war"}) - if err != nil { - t.Errorf("Search command should not error: %v", err) - } - if result == "" { - t.Error("Search command should return results") - } - - // Test search command without args - _, err = manager.ProcessClassCommand("search", []string{}) - if err == nil { - t.Error("Search command without args should error") - } - - // Test progression command - result, err = manager.ProcessClassCommand("progression", []string{"GUARDIAN"}) - if err != nil { - t.Errorf("Progression command should not error: %v", err) - } - if result == "" { - t.Error("Progression command should return results") - } - - // Test unknown command - _, err = manager.ProcessClassCommand("unknown", []string{}) - if err == nil { - t.Error("Unknown command should error") - } -} - -func TestValidateEntityClasses(t *testing.T) { - manager := NewClassManager() - - entities := []ClassAware{ - NewMockClassAware(ClassWarrior), - NewMockClassAware(ClassCleric), - NewMockClassAware(-1), // Invalid - NewMockClassAware(ClassWizard), - NewMockClassAware(100), // Invalid - } - - results := manager.ValidateEntityClasses(entities) - - if results["total_entities"] != len(entities) { - t.Error("Validation results should include total entity count") - } - - if results["valid_count"] != 3 { - t.Errorf("Should find 3 valid entities, found %v", results["valid_count"]) - } - - if results["invalid_count"] != 2 { - t.Errorf("Should find 2 invalid entities, found %v", results["invalid_count"]) - } - - if results["class_distribution"] == nil { - t.Error("Validation results should include class distribution") - } - - if results["invalid_entities"] == nil { - t.Error("Validation results should include invalid entities list") - } -} - -func TestGetClassRecommendations(t *testing.T) { - manager := NewClassManager() - - // Test recommendations by class type - preferences := map[string]any{ - "class_type": ClassTypeAdventure, - } - - recommendations := manager.GetClassRecommendations(preferences) - if len(recommendations) == 0 { - t.Error("Should get recommendations for adventure classes") - } - - classes := GetGlobalClasses() - for _, classID := range recommendations { - if !classes.IsAdventureClass(classID) { - t.Error("Adventure class recommendations should only include adventure classes") - } - } - - // Test recommendations by base class - basePreferences := map[string]any{ - "base_class": ClassFighter, - } - - baseRecommendations := manager.GetClassRecommendations(basePreferences) - if len(baseRecommendations) == 0 { - t.Error("Should get recommendations for fighter base class") - } - - // Test recommendations by preferred stats - statPreferences := map[string]any{ - "preferred_stats": []string{"strength", "stamina"}, - } - - statRecommendations := manager.GetClassRecommendations(statPreferences) - if len(statRecommendations) == 0 { - t.Error("Should get recommendations for preferred stats") - } - - // Test empty preferences (should get defaults) - emptyPreferences := map[string]any{} - defaultRecommendations := manager.GetClassRecommendations(emptyPreferences) - if len(defaultRecommendations) == 0 { - t.Error("Should get default recommendations when no preferences given") - } -} - -func TestGetGlobalClassManager(t *testing.T) { - manager1 := GetGlobalClassManager() - manager2 := GetGlobalClassManager() - - if manager1 != manager2 { - t.Error("GetGlobalClassManager should return the same instance (singleton)") - } - - if manager1 == nil { - t.Error("GetGlobalClassManager should not return nil") - } -} - -// Constants tests -func TestClassConstants(t *testing.T) { - // Test class ID constants - if ClassCommoner != 0 { - t.Errorf("ClassCommoner = %d, want 0", ClassCommoner) - } - - if ClassFighter != 1 { - t.Errorf("ClassFighter = %d, want 1", ClassFighter) - } - - if ClassAlchemist != 57 { - t.Errorf("ClassAlchemist = %d, want 57", ClassAlchemist) - } - - // Test validation constants - if MinClassID != 0 { - t.Errorf("MinClassID = %d, want 0", MinClassID) - } - - if MaxClassID != 57 { - t.Errorf("MaxClassID = %d, want 57", MaxClassID) - } - - if MaxClasses != 58 { - t.Errorf("MaxClasses = %d, want 58", MaxClasses) - } - - // Test class type constants - if ClassTypeAdventure != "adventure" { - t.Errorf("ClassTypeAdventure = %s, want adventure", ClassTypeAdventure) - } - - if ClassTypeTradeskill != "tradeskill" { - t.Errorf("ClassTypeTradeskill = %s, want tradeskill", ClassTypeTradeskill) - } -} - -// Concurrency tests -func TestClassesConcurrency(t *testing.T) { - classes := NewClasses() - var wg sync.WaitGroup - - // Concurrent reads - for i := range 100 { - wg.Add(1) - go func(classID int8) { - defer wg.Done() - classes.GetClassName(classID) - classes.GetClassNameCase(classID) - classes.GetBaseClass(classID) - classes.IsValidClassID(classID) - classes.GetAllClasses() - }(int8(i % MaxClasses)) - } - - wg.Wait() - - // Verify state consistency after concurrent access - allClasses := classes.GetAllClasses() - if len(allClasses) != MaxClasses { - t.Error("Concurrent access should not affect class data integrity") - } -} - -func TestClassManagerConcurrency(t *testing.T) { - manager := NewClassManager() - var wg sync.WaitGroup - - // Concurrent usage registration - for i := range 100 { - wg.Add(1) - go func(classID int8) { - defer wg.Done() - manager.RegisterClassUsage(classID) - }(int8(i % 10)) // Use first 10 classes - } - - // Concurrent stats reading - for range 50 { - wg.Add(1) - go func() { - defer wg.Done() - manager.GetClassUsageStats() - manager.GetMostPopularClass() - manager.GetLeastPopularClass() - }() - } - - wg.Wait() - - // Verify final state - stats := manager.GetClassUsageStats() - totalUsage := int32(0) - for _, count := range stats { - totalUsage += count - } - - if totalUsage != 100 { - t.Errorf("Total usage should be 100, got %d", totalUsage) - } -} - -func TestMockEntitiesConcurrency(t *testing.T) { - entity := NewMockClassAware(ClassWarrior) - var wg sync.WaitGroup - - // Concurrent reads and writes - for i := range 100 { - wg.Add(1) - go func(classID int8) { - defer wg.Done() - entity.SetClass(classID) - entity.GetClass() - }(int8(i % MaxClasses)) - } - - wg.Wait() - - // Entity should have a valid class after concurrent access - finalClass := entity.GetClass() - classes := GetGlobalClasses() - if !classes.IsValidClassID(finalClass) { - t.Errorf("Final class %d should be valid after concurrent access", finalClass) - } -} - -// Benchmarks -func BenchmarkGetClassID(b *testing.B) { - classes := NewClasses() - - for b.Loop() { - classes.GetClassID("WARRIOR") - } -} - -func BenchmarkGetClassName(b *testing.B) { - classes := NewClasses() - - for b.Loop() { - classes.GetClassName(ClassWarrior) - } -} - -func BenchmarkGetBaseClass(b *testing.B) { - classes := NewClasses() - - for b.Loop() { - classes.GetBaseClass(ClassGuardian) - } -} - -func BenchmarkGetAllClasses(b *testing.B) { - classes := NewClasses() - - for b.Loop() { - classes.GetAllClasses() - } -} - -func BenchmarkParseClassName(b *testing.B) { - utils := NewClassUtils() - - for b.Loop() { - utils.ParseClassName("WARRIOR") - } -} - -func BenchmarkGetRandomClassByType(b *testing.B) { - utils := NewClassUtils() - - for b.Loop() { - utils.GetRandomClassByType(ClassTypeAdventure) - } -} - -func BenchmarkRegisterClassUsage(b *testing.B) { - manager := NewClassManager() - - for b.Loop() { - manager.RegisterClassUsage(ClassWarrior) - } -} - -func BenchmarkValidateEntityClass(b *testing.B) { - integration := NewClassIntegration() - entity := NewMockClassAware(ClassWarrior) - - for b.Loop() { - integration.ValidateEntityClass(entity) - } -} - -func BenchmarkMockClassAwareGetSet(b *testing.B) { - entity := NewMockClassAware(ClassWarrior) - - for b.Loop() { - entity.SetClass(ClassCleric) - entity.GetClass() - } -} diff --git a/internal/classes/constants.go b/internal/classes/constants.go index fa9bd04..159c7d2 100644 --- a/internal/classes/constants.go +++ b/internal/classes/constants.go @@ -101,126 +101,3 @@ const ( ClassTypeSpecial = "special" ) -// Class name constants for lookup (uppercase keys from C++) -const ( - ClassNameCommoner = "COMMONER" - ClassNameFighter = "FIGHTER" - ClassNameWarrior = "WARRIOR" - ClassNameGuardian = "GUARDIAN" - ClassNameBerserker = "BERSERKER" - ClassNameBrawler = "BRAWLER" - ClassNameMonk = "MONK" - ClassNameBruiser = "BRUISER" - ClassNameCrusader = "CRUSADER" - ClassNameShadowknight = "SHADOWKNIGHT" - ClassNamePaladin = "PALADIN" - ClassNamePriest = "PRIEST" - ClassNameCleric = "CLERIC" - ClassNameTemplar = "TEMPLAR" - ClassNameInquisitor = "INQUISITOR" - ClassNameDruid = "DRUID" - ClassNameWarden = "WARDEN" - ClassNameFury = "FURY" - ClassNameShaman = "SHAMAN" - ClassNameMystic = "MYSTIC" - ClassNameDefiler = "DEFILER" - ClassNameMage = "MAGE" - ClassNameSorcerer = "SORCERER" - ClassNameWizard = "WIZARD" - ClassNameWarlock = "WARLOCK" - ClassNameEnchanter = "ENCHANTER" - ClassNameIllusionist = "ILLUSIONIST" - ClassNameCoercer = "COERCER" - ClassNameSummoner = "SUMMONER" - ClassNameConjuror = "CONJUROR" - ClassNameNecromancer = "NECROMANCER" - ClassNameScout = "SCOUT" - ClassNameRogue = "ROGUE" - ClassNameSwashbuckler = "SWASHBUCKLER" - ClassNameBrigand = "BRIGAND" - ClassNameBard = "BARD" - ClassNameTroubador = "TROUBADOR" - ClassNameDirge = "DIRGE" - ClassNamePredator = "PREDATOR" - ClassNameRanger = "RANGER" - ClassNameAssassin = "ASSASSIN" - ClassNameAnimalist = "ANIMALIST" - ClassNameBeastlord = "BEASTLORD" - ClassNameShaper = "SHAPER" - ClassNameChanneler = "CHANNELER" - ClassNameArtisan = "ARTISAN" - ClassNameCraftsman = "CRAFTSMAN" - ClassNameProvisioner = "PROVISIONER" - ClassNameWoodworker = "WOODWORKER" - ClassNameCarpenter = "CARPENTER" - ClassNameOutfitter = "OUTFITTER" - ClassNameArmorer = "ARMORER" - ClassNameWeaponsmith = "WEAPONSMITH" - ClassNameTailor = "TAILOR" - ClassNameScholar = "SCHOLAR" - ClassNameJeweler = "JEWELER" - ClassNameSage = "SAGE" - ClassNameAlchemist = "ALCHEMIST" -) - -// Class display names (proper case) -const ( - DisplayNameCommoner = "Commoner" - DisplayNameFighter = "Fighter" - DisplayNameWarrior = "Warrior" - DisplayNameGuardian = "Guardian" - DisplayNameBerserker = "Berserker" - DisplayNameBrawler = "Brawler" - DisplayNameMonk = "Monk" - DisplayNameBruiser = "Bruiser" - DisplayNameCrusader = "Crusader" - DisplayNameShadowknight = "Shadowknight" - DisplayNamePaladin = "Paladin" - DisplayNamePriest = "Priest" - DisplayNameCleric = "Cleric" - DisplayNameTemplar = "Templar" - DisplayNameInquisitor = "Inquisitor" - DisplayNameDruid = "Druid" - DisplayNameWarden = "Warden" - DisplayNameFury = "Fury" - DisplayNameShaman = "Shaman" - DisplayNameMystic = "Mystic" - DisplayNameDefiler = "Defiler" - DisplayNameMage = "Mage" - DisplayNameSorcerer = "Sorcerer" - DisplayNameWizard = "Wizard" - DisplayNameWarlock = "Warlock" - DisplayNameEnchanter = "Enchanter" - DisplayNameIllusionist = "Illusionist" - DisplayNameCoercer = "Coercer" - DisplayNameSummoner = "Summoner" - DisplayNameConjuror = "Conjuror" - DisplayNameNecromancer = "Necromancer" - DisplayNameScout = "Scout" - DisplayNameRogue = "Rogue" - DisplayNameSwashbuckler = "Swashbuckler" - DisplayNameBrigand = "Brigand" - DisplayNameBard = "Bard" - DisplayNameTroubador = "Troubador" - DisplayNameDirge = "Dirge" - DisplayNamePredator = "Predator" - DisplayNameRanger = "Ranger" - DisplayNameAssassin = "Assassin" - DisplayNameAnimalist = "Animalist" - DisplayNameBeastlord = "Beastlord" - DisplayNameShaper = "Shaper" - DisplayNameChanneler = "Channeler" - DisplayNameArtisan = "Artisan" - DisplayNameCraftsman = "Craftsman" - DisplayNameProvisioner = "Provisioner" - DisplayNameWoodworker = "Woodworker" - DisplayNameCarpenter = "Carpenter" - DisplayNameOutfitter = "Outfitter" - DisplayNameArmorer = "Armorer" - DisplayNameWeaponsmith = "Weaponsmith" - DisplayNameTailor = "Tailor" - DisplayNameScholar = "Scholar" - DisplayNameJeweler = "Jeweler" - DisplayNameSage = "Sage" - DisplayNameAlchemist = "Alchemist" -) diff --git a/internal/classes/doc.go b/internal/classes/doc.go new file mode 100644 index 0000000..d34e695 --- /dev/null +++ b/internal/classes/doc.go @@ -0,0 +1,47 @@ +// Package classes provides EverQuest II class definitions and lookup functions. +// +// This package manages all adventure and tradeskill class information including +// class IDs, names, hierarchies, and relationships. It provides static lookups +// for class data without requiring database access. +// +// Basic Usage: +// +// // Get class ID from name +// classID := classes.GetClassID("WARRIOR") +// +// // Get class display name +// name := classes.GetClassNameCase(classes.ClassWarrior) +// +// // Check class hierarchy +// baseClass := classes.GetBaseClass(classes.ClassGuardian) // Returns ClassFighter +// secondary := classes.GetSecondaryBaseClass(classes.ClassGuardian) // Returns ClassWarrior +// +// Class Hierarchy: +// +// Fighter -> Warrior -> Guardian/Berserker +// -> Brawler -> Monk/Bruiser +// -> Crusader -> Shadowknight/Paladin +// +// Priest -> Cleric -> Templar/Inquisitor +// -> Druid -> Warden/Fury +// -> Shaman -> Mystic/Defiler +// +// Mage -> Sorcerer -> Wizard/Warlock +// -> Enchanter -> Illusionist/Coercer +// -> Summoner -> Conjuror/Necromancer +// +// Scout -> Rogue -> Swashbuckler/Brigand +// -> Bard -> Troubador/Dirge +// -> Predator -> Ranger/Assassin +// -> Animalist -> Beastlord +// +// Tradeskill Classes: +// +// Artisan -> Craftsman -> Provisioner +// -> Woodworker -> Carpenter +// -> Outfitter -> Armorer/Weaponsmith/Tailor +// -> Scholar -> Jeweler/Sage/Alchemist +// +// The package includes all 58 class definitions from EverQuest II including +// adventure classes (0-44) and tradeskill classes (45-57). +package classes \ No newline at end of file diff --git a/internal/classes/integration.go b/internal/classes/integration.go deleted file mode 100644 index 11a0ffa..0000000 --- a/internal/classes/integration.go +++ /dev/null @@ -1,352 +0,0 @@ -package classes - -import ( - "fmt" -) - -// ClassAware interface for entities that have class information -type ClassAware interface { - GetClass() int8 - SetClass(int8) -} - -// EntityWithClass interface extends ClassAware with additional entity properties -type EntityWithClass interface { - ClassAware - GetID() int32 - GetName() string - GetLevel() int8 -} - -// ClassIntegration provides class-related functionality for other systems -type ClassIntegration struct { - classes *Classes - utils *ClassUtils -} - -// NewClassIntegration creates a new class integration helper -func NewClassIntegration() *ClassIntegration { - return &ClassIntegration{ - classes: GetGlobalClasses(), - utils: NewClassUtils(), - } -} - -// ValidateEntityClass validates an entity's class and provides detailed information -func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string, map[string]any) { - classID := entity.GetClass() - - if !ci.classes.IsValidClassID(classID) { - return false, fmt.Sprintf("Invalid class ID: %d", classID), nil - } - - classInfo := ci.classes.GetClassInfo(classID) - return true, "Valid class", classInfo -} - -// GetEntityClassInfo returns comprehensive class information for an entity -func (ci *ClassIntegration) GetEntityClassInfo(entity EntityWithClass) map[string]any { - info := make(map[string]any) - - // Basic entity info - info["entity_id"] = entity.GetID() - info["entity_name"] = entity.GetName() - info["entity_level"] = entity.GetLevel() - - // Class information - classID := entity.GetClass() - classInfo := ci.classes.GetClassInfo(classID) - info["class"] = classInfo - - // Additional class-specific info - info["description"] = ci.utils.GetClassDescription(classID) - info["eq_class_name"] = ci.utils.GetEQClassName(classID, entity.GetLevel()) - info["progression"] = ci.utils.GetClassProgression(classID) - info["aliases"] = ci.utils.GetClassAliases(classID) - info["is_base_class"] = ci.utils.IsBaseClass(classID) - info["is_secondary_base"] = ci.utils.IsSecondaryBaseClass(classID) - - return info -} - -// ChangeEntityClass changes an entity's class with validation -func (ci *ClassIntegration) ChangeEntityClass(entity ClassAware, newClassID int8) error { - if !ci.classes.IsValidClassID(newClassID) { - return fmt.Errorf("invalid class ID: %d", newClassID) - } - - oldClassID := entity.GetClass() - - // Validate the class transition - if valid, reason := ci.utils.ValidateClassTransition(oldClassID, newClassID); !valid { - return fmt.Errorf("class change not allowed: %s", reason) - } - - // Perform the class change - entity.SetClass(newClassID) - - return nil -} - -// GetRandomClassForEntity returns a random class appropriate for an entity -func (ci *ClassIntegration) GetRandomClassForEntity(classType string) int8 { - return ci.utils.GetRandomClassByType(classType) -} - -// CheckClassCompatibility checks if two entities' classes are compatible for grouping -func (ci *ClassIntegration) CheckClassCompatibility(entity1, entity2 ClassAware) bool { - class1 := entity1.GetClass() - class2 := entity2.GetClass() - - if !ci.classes.IsValidClassID(class1) || !ci.classes.IsValidClassID(class2) { - return false - } - - // Same class is always compatible - if class1 == class2 { - return true - } - - // Check if they share the same base class (good for grouping) - // base1 := ci.classes.GetBaseClass(class1) - // base2 := ci.classes.GetBaseClass(class2) - - // Different base classes can group together (provides diversity) - // Same base class provides synergy - return true // For now, all classes are compatible for grouping -} - -// FormatEntityClass returns a formatted class name for an entity -func (ci *ClassIntegration) FormatEntityClass(entity EntityWithClass, format string) string { - classID := entity.GetClass() - level := entity.GetLevel() - - switch format { - case "eq": - return ci.utils.GetEQClassName(classID, level) - default: - return ci.utils.FormatClassName(classID, format) - } -} - -// GetEntityBaseClass returns an entity's base class -func (ci *ClassIntegration) GetEntityBaseClass(entity ClassAware) int8 { - classID := entity.GetClass() - return ci.classes.GetBaseClass(classID) -} - -// GetEntitySecondaryBaseClass returns an entity's secondary base class -func (ci *ClassIntegration) GetEntitySecondaryBaseClass(entity ClassAware) int8 { - classID := entity.GetClass() - return ci.classes.GetSecondaryBaseClass(classID) -} - -// IsEntityAdventureClass checks if an entity has an adventure class -func (ci *ClassIntegration) IsEntityAdventureClass(entity ClassAware) bool { - classID := entity.GetClass() - return ci.classes.IsAdventureClass(classID) -} - -// IsEntityTradeskillClass checks if an entity has a tradeskill class -func (ci *ClassIntegration) IsEntityTradeskillClass(entity ClassAware) bool { - classID := entity.GetClass() - return ci.classes.IsTradeskillClass(classID) -} - -// GetEntitiesByClass filters entities by class -func (ci *ClassIntegration) GetEntitiesByClass(entities []ClassAware, classID int8) []ClassAware { - result := make([]ClassAware, 0) - - for _, entity := range entities { - if entity.GetClass() == classID { - result = append(result, entity) - } - } - - return result -} - -// GetEntitiesByBaseClass filters entities by base class -func (ci *ClassIntegration) GetEntitiesByBaseClass(entities []ClassAware, baseClassID int8) []ClassAware { - result := make([]ClassAware, 0) - - for _, entity := range entities { - if ci.GetEntityBaseClass(entity) == baseClassID { - result = append(result, entity) - } - } - - return result -} - -// GetEntitiesByClassType filters entities by class type (adventure/tradeskill) -func (ci *ClassIntegration) GetEntitiesByClassType(entities []ClassAware, classType string) []ClassAware { - result := make([]ClassAware, 0) - - for _, entity := range entities { - classID := entity.GetClass() - if ci.classes.GetClassType(classID) == classType { - result = append(result, entity) - } - } - - return result -} - -// ValidateClassForRace checks if a class/race combination is valid -func (ci *ClassIntegration) ValidateClassForRace(classID, raceID int8) (bool, string) { - if !ci.classes.IsValidClassID(classID) { - return false, "Invalid class" - } - - // Use the utility function (which currently allows all combinations) - if ci.utils.ValidateClassForRace(classID, raceID) { - return true, "" - } - - className := ci.classes.GetClassNameCase(classID) - return false, fmt.Sprintf("Class %s cannot be race %d", className, raceID) -} - -// GetClassStartingStats returns the starting stats for a class -func (ci *ClassIntegration) GetClassStartingStats(classID int8) map[string]int16 { - // Base stats that all classes start with - baseStats := map[string]int16{ - "strength": 50, - "stamina": 50, - "agility": 50, - "wisdom": 50, - "intelligence": 50, - } - - // Apply class modifiers based on class type and role - switch ci.classes.GetBaseClass(classID) { - case ClassFighter: - baseStats["strength"] += 5 - baseStats["stamina"] += 5 - baseStats["intelligence"] -= 3 - case ClassPriest: - baseStats["wisdom"] += 5 - baseStats["intelligence"] += 3 - baseStats["strength"] -= 2 - case ClassMage: - baseStats["intelligence"] += 5 - baseStats["wisdom"] += 3 - baseStats["strength"] -= 3 - baseStats["stamina"] -= 2 - case ClassScout: - baseStats["agility"] += 5 - baseStats["stamina"] += 3 - baseStats["wisdom"] -= 2 - } - - // Fine-tune for specific secondary base classes - switch ci.classes.GetSecondaryBaseClass(classID) { - case ClassWarrior: - baseStats["strength"] += 2 - baseStats["stamina"] += 2 - case ClassBrawler: - baseStats["agility"] += 2 - baseStats["strength"] += 1 - case ClassCrusader: - baseStats["wisdom"] += 2 - baseStats["strength"] += 1 - case ClassCleric: - baseStats["wisdom"] += 3 - case ClassDruid: - baseStats["wisdom"] += 2 - baseStats["intelligence"] += 1 - case ClassShaman: - baseStats["wisdom"] += 2 - baseStats["stamina"] += 1 - case ClassSorcerer: - baseStats["intelligence"] += 3 - case ClassEnchanter: - baseStats["intelligence"] += 2 - baseStats["agility"] += 1 - case ClassSummoner: - baseStats["intelligence"] += 2 - baseStats["wisdom"] += 1 - case ClassRogue: - baseStats["agility"] += 3 - case ClassBard: - baseStats["agility"] += 2 - baseStats["intelligence"] += 1 - case ClassPredator: - baseStats["agility"] += 2 - baseStats["stamina"] += 1 - } - - return baseStats -} - -// CreateClassSpecificEntity creates entity data with class-specific properties -func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]any { - if !ci.classes.IsValidClassID(classID) { - return nil - } - - entityData := make(map[string]any) - - // Basic class info - entityData["class_id"] = classID - entityData["class_name"] = ci.classes.GetClassNameCase(classID) - entityData["class_type"] = ci.classes.GetClassType(classID) - - // Starting stats - entityData["starting_stats"] = ci.GetClassStartingStats(classID) - - // Class progression - entityData["progression"] = ci.utils.GetClassProgression(classID) - - // Class description - entityData["description"] = ci.utils.GetClassDescription(classID) - - // Role information - entityData["base_class"] = ci.classes.GetBaseClass(classID) - entityData["secondary_base_class"] = ci.classes.GetSecondaryBaseClass(classID) - - return entityData -} - -// GetClassSelectionData returns data for class selection UI -func (ci *ClassIntegration) GetClassSelectionData() map[string]any { - data := make(map[string]any) - - // All available adventure classes (exclude tradeskill for character creation) - allClasses := ci.classes.GetAllClasses() - adventureClasses := make([]map[string]any, 0) - - for classID, displayName := range allClasses { - if ci.classes.IsAdventureClass(classID) { - classData := map[string]any{ - "id": classID, - "name": displayName, - "type": ci.classes.GetClassType(classID), - "description": ci.utils.GetClassDescription(classID), - "base_class": ci.classes.GetBaseClass(classID), - "secondary_base_class": ci.classes.GetSecondaryBaseClass(classID), - "starting_stats": ci.GetClassStartingStats(classID), - "progression": ci.utils.GetClassProgression(classID), - "is_base_class": ci.utils.IsBaseClass(classID), - } - adventureClasses = append(adventureClasses, classData) - } - } - - data["adventure_classes"] = adventureClasses - data["statistics"] = ci.utils.GetClassStatistics() - - return data -} - -// Global class integration instance -var globalClassIntegration *ClassIntegration - -// GetGlobalClassIntegration returns the global class integration helper -func GetGlobalClassIntegration() *ClassIntegration { - if globalClassIntegration == nil { - globalClassIntegration = NewClassIntegration() - } - return globalClassIntegration -} diff --git a/internal/classes/manager.go b/internal/classes/manager.go deleted file mode 100644 index dc4e177..0000000 --- a/internal/classes/manager.go +++ /dev/null @@ -1,455 +0,0 @@ -package classes - -import ( - "fmt" - "sync" -) - -// ClassManager provides high-level class management functionality -type ClassManager struct { - classes *Classes - utils *ClassUtils - integration *ClassIntegration - - // Statistics tracking - classUsageStats map[int8]int32 // Track how often each class is used - - // Thread safety - mutex sync.RWMutex -} - -// NewClassManager creates a new class manager -func NewClassManager() *ClassManager { - return &ClassManager{ - classes: GetGlobalClasses(), - utils: NewClassUtils(), - integration: NewClassIntegration(), - classUsageStats: make(map[int8]int32), - } -} - -// RegisterClassUsage tracks class usage for statistics -func (cm *ClassManager) RegisterClassUsage(classID int8) { - if !cm.classes.IsValidClassID(classID) { - return - } - - cm.mutex.Lock() - defer cm.mutex.Unlock() - - cm.classUsageStats[classID]++ -} - -// GetClassUsageStats returns class usage statistics -func (cm *ClassManager) GetClassUsageStats() map[int8]int32 { - cm.mutex.RLock() - defer cm.mutex.RUnlock() - - // Return a copy to prevent external modification - stats := make(map[int8]int32) - for classID, count := range cm.classUsageStats { - stats[classID] = count - } - - return stats -} - -// GetMostPopularClass returns the most frequently used class -func (cm *ClassManager) GetMostPopularClass() (int8, int32) { - cm.mutex.RLock() - defer cm.mutex.RUnlock() - - var mostPopularClass int8 = -1 - var maxUsage int32 = 0 - - for classID, usage := range cm.classUsageStats { - if usage > maxUsage { - maxUsage = usage - mostPopularClass = classID - } - } - - return mostPopularClass, maxUsage -} - -// GetLeastPopularClass returns the least frequently used class -func (cm *ClassManager) GetLeastPopularClass() (int8, int32) { - cm.mutex.RLock() - defer cm.mutex.RUnlock() - - var leastPopularClass int8 = -1 - var minUsage int32 = -1 - - for classID, usage := range cm.classUsageStats { - if minUsage == -1 || usage < minUsage { - minUsage = usage - leastPopularClass = classID - } - } - - return leastPopularClass, minUsage -} - -// ResetUsageStats clears all usage statistics -func (cm *ClassManager) ResetUsageStats() { - cm.mutex.Lock() - defer cm.mutex.Unlock() - - cm.classUsageStats = make(map[int8]int32) -} - -// ProcessClassCommand handles class-related commands -func (cm *ClassManager) ProcessClassCommand(command string, args []string) (string, error) { - switch command { - case "list": - return cm.handleListCommand(args) - case "info": - return cm.handleInfoCommand(args) - case "random": - return cm.handleRandomCommand(args) - case "stats": - return cm.handleStatsCommand(args) - case "search": - return cm.handleSearchCommand(args) - case "progression": - return cm.handleProgressionCommand(args) - default: - return "", fmt.Errorf("unknown class command: %s", command) - } -} - -// handleListCommand lists classes by criteria -func (cm *ClassManager) handleListCommand(args []string) (string, error) { - if len(args) == 0 { - // List all classes - allClasses := cm.classes.GetAllClasses() - result := "All Classes:\n" - for classID, displayName := range allClasses { - classType := cm.classes.GetClassType(classID) - baseClass := cm.classes.GetBaseClass(classID) - baseClassName := cm.classes.GetClassNameCase(baseClass) - result += fmt.Sprintf("%d: %s (%s, Base: %s)\n", classID, displayName, classType, baseClassName) - } - return result, nil - } - - // List classes by type - classType := args[0] - allClasses := cm.classes.GetAllClasses() - result := fmt.Sprintf("%s Classes:\n", classType) - count := 0 - - for classID, displayName := range allClasses { - if cm.classes.GetClassType(classID) == classType { - baseClass := cm.classes.GetBaseClass(classID) - baseClassName := cm.classes.GetClassNameCase(baseClass) - result += fmt.Sprintf("%d: %s (Base: %s)\n", classID, displayName, baseClassName) - count++ - } - } - - if count == 0 { - return fmt.Sprintf("No classes found for type: %s", classType), nil - } - - return result, nil -} - -// handleInfoCommand provides detailed information about a class -func (cm *ClassManager) handleInfoCommand(args []string) (string, error) { - if len(args) == 0 { - return "", fmt.Errorf("class name or ID required") - } - - // Try to parse as class name or ID - classID := cm.utils.ParseClassName(args[0]) - if classID == -1 { - return fmt.Sprintf("Invalid class: %s", args[0]), nil - } - - classInfo := cm.classes.GetClassInfo(classID) - if !classInfo["valid"].(bool) { - return fmt.Sprintf("Invalid class ID: %d", classID), nil - } - - result := "Class Information:\n" - result += fmt.Sprintf("ID: %d\n", classID) - result += fmt.Sprintf("Name: %s\n", classInfo["display_name"]) - result += fmt.Sprintf("Type: %s\n", classInfo["type"]) - result += fmt.Sprintf("Base Class: %s\n", cm.classes.GetClassNameCase(classInfo["base_class"].(int8))) - - if secondaryBase := classInfo["secondary_base_class"].(int8); secondaryBase != DefaultClassID { - result += fmt.Sprintf("Secondary Base: %s\n", cm.classes.GetClassNameCase(secondaryBase)) - } - - result += fmt.Sprintf("Description: %s\n", cm.utils.GetClassDescription(classID)) - - // Add progression path - progression := cm.utils.GetClassProgression(classID) - if len(progression) > 1 { - result += "Progression Path: " - progressionNames := make([]string, len(progression)) - for i, progClassID := range progression { - progressionNames[i] = cm.classes.GetClassNameCase(progClassID) - } - result += fmt.Sprintf("%s\n", cm.utils.FormatClassList(progression, " → ")) - } - - // Add starting stats - startingStats := cm.integration.GetClassStartingStats(classID) - if len(startingStats) > 0 { - result += "Starting Stats:\n" - for stat, value := range startingStats { - result += fmt.Sprintf(" %s: %d\n", stat, value) - } - } - - // Add usage statistics if available - cm.mutex.RLock() - usage, hasUsage := cm.classUsageStats[classID] - cm.mutex.RUnlock() - - if hasUsage { - result += fmt.Sprintf("Usage Count: %d\n", usage) - } - - return result, nil -} - -// handleRandomCommand generates random classes -func (cm *ClassManager) handleRandomCommand(args []string) (string, error) { - classType := ClassTypeAdventure - if len(args) > 0 { - classType = args[0] - } - - classID := cm.utils.GetRandomClassByType(classType) - if classID == -1 { - return "Failed to generate random class", nil - } - - displayName := cm.classes.GetClassNameCase(classID) - actualType := cm.classes.GetClassType(classID) - - return fmt.Sprintf("Random %s Class: %s (ID: %d)", actualType, displayName, classID), nil -} - -// handleStatsCommand shows class system statistics -func (cm *ClassManager) handleStatsCommand(args []string) (string, error) { - systemStats := cm.utils.GetClassStatistics() - usageStats := cm.GetClassUsageStats() - - result := "Class System Statistics:\n" - result += fmt.Sprintf("Total Classes: %d\n", systemStats["total_classes"]) - result += fmt.Sprintf("Adventure Classes: %d\n", systemStats["adventure_classes"]) - result += fmt.Sprintf("Tradeskill Classes: %d\n", systemStats["tradeskill_classes"]) - result += fmt.Sprintf("Special Classes: %d\n", systemStats["special_classes"]) - - if len(usageStats) > 0 { - result += "\nUsage Statistics:\n" - mostPopular, maxUsage := cm.GetMostPopularClass() - leastPopular, minUsage := cm.GetLeastPopularClass() - - if mostPopular != -1 { - mostPopularName := cm.classes.GetClassNameCase(mostPopular) - result += fmt.Sprintf("Most Popular: %s (%d uses)\n", mostPopularName, maxUsage) - } - - if leastPopular != -1 { - leastPopularName := cm.classes.GetClassNameCase(leastPopular) - result += fmt.Sprintf("Least Popular: %s (%d uses)\n", leastPopularName, minUsage) - } - } - - // Show base class distribution - if baseDistribution, exists := systemStats["base_class_distribution"]; exists { - result += "\nBase Class Distribution:\n" - distribution := baseDistribution.(map[string][]string) - for baseClass, subClasses := range distribution { - result += fmt.Sprintf("%s: %d subclasses\n", baseClass, len(subClasses)) - } - } - - return result, nil -} - -// handleSearchCommand searches for classes by pattern -func (cm *ClassManager) handleSearchCommand(args []string) (string, error) { - if len(args) == 0 { - return "", fmt.Errorf("search pattern required") - } - - pattern := args[0] - matchingClasses := cm.utils.GetClassesByPattern(pattern) - - if len(matchingClasses) == 0 { - return fmt.Sprintf("No classes found matching pattern: %s", pattern), nil - } - - result := fmt.Sprintf("Classes matching '%s':\n", pattern) - for _, classID := range matchingClasses { - displayName := cm.classes.GetClassNameCase(classID) - classType := cm.classes.GetClassType(classID) - baseClass := cm.classes.GetBaseClass(classID) - baseClassName := cm.classes.GetClassNameCase(baseClass) - result += fmt.Sprintf("%d: %s (%s, Base: %s)\n", classID, displayName, classType, baseClassName) - } - - return result, nil -} - -// handleProgressionCommand shows class progression information -func (cm *ClassManager) handleProgressionCommand(args []string) (string, error) { - if len(args) == 0 { - return "", fmt.Errorf("class name or ID required") - } - - classID := cm.utils.ParseClassName(args[0]) - if classID == -1 { - return fmt.Sprintf("Invalid class: %s", args[0]), nil - } - - progression := cm.utils.GetClassProgression(classID) - if len(progression) <= 1 { - return fmt.Sprintf("Class %s has no progression path", cm.classes.GetClassNameCase(classID)), nil - } - - result := fmt.Sprintf("Progression Path for %s:\n", cm.classes.GetClassNameCase(classID)) - for i, stepClassID := range progression { - stepName := cm.classes.GetClassNameCase(stepClassID) - if i == 0 { - result += fmt.Sprintf("1. %s (Starting Class)\n", stepName) - } else if i == len(progression)-1 { - result += fmt.Sprintf("%d. %s (Final Class)\n", i+1, stepName) - } else { - result += fmt.Sprintf("%d. %s\n", i+1, stepName) - } - } - - return result, nil -} - -// ValidateEntityClasses validates classes for a collection of entities -func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]any { - validationResults := make(map[string]any) - - validCount := 0 - invalidCount := 0 - classDistribution := make(map[int8]int) - - for i, entity := range entities { - classID := entity.GetClass() - isValid := cm.classes.IsValidClassID(classID) - - if isValid { - validCount++ - classDistribution[classID]++ - } else { - invalidCount++ - } - - // Track invalid entities - if !isValid { - if validationResults["invalid_entities"] == nil { - validationResults["invalid_entities"] = make([]map[string]any, 0) - } - - invalidList := validationResults["invalid_entities"].([]map[string]any) - invalidList = append(invalidList, map[string]any{ - "index": i, - "class_id": classID, - }) - validationResults["invalid_entities"] = invalidList - } - } - - validationResults["total_entities"] = len(entities) - validationResults["valid_count"] = validCount - validationResults["invalid_count"] = invalidCount - validationResults["class_distribution"] = classDistribution - - return validationResults -} - -// GetClassRecommendations returns class recommendations for character creation -func (cm *ClassManager) GetClassRecommendations(preferences map[string]any) []int8 { - recommendations := make([]int8, 0) - - // Check for class type preference - if classType, exists := preferences["class_type"]; exists { - if typeStr, ok := classType.(string); ok { - allClasses := cm.classes.GetAllClasses() - for classID := range allClasses { - if cm.classes.GetClassType(classID) == typeStr { - recommendations = append(recommendations, classID) - } - } - } - } - - // Check for base class preference - if baseClass, exists := preferences["base_class"]; exists { - if baseClassID, ok := baseClass.(int8); ok { - subClasses := cm.utils.GetClassesForBaseClass(baseClassID) - recommendations = append(recommendations, subClasses...) - } - } - - // Check for specific stat preferences - if preferredStats, exists := preferences["preferred_stats"]; exists { - if stats, ok := preferredStats.([]string); ok { - allClasses := cm.classes.GetAllClasses() - - for classID := range allClasses { - startingStats := cm.integration.GetClassStartingStats(classID) - - // Check if this class has bonuses in preferred stats - hasPreferredBonus := false - for _, preferredStat := range stats { - if statValue, exists := startingStats[preferredStat]; exists && statValue > 52 { // Above base of 50 + minor bonus - hasPreferredBonus = true - break - } - } - - if hasPreferredBonus { - recommendations = append(recommendations, classID) - } - } - } - } - - // If no specific preferences, recommend popular classes - if len(recommendations) == 0 { - // Get usage stats and recommend most popular classes - usageStats := cm.GetClassUsageStats() - if len(usageStats) > 0 { - // Sort by usage and take top classes - // For simplicity, just return all classes with usage > 0 - for classID, usage := range usageStats { - if usage > 0 { - recommendations = append(recommendations, classID) - } - } - } - - // If still no recommendations, return a default set of beginner-friendly classes - if len(recommendations) == 0 { - recommendations = []int8{ClassWarrior, ClassCleric, ClassWizard, ClassRogue} - } - } - - return recommendations -} - -// Global class manager instance -var globalClassManager *ClassManager -var initClassManagerOnce sync.Once - -// GetGlobalClassManager returns the global class manager (singleton) -func GetGlobalClassManager() *ClassManager { - initClassManagerOnce.Do(func() { - globalClassManager = NewClassManager() - }) - return globalClassManager -} diff --git a/internal/classes/utils.go b/internal/classes/utils.go deleted file mode 100644 index f350b15..0000000 --- a/internal/classes/utils.go +++ /dev/null @@ -1,450 +0,0 @@ -package classes - -import ( - "math/rand" - "strings" -) - -// ClassUtils provides utility functions for class operations -type ClassUtils struct { - classes *Classes -} - -// NewClassUtils creates a new class utilities instance -func NewClassUtils() *ClassUtils { - return &ClassUtils{ - classes: GetGlobalClasses(), - } -} - -// ParseClassName attempts to parse a class name from various input formats -func (cu *ClassUtils) ParseClassName(input string) int8 { - if input == "" { - return -1 - } - - // Try direct lookup first - classID := cu.classes.GetClassID(input) - if classID != -1 { - return classID - } - - // Try with common variations - variations := []string{ - strings.ToUpper(input), - strings.ReplaceAll(strings.ToUpper(input), " ", ""), - strings.ReplaceAll(strings.ToUpper(input), "_", ""), - strings.ReplaceAll(strings.ToUpper(input), "-", ""), - } - - for _, variation := range variations { - if classID := cu.classes.GetClassID(variation); classID != -1 { - return classID - } - } - - // Try matching against friendly names (case insensitive) - inputLower := strings.ToLower(input) - allClasses := cu.classes.GetAllClasses() - for classID, displayName := range allClasses { - if strings.ToLower(displayName) == inputLower { - return classID - } - } - - return -1 // Not found -} - -// FormatClassName returns a properly formatted class name -func (cu *ClassUtils) FormatClassName(classID int8, format string) string { - switch strings.ToLower(format) { - case "display", "friendly", "proper": - return cu.classes.GetClassNameCase(classID) - case "upper", "uppercase": - return cu.classes.GetClassName(classID) - case "lower", "lowercase": - return strings.ToLower(cu.classes.GetClassName(classID)) - default: - return cu.classes.GetClassNameCase(classID) // Default to friendly name - } -} - -// GetRandomClassByType returns a random class of the specified type -func (cu *ClassUtils) GetRandomClassByType(classType string) int8 { - allClasses := cu.classes.GetAllClasses() - validClasses := make([]int8, 0) - - for classID := range allClasses { - if cu.classes.GetClassType(classID) == classType { - validClasses = append(validClasses, classID) - } - } - - if len(validClasses) == 0 { - return DefaultClassID - } - - return validClasses[rand.Intn(len(validClasses))] -} - -// GetRandomAdventureClass returns a random adventure class -func (cu *ClassUtils) GetRandomAdventureClass() int8 { - return cu.GetRandomClassByType(ClassTypeAdventure) -} - -// GetRandomTradeskillClass returns a random tradeskill class -func (cu *ClassUtils) GetRandomTradeskillClass() int8 { - return cu.GetRandomClassByType(ClassTypeTradeskill) -} - -// ValidateClassForRace checks if a class is valid for a specific race -// This is a placeholder for future race-class restrictions -func (cu *ClassUtils) ValidateClassForRace(classID, raceID int8) bool { - // TODO: Implement race-class restrictions when race system is available - // For now, all classes can be all races - return cu.classes.IsValidClassID(classID) -} - -// GetClassDescription returns a description of the class -func (cu *ClassUtils) GetClassDescription(classID int8) string { - // This would typically come from a database or configuration - // For now, provide basic descriptions based on class - - switch classID { - case ClassCommoner: - return "A starting class for all characters before choosing their path." - case ClassFighter: - return "Warriors who excel in melee combat and defense." - case ClassWarrior: - return "Masters of weapons and armor, the ultimate melee combatants." - case ClassGuardian: - return "Defensive warriors who protect their allies with shield and sword." - case ClassBerserker: - return "Rage-fueled fighters who sacrifice defense for devastating attacks." - case ClassBrawler: - return "Hand-to-hand combat specialists who fight with fists and focus." - case ClassMonk: - return "Disciplined fighters who use martial arts and inner peace." - case ClassBruiser: - return "Brutal brawlers who overwhelm enemies with raw power." - case ClassCrusader: - return "Holy warriors who blend combat prowess with divine magic." - case ClassShadowknight: - return "Dark knights who wield unholy magic alongside martial skill." - case ClassPaladin: - return "Champions of good who protect the innocent with sword and spell." - case ClassPriest: - return "Divine casters who channel the power of the gods." - case ClassCleric: - return "Healers and supporters who keep their allies alive and fighting." - case ClassTemplar: - return "Protective priests who shield allies from harm." - case ClassInquisitor: - return "Militant clerics who combine healing with righteous fury." - case ClassDruid: - return "Nature priests who harness the power of the natural world." - case ClassWarden: - return "Protective druids who shield allies with nature's blessing." - case ClassFury: - return "Destructive druids who unleash nature's wrath upon enemies." - case ClassShaman: - return "Spirit-workers who commune with ancestors and totems." - case ClassMystic: - return "Supportive shamans who provide wards and spiritual guidance." - case ClassDefiler: - return "Dark shamans who corrupt and weaken their enemies." - case ClassMage: - return "Wielders of arcane magic who bend reality to their will." - case ClassSorcerer: - return "Destructive mages who specialize in damaging spells." - case ClassWizard: - return "Scholarly sorcerers who master the elements." - case ClassWarlock: - return "Dark sorcerers who deal in forbidden magic." - case ClassEnchanter: - return "Mind-controlling mages who manipulate enemies and allies." - case ClassIllusionist: - return "Deceptive enchanters who confuse and misdirect." - case ClassCoercer: - return "Dominating enchanters who force enemies to obey." - case ClassSummoner: - return "Mages who call forth creatures to fight for them." - case ClassConjuror: - return "Elemental summoners who command earth and air." - case ClassNecromancer: - return "Death mages who raise undead minions and drain life." - case ClassScout: - return "Agile fighters who rely on speed and cunning." - case ClassRogue: - return "Stealthy combatants who strike from the shadows." - case ClassSwashbuckler: - return "Dashing rogues who fight with finesse and flair." - case ClassBrigand: - return "Brutal rogues who prefer dirty fighting tactics." - case ClassBard: - return "Musical combatants who inspire allies and demoralize foes." - case ClassTroubador: - return "Supportive bards who strengthen their allies." - case ClassDirge: - return "Dark bards who weaken enemies with haunting melodies." - case ClassPredator: - return "Hunters who excel at tracking and ranged combat." - case ClassRanger: - return "Nature-loving predators who protect the wilderness." - case ClassAssassin: - return "Deadly predators who eliminate targets with precision." - case ClassAnimalist: - return "Beast masters who fight alongside animal companions." - case ClassBeastlord: - return "Animalists who have formed powerful bonds with their pets." - case ClassShaper: - return "Mystic priests who manipulate spiritual energy." - case ClassChanneler: - return "Shapers who focus spiritual power through channeling." - case ClassArtisan: - return "Crafters who create useful items for adventurers." - case ClassCraftsman: - return "Specialized artisans who work with physical materials." - case ClassProvisioner: - return "Food and drink specialists who create consumables." - case ClassWoodworker: - return "Crafters who work with wood to create furniture and tools." - case ClassCarpenter: - return "Master woodworkers who create complex wooden items." - case ClassOutfitter: - return "Equipment crafters who create armor and weapons." - case ClassArmorer: - return "Specialists in creating protective armor." - case ClassWeaponsmith: - return "Masters of weapon crafting and enhancement." - case ClassTailor: - return "Cloth workers who create clothing and soft armor." - case ClassScholar: - return "Academic crafters who create magical and scholarly items." - case ClassJeweler: - return "Specialists in creating jewelry and accessories." - case ClassSage: - return "Book and scroll crafters who preserve knowledge." - case ClassAlchemist: - return "Potion makers who brew magical elixirs and potions." - default: - return "An unknown class with mysterious abilities." - } -} - -// GetClassProgression returns the class progression path -func (cu *ClassUtils) GetClassProgression(classID int8) []int8 { - progression := make([]int8, 0) - - // Always start with Commoner (except for Commoner itself) - if classID != ClassCommoner { - progression = append(progression, ClassCommoner) - } - - // Add base class if different from current - baseClass := cu.classes.GetBaseClass(classID) - if baseClass != classID && baseClass != ClassCommoner { - progression = append(progression, baseClass) - } - - // Add secondary base class if different - secondaryBase := cu.classes.GetSecondaryBaseClass(classID) - if secondaryBase != classID && secondaryBase != baseClass && secondaryBase != ClassCommoner { - progression = append(progression, secondaryBase) - } - - // Add the final class - progression = append(progression, classID) - - return progression -} - -// GetClassesForBaseClass returns all classes that belong to a base class -func (cu *ClassUtils) GetClassesForBaseClass(baseClassID int8) []int8 { - result := make([]int8, 0) - allClasses := cu.classes.GetAllClasses() - - for classID := range allClasses { - if cu.classes.GetBaseClass(classID) == baseClassID { - result = append(result, classID) - } - } - - return result -} - -// GetClassesBySecondaryBase returns all classes that belong to a secondary base class -func (cu *ClassUtils) GetClassesBySecondaryBase(secondaryBaseID int8) []int8 { - result := make([]int8, 0) - allClasses := cu.classes.GetAllClasses() - - for classID := range allClasses { - if cu.classes.GetSecondaryBaseClass(classID) == secondaryBaseID { - result = append(result, classID) - } - } - - return result -} - -// GetClassesByPattern returns classes matching a name pattern -func (cu *ClassUtils) GetClassesByPattern(pattern string) []int8 { - pattern = strings.ToLower(pattern) - result := make([]int8, 0) - - allClasses := cu.classes.GetAllClasses() - for classID, displayName := range allClasses { - if strings.Contains(strings.ToLower(displayName), pattern) { - result = append(result, classID) - } - } - - return result -} - -// ValidateClassTransition checks if a class change is allowed -func (cu *ClassUtils) ValidateClassTransition(fromClassID, toClassID int8) (bool, string) { - if !cu.classes.IsValidClassID(fromClassID) { - return false, "Invalid source class" - } - - if !cu.classes.IsValidClassID(toClassID) { - return false, "Invalid target class" - } - - if fromClassID == toClassID { - return false, "Cannot change to the same class" - } - - // Basic progression validation - can only advance, not go backward - fromProgression := cu.GetClassProgression(fromClassID) - toProgression := cu.GetClassProgression(toClassID) - - // Check if the target class is a valid advancement - if len(toProgression) <= len(fromProgression) { - return false, "Cannot regress to a lower tier class" - } - - // Check if the progressions are compatible (share the same base path) - for i := 0; i < len(fromProgression); i++ { - if i >= len(toProgression) || fromProgression[i] != toProgression[i] { - return false, "Incompatible class progression paths" - } - } - - return true, "" -} - -// GetClassAliases returns common aliases for a class -func (cu *ClassUtils) GetClassAliases(classID int8) []string { - aliases := make([]string, 0) - - switch classID { - case ClassShadowknight: - aliases = append(aliases, "SK", "Shadow Knight", "Dark Knight") - case ClassSwashbuckler: - aliases = append(aliases, "Swash", "Swashy") - case ClassTroubador: - aliases = append(aliases, "Troub", "Troubadour") - case ClassIllusionist: - aliases = append(aliases, "Illy", "Illusion") - case ClassConjuror: - aliases = append(aliases, "Conj", "Conjurer") - case ClassNecromancer: - aliases = append(aliases, "Necro", "Nec") - case ClassBeastlord: - aliases = append(aliases, "BL", "Beast Lord") - case ClassWeaponsmith: - aliases = append(aliases, "WS", "Weapon Smith") - } - - // Always include the official names - aliases = append(aliases, cu.classes.GetClassName(classID)) - aliases = append(aliases, cu.classes.GetClassNameCase(classID)) - - return aliases -} - -// GetClassStatistics returns statistics about the class system -func (cu *ClassUtils) GetClassStatistics() map[string]any { - stats := make(map[string]any) - - allClasses := cu.classes.GetAllClasses() - stats["total_classes"] = len(allClasses) - - adventureCount := 0 - tradeskillCount := 0 - specialCount := 0 - - for classID := range allClasses { - switch cu.classes.GetClassType(classID) { - case ClassTypeAdventure: - adventureCount++ - case ClassTypeTradeskill: - tradeskillCount++ - default: - specialCount++ - } - } - - stats["adventure_classes"] = adventureCount - stats["tradeskill_classes"] = tradeskillCount - stats["special_classes"] = specialCount - - // Base class distribution - baseClassDistribution := make(map[string][]string) - for classID, displayName := range allClasses { - if cu.classes.IsAdventureClass(classID) { - baseClassID := cu.classes.GetBaseClass(classID) - baseClassName := cu.classes.GetClassNameCase(baseClassID) - baseClassDistribution[baseClassName] = append(baseClassDistribution[baseClassName], displayName) - } - } - stats["base_class_distribution"] = baseClassDistribution - - return stats -} - -// FormatClassList returns a formatted string of class names -func (cu *ClassUtils) FormatClassList(classIDs []int8, separator string) string { - if len(classIDs) == 0 { - return "" - } - - names := make([]string, len(classIDs)) - for i, classID := range classIDs { - names[i] = cu.classes.GetClassNameCase(classID) - } - - return strings.Join(names, separator) -} - -// GetEQClassName returns the EQ-style class name for a given class and level -// This is a placeholder for the original C++ GetEQClassName functionality -func (cu *ClassUtils) GetEQClassName(classID int8, level int8) string { - // TODO: Implement level-based class names when level system is available - // For now, just return the display name - return cu.classes.GetClassNameCase(classID) -} - -// GetStartingClass returns the appropriate starting class for character creation -func (cu *ClassUtils) GetStartingClass() int8 { - return ClassCommoner -} - -// IsBaseClass checks if a class is a base class (Fighter, Priest, Mage, Scout) -func (cu *ClassUtils) IsBaseClass(classID int8) bool { - return classID == ClassFighter || classID == ClassPriest || classID == ClassMage || classID == ClassScout -} - -// IsSecondaryBaseClass checks if a class is a secondary base class -func (cu *ClassUtils) IsSecondaryBaseClass(classID int8) bool { - // Check if any class has this as their secondary base - allClasses := cu.classes.GetAllClasses() - for checkClassID := range allClasses { - if cu.classes.GetSecondaryBaseClass(checkClassID) == classID && checkClassID != classID { - return true - } - } - return false -}