package traits import ( "fmt" "log" "sync" ) // NewMasterTraitList creates a new master trait list. func NewMasterTraitList() *MasterTraitList { return &MasterTraitList{ traitList: make([]TraitData, 0), } } // AddTrait adds a trait to the master list. func (mtl *MasterTraitList) AddTrait(data *TraitData) error { if data == nil { return ErrInvalidPlayer } if err := data.Validate(); err != nil { return err } mtl.mutex.Lock() defer mtl.mutex.Unlock() // Make a copy to avoid external modifications traitCopy := *data mtl.traitList = append(mtl.traitList, traitCopy) return nil } // Size returns the total number of traits in the master list. func (mtl *MasterTraitList) Size() int { mtl.mutex.RLock() defer mtl.mutex.RUnlock() return len(mtl.traitList) } // GetTrait retrieves a trait by spell ID. func (mtl *MasterTraitList) GetTrait(spellID uint32) *TraitData { mtl.mutex.RLock() defer mtl.mutex.RUnlock() for i := range mtl.traitList { if mtl.traitList[i].SpellID == spellID { // Return a copy to prevent external modifications traitCopy := mtl.traitList[i] return &traitCopy } } return nil } // GetTraitByItemID retrieves a trait by item ID. func (mtl *MasterTraitList) GetTraitByItemID(itemID uint32) *TraitData { mtl.mutex.RLock() defer mtl.mutex.RUnlock() for i := range mtl.traitList { if mtl.traitList[i].ItemID == itemID { // Return a copy to prevent external modifications traitCopy := mtl.traitList[i] return &traitCopy } } return nil } // DestroyTraits clears all traits from the master list. func (mtl *MasterTraitList) DestroyTraits() { mtl.mutex.Lock() defer mtl.mutex.Unlock() mtl.traitList = mtl.traitList[:0] } // GenerateTraitLists organizes traits into categorized lists for a specific player. func (mtl *MasterTraitList) GenerateTraitLists(playerState *PlayerTraitState, maxLevel int16, traitGroup int8) bool { if playerState == nil { log.Printf("GenerateTraitLists called with nil player state") return false } if mtl.Size() == 0 { return false } mtl.mutex.RLock() defer mtl.mutex.RUnlock() // Clear existing lists playerState.TraitLists.Clear() for i := range mtl.traitList { trait := &mtl.traitList[i] // Skip if level requirement not met if maxLevel > 0 && trait.Level > int8(maxLevel) { continue } // Skip if specific group requested and this isn't it if traitGroup != UnassignedGroupID && traitGroup != trait.Group { continue } // Categorize the trait mtl.categorizeTraitForPlayer(trait, playerState) } return true } // categorizeTraitForPlayer adds a trait to the appropriate category for a player. func (mtl *MasterTraitList) categorizeTraitForPlayer(trait *TraitData, playerState *PlayerTraitState) { // Character Traits (universal traits) if trait.IsUniversalTrait() { mtl.addToSortedTraitList(trait, playerState.TraitLists.SortedTraitList) log.Printf("Added Character Trait: %d Tier %d", trait.SpellID, trait.Tier) return } // Class Training if trait.ClassReq == playerState.Class && trait.IsTraining { mtl.addToLevelMap(trait, playerState.TraitLists.ClassTraining) return } // Racial Abilities (non-innate) if trait.RaceReq == playerState.Race && !trait.IsInnate && !trait.IsTraining { mtl.addToGroupMap(trait, playerState.TraitLists.RaceTraits) return } // Innate Racial Abilities if trait.RaceReq == playerState.Race && trait.IsInnate { mtl.addToGroupMap(trait, playerState.TraitLists.InnateRaceTraits) return } // Focus Effects if (trait.ClassReq == playerState.Class || trait.ClassReq == UniversalClassReq) && trait.IsFocusEffect { mtl.addToGroupMap(trait, playerState.TraitLists.FocusEffects) return } } // addToSortedTraitList adds a trait to the sorted trait list (group -> level -> traits). func (mtl *MasterTraitList) addToSortedTraitList(trait *TraitData, sortedList map[int8]map[int8][]*TraitData) { // Ensure group map exists if sortedList[trait.Group] == nil { sortedList[trait.Group] = make(map[int8][]*TraitData) } // Add to the appropriate level within the group sortedList[trait.Group][trait.Level] = append(sortedList[trait.Group][trait.Level], trait) } // addToLevelMap adds a trait to a level-indexed map. func (mtl *MasterTraitList) addToLevelMap(trait *TraitData, levelMap map[int8][]*TraitData) { levelMap[trait.Level] = append(levelMap[trait.Level], trait) } // addToGroupMap adds a trait to a group-indexed map. func (mtl *MasterTraitList) addToGroupMap(trait *TraitData, groupMap map[int8][]*TraitData) { groupMap[trait.Group] = append(groupMap[trait.Group], trait) } // IsPlayerAllowedTrait checks if a player is allowed to select a specific trait. func (mtl *MasterTraitList) IsPlayerAllowedTrait(playerState *PlayerTraitState, trait *TraitData, config *TraitSystemConfig) bool { if playerState == nil || trait == nil || config == nil { return false } // Refresh trait lists if needed if playerState.NeedTraitUpdate { if !mtl.GenerateTraitLists(playerState, 0, UnassignedGroupID) { return false } playerState.NeedTraitUpdate = false } // Check trait type and calculate availability if trait.IsFocusEffect { return mtl.checkFocusEffectAllowed(playerState, config) } if trait.IsTraining { return mtl.checkTrainingAllowed(playerState, config) } if trait.RaceReq == playerState.Race { return mtl.checkRacialTraitAllowed(playerState, config) } // Character trait return mtl.checkCharacterTraitAllowed(playerState, config) } // checkFocusEffectAllowed checks if player can select focus effects. func (mtl *MasterTraitList) checkFocusEffectAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool { var numAvailableSelections int16 if config.FocusSelectLevel > 0 { numAvailableSelections = playerState.Level / int16(config.FocusSelectLevel) } totalUsed := mtl.getSpellCount(playerState, playerState.TraitLists.FocusEffects, false) // Check classic table if enabled if config.UseClassicLevelTable { classicAvail := mtl.getClassicAvailability(PersonalTraitLevelLimits, totalUsed, playerState.Level) if classicAvail >= 0 { numAvailableSelections = classicAvail } else { numAvailableSelections = 0 } } log.Printf("Player %d FocusEffects used %d, available %d", playerState.PlayerID, totalUsed, numAvailableSelections) return totalUsed < numAvailableSelections } // checkTrainingAllowed checks if player can select training abilities. func (mtl *MasterTraitList) checkTrainingAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool { var numAvailableSelections int16 if config.TrainingSelectLevel > 0 { numAvailableSelections = playerState.Level / int16(config.TrainingSelectLevel) } totalUsed := mtl.getSpellCount(playerState, playerState.TraitLists.ClassTraining, false) // Check classic table if enabled if config.UseClassicLevelTable { classicAvail := mtl.getClassicAvailability(TrainingTraitLevelLimits, totalUsed, playerState.Level) if classicAvail >= 0 { numAvailableSelections = classicAvail } else { numAvailableSelections = 0 } } log.Printf("Player %d ClassTraining used %d, available %d", playerState.PlayerID, totalUsed, numAvailableSelections) return totalUsed < numAvailableSelections } // checkRacialTraitAllowed checks if player can select racial traits. func (mtl *MasterTraitList) checkRacialTraitAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool { var numAvailableSelections int16 if config.RaceSelectLevel > 0 { numAvailableSelections = playerState.Level / int16(config.RaceSelectLevel) } totalUsed := mtl.getSpellCount(playerState, playerState.TraitLists.RaceTraits, false) + mtl.getSpellCount(playerState, playerState.TraitLists.InnateRaceTraits, false) // Check classic table if enabled if config.UseClassicLevelTable { classicAvail := mtl.getClassicAvailability(RacialTraitLevelLimits, totalUsed, playerState.Level) if classicAvail >= 0 { numAvailableSelections = classicAvail } else { numAvailableSelections = 0 } } log.Printf("Player %d RaceTraits used %d, available %d", playerState.PlayerID, totalUsed, numAvailableSelections) return totalUsed < numAvailableSelections } // checkCharacterTraitAllowed checks if player can select character traits. func (mtl *MasterTraitList) checkCharacterTraitAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool { var numAvailableSelections int16 if config.CharacterSelectLevel > 0 { numAvailableSelections = playerState.Level / int16(config.CharacterSelectLevel) } // Count character traits from sorted list totalUsed := int16(0) for _, levelMap := range playerState.TraitLists.SortedTraitList { totalUsed += mtl.getSpellCount(playerState, levelMap, true) } // Check classic table if enabled if config.UseClassicLevelTable { classicAvail := mtl.getClassicAvailability(CharacterTraitLevelLimits, totalUsed, playerState.Level) if classicAvail >= 0 { numAvailableSelections = classicAvail } else { numAvailableSelections = 0 } } log.Printf("Player %d CharacterTraits used %d, available %d", playerState.PlayerID, totalUsed, numAvailableSelections) return totalUsed < numAvailableSelections } // getClassicAvailability calculates availability using classic level tables. func (mtl *MasterTraitList) getClassicAvailability(levelLimits []int16, totalUsed int16, playerLevel int16) int16 { nextIndex := int(totalUsed + 1) if nextIndex < len(levelLimits) { classicLevelReq := levelLimits[nextIndex] if playerLevel >= classicLevelReq { return totalUsed + 1 } } return -1 } // getSpellCount counts how many spells from a trait map the player has selected. func (mtl *MasterTraitList) getSpellCount(playerState *PlayerTraitState, traitMap any, onlyCharTraits bool) int16 { count := int16(0) switch tm := traitMap.(type) { case map[int8][]*TraitData: // Level-indexed map (like ClassTraining) for _, traits := range tm { for _, trait := range traits { if playerState.HasTrait(trait.SpellID) { if !onlyCharTraits || (onlyCharTraits && trait.IsUniversalTrait()) { count++ } } } } case map[int8]map[int8][]*TraitData: // Group-level indexed map (like SortedTraitList) for _, levelMap := range tm { for _, traits := range levelMap { for _, trait := range traits { if playerState.HasTrait(trait.SpellID) { if !onlyCharTraits || (onlyCharTraits && trait.IsUniversalTrait()) { count++ } } } } } } return count } // IdentifyNextTrait identifies traits available for selection based on progression rules. func (mtl *MasterTraitList) IdentifyNextTrait(playerState *PlayerTraitState, traitMap map[int8][]*TraitData, context *TraitSelectionContext, omitFoundMatches bool) bool { foundMatch := false for _, traits := range traitMap { for _, trait := range traits { // Handle tiered selection logic if context.TieredSelection { if context.FoundSpellMatch && trait.Group == context.GroupToApply { continue // Skip this group } else if trait.Group != context.GroupToApply { if context.GroupToApply != UnassignedGroupID && !context.FoundSpellMatch { log.Printf("Found match to group id %d", context.GroupToApply) foundMatch = true break } else { log.Printf("Try match to group... spell id %d, group id %d", trait.SpellID, trait.Group) context.FoundSpellMatch = false context.GroupToApply = trait.Group if !omitFoundMatches { context.TieredTraits = context.TieredTraits[:0] } } } } // Check if spell was previously matched if prevGroup, exists := context.PreviousMatchedSpells[trait.SpellID]; exists && trait.Group > prevGroup { continue } // Check if player is allowed this trait config := &TraitSystemConfig{ TieringSelection: context.TieredSelection, UseClassicLevelTable: true, // TODO: Get from rules FocusSelectLevel: DefaultFocusSelectLevel, TrainingSelectLevel: DefaultTrainingSelectLevel, RaceSelectLevel: DefaultRaceSelectLevel, CharacterSelectLevel: DefaultCharacterSelectLevel, } if !mtl.IsPlayerAllowedTrait(playerState, trait, config) { log.Printf("Player not allowed trait: spell id %d, group id %d", trait.SpellID, trait.Group) context.FoundSpellMatch = true } else if playerState.HasTrait(trait.SpellID) { log.Printf("Found existing spell match: spell id %d, group id %d", trait.SpellID, trait.Group) if !omitFoundMatches { context.FoundSpellMatch = true } context.PreviousMatchedSpells[trait.SpellID] = trait.Group } else { context.TieredTraits = append(context.TieredTraits, trait) context.CollectTraits = append(context.CollectTraits, trait) } } if foundMatch { break } } // Final match check if !foundMatch && context.GroupToApply != UnassignedGroupID && !context.FoundSpellMatch { foundMatch = true } else if !context.TieredSelection && len(context.CollectTraits) > 0 { foundMatch = true } return foundMatch } // ChooseNextTrait processes trait selection for a player and returns available choices. func (mtl *MasterTraitList) ChooseNextTrait(playerState *PlayerTraitState, config *TraitSystemConfig) ([]*TraitData, error) { if playerState == nil { return nil, ErrInvalidPlayer } // Generate trait lists if !mtl.GenerateTraitLists(playerState, playerState.Level, UnassignedGroupID) { return nil, fmt.Errorf("failed to generate trait lists") } context := NewTraitSelectionContext(config.TieringSelection) match := false // Check different trait types in priority order if !match || !config.TieringSelection { match = mtl.IdentifyNextTrait(playerState, playerState.TraitLists.ClassTraining, context, false) } if !match || !config.TieringSelection { match = mtl.IdentifyNextTrait(playerState, playerState.TraitLists.RaceTraits, context, true) overrideMatch := mtl.IdentifyNextTrait(playerState, playerState.TraitLists.InnateRaceTraits, context, true) if !match && overrideMatch { match = true } } if !match || !config.TieringSelection { match = mtl.IdentifyNextTrait(playerState, playerState.TraitLists.FocusEffects, context, false) } // Return appropriate trait list if !config.TieringSelection && len(context.CollectTraits) > 0 { return context.CollectTraits, nil } else if match { return context.TieredTraits, nil } return nil, nil } // GetStats returns statistics about the master trait list. func (mtl *MasterTraitList) GetStats() TraitManagerStats { mtl.mutex.RLock() defer mtl.mutex.RUnlock() stats := TraitManagerStats{ TotalTraits: int32(len(mtl.traitList)), TraitsByType: make(map[string]int32), TraitsByGroup: make(map[int8]int32), TraitsByLevel: make(map[int8]int32), } for i := range mtl.traitList { trait := &mtl.traitList[i] // Count by type traitType := trait.GetTraitType() stats.TraitsByType[traitType]++ // Count by group stats.TraitsByGroup[trait.Group]++ // Count by level stats.TraitsByLevel[trait.Level]++ } return stats } // ValidateTraitSelection validates a trait selection request. func (mtl *MasterTraitList) ValidateTraitSelection(playerState *PlayerTraitState, request *TraitSelectionRequest, config *TraitSystemConfig) *TraitValidationResult { result := &TraitValidationResult{ Allowed: false, Reason: "Unknown validation error", } if playerState == nil || request == nil { result.Reason = "Invalid player or request" return result } // Validate each requested trait for _, spellID := range request.TraitSpells { trait := mtl.GetTrait(spellID) if trait == nil { result.Reason = fmt.Sprintf("Trait not found: %d", spellID) return result } if !mtl.IsPlayerAllowedTrait(playerState, trait, config) { result.Reason = fmt.Sprintf("Trait not allowed: %d", spellID) return result } } result.Allowed = true result.Reason = "Valid selection" return result } // TraitManager manages trait operations and caches player states. type TraitManager struct { masterList *MasterTraitList playerStates map[uint32]*PlayerTraitState config *TraitSystemConfig mutex sync.RWMutex } // NewTraitManager creates a new trait manager. func NewTraitManager(masterList *MasterTraitList, config *TraitSystemConfig) *TraitManager { return &TraitManager{ masterList: masterList, playerStates: make(map[uint32]*PlayerTraitState), config: config, } } // GetPlayerState gets or creates a player trait state. func (tm *TraitManager) GetPlayerState(playerID uint32, level int16, classID, raceID int8) *PlayerTraitState { tm.mutex.Lock() defer tm.mutex.Unlock() state, exists := tm.playerStates[playerID] if !exists { state = NewPlayerTraitState(playerID, level, classID, raceID) tm.playerStates[playerID] = state } else { // Update level if changed state.UpdateLevel(level) } return state } // SelectTraits processes a trait selection request. func (tm *TraitManager) SelectTraits(request *TraitSelectionRequest, playerLevel int16, classID, raceID int8) error { playerState := tm.GetPlayerState(request.PlayerID, playerLevel, classID, raceID) // Validate the selection result := tm.masterList.ValidateTraitSelection(playerState, request, tm.config) if !result.Allowed { return fmt.Errorf("trait selection not allowed: %s", result.Reason) } // Apply the selection for _, spellID := range request.TraitSpells { playerState.SelectTrait(spellID) } log.Printf("Player %d selected %d traits", request.PlayerID, len(request.TraitSpells)) return nil } // GetAvailableTraits gets traits available for selection by a player. func (tm *TraitManager) GetAvailableTraits(playerID uint32, level int16, classID, raceID int8) ([]*TraitData, error) { playerState := tm.GetPlayerState(playerID, level, classID, raceID) return tm.masterList.ChooseNextTrait(playerState, tm.config) } // ClearPlayerState removes a player's cached trait state. func (tm *TraitManager) ClearPlayerState(playerID uint32) { tm.mutex.Lock() defer tm.mutex.Unlock() delete(tm.playerStates, playerID) } // GetManagerStats returns statistics about the trait manager. func (tm *TraitManager) GetManagerStats() TraitManagerStats { tm.mutex.RLock() playersWithTraits := int32(len(tm.playerStates)) tm.mutex.RUnlock() stats := tm.masterList.GetStats() stats.PlayersWithTraits = playersWithTraits return stats }