eq2go/internal/traits/manager.go

612 lines
18 KiB
Go

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 interface{}, 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
}