612 lines
18 KiB
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 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
|
|
}
|