package traits import ( "fmt" "log" "strconv" ) // TraitPacketBuilder handles building trait-related packets for client communication. type TraitPacketBuilder interface { // BuildTraitListPacket builds the main trait list packet (WS_TraitsList) BuildTraitListPacket(playerState *PlayerTraitState, clientVersion int16) (*TraitPacketData, error) // BuildTraitRewardPacket builds a trait reward selection packet (WS_QuestRewardPackMsg) BuildTraitRewardPacket(traits []*TraitData, packetType int8) (*TraitRewardPacket, error) } // TraitRewardPacket represents a trait reward selection packet. type TraitRewardPacket struct { PacketType int8 // Type of trait packet (0-3) SelectRewards []TraitRewardItem // Available trait selections ItemRewards []TraitRewardItem // Associated item rewards } // TraitRewardItem represents a reward item in trait selection. type TraitRewardItem struct { SpellID uint32 // Spell ID for the trait ItemID uint32 // Associated item ID (if any) Name string // Display name Icon uint16 // Icon for display } // DefaultTraitPacketBuilder is the default implementation of TraitPacketBuilder. type DefaultTraitPacketBuilder struct { spellManager SpellManager // Interface to spell system itemManager ItemManager // Interface to item system } // NewDefaultTraitPacketBuilder creates a new default trait packet builder. func NewDefaultTraitPacketBuilder(spellMgr SpellManager, itemMgr ItemManager) *DefaultTraitPacketBuilder { return &DefaultTraitPacketBuilder{ spellManager: spellMgr, itemManager: itemMgr, } } // BuildTraitListPacket builds the comprehensive trait list packet for a player. func (pb *DefaultTraitPacketBuilder) BuildTraitListPacket(playerState *PlayerTraitState, clientVersion int16) (*TraitPacketData, error) { if playerState == nil { return nil, ErrInvalidPlayer } packetData := &TraitPacketData{} // Build character traits section err := pb.buildCharacterTraits(playerState, packetData) if err != nil { return nil, fmt.Errorf("failed to build character traits: %w", err) } // Build class training section err = pb.buildClassTraining(playerState, packetData) if err != nil { return nil, fmt.Errorf("failed to build class training: %w", err) } // Build racial traits section err = pb.buildRacialTraits(playerState, packetData) if err != nil { return nil, fmt.Errorf("failed to build racial traits: %w", err) } // Build innate abilities section err = pb.buildInnateAbilities(playerState, packetData) if err != nil { return nil, fmt.Errorf("failed to build innate abilities: %w", err) } // Build focus effects section (for supported client versions) if clientVersion >= FocusEffectsMinVersion { err = pb.buildFocusEffects(playerState, packetData) if err != nil { return nil, fmt.Errorf("failed to build focus effects: %w", err) } } // Calculate selection availability pb.calculateSelectionAvailability(playerState, packetData) return packetData, nil } // buildCharacterTraits builds the character traits section of the packet. func (pb *DefaultTraitPacketBuilder) buildCharacterTraits(playerState *PlayerTraitState, packetData *TraitPacketData) error { traitLevels := make([]TraitLevelData, 0) // Iterate through sorted trait list (group -> level -> traits) for _, levelMap := range playerState.TraitLists.SortedTraitList { for level, traits := range levelMap { if len(traits) == 0 { continue } levelData := TraitLevelData{ Level: level, SelectedLine: UnassignedGroupID, // Default to no selection Traits: make([]TraitInfo, 0, MaxTraitsPerLine), } // Add up to MaxTraitsPerLine traits for i, trait := range traits { if i >= MaxTraitsPerLine { break } traitInfo, err := pb.buildTraitInfo(trait, playerState) if err != nil { log.Printf("Warning: Failed to build trait info for spell %d: %v", trait.SpellID, err) continue } levelData.Traits = append(levelData.Traits, *traitInfo) // Check if this trait is selected if playerState.HasTrait(trait.SpellID) { levelData.SelectedLine = int8(i) } } // Fill remaining slots with empty entries for len(levelData.Traits) < MaxTraitsPerLine { levelData.Traits = append(levelData.Traits, TraitInfo{ SpellID: EmptyTraitID, Name: "", Icon: EmptyTraitIcon, Icon2: EmptyTraitIcon, Selected: false, Unknown1: EmptyTraitUnknown, Unknown2: EmptyTraitUnknown, }) } traitLevels = append(traitLevels, levelData) } } packetData.CharacterTraits = traitLevels return nil } // buildClassTraining builds the class training section of the packet. func (pb *DefaultTraitPacketBuilder) buildClassTraining(playerState *PlayerTraitState, packetData *TraitPacketData) error { trainingLevels := make([]TraitLevelData, 0) for level, traits := range playerState.TraitLists.ClassTraining { if len(traits) == 0 { continue } levelData := TraitLevelData{ Level: level, SelectedLine: UnassignedGroupID, Traits: make([]TraitInfo, 0, MaxTraitsPerLine), } // Add up to MaxTraitsPerLine traits for i, trait := range traits { if i >= MaxTraitsPerLine { break } traitInfo, err := pb.buildTraitInfo(trait, playerState) if err != nil { log.Printf("Warning: Failed to build training info for spell %d: %v", trait.SpellID, err) continue } levelData.Traits = append(levelData.Traits, *traitInfo) if playerState.HasTrait(trait.SpellID) { levelData.SelectedLine = int8(i) } } // Fill remaining slots with empty entries for len(levelData.Traits) < MaxTraitsPerLine { levelData.Traits = append(levelData.Traits, TraitInfo{ SpellID: EmptyTraitID, Name: "", Icon: EmptyTraitIcon, Icon2: EmptyTraitIcon, Selected: false, Unknown1: EmptyTraitUnknown, Unknown2: EmptyTraitUnknown, }) } trainingLevels = append(trainingLevels, levelData) } packetData.ClassTraining = trainingLevels return nil } // buildRacialTraits builds the racial traits section of the packet. func (pb *DefaultTraitPacketBuilder) buildRacialTraits(playerState *PlayerTraitState, packetData *TraitPacketData) error { racialGroups := make([]RacialTraitGroup, 0) for groupID, traits := range playerState.TraitLists.RaceTraits { if len(traits) == 0 { continue } groupName := TraitGroupNames[groupID] if groupName == "" { groupName = "Unknown" } group := RacialTraitGroup{ GroupName: groupName, Traits: make([]TraitInfo, 0), } for _, trait := range traits { traitInfo, err := pb.buildTraitInfo(trait, playerState) if err != nil { log.Printf("Warning: Failed to build racial trait info for spell %d: %v", trait.SpellID, err) continue } group.Traits = append(group.Traits, *traitInfo) } racialGroups = append(racialGroups, group) } packetData.RacialTraits = racialGroups return nil } // buildInnateAbilities builds the innate abilities section of the packet. func (pb *DefaultTraitPacketBuilder) buildInnateAbilities(playerState *PlayerTraitState, packetData *TraitPacketData) error { innateAbilities := make([]TraitInfo, 0) for _, traits := range playerState.TraitLists.InnateRaceTraits { for _, trait := range traits { traitInfo, err := pb.buildTraitInfo(trait, playerState) if err != nil { log.Printf("Warning: Failed to build innate ability info for spell %d: %v", trait.SpellID, err) continue } innateAbilities = append(innateAbilities, *traitInfo) } } packetData.InnateAbilities = innateAbilities return nil } // buildFocusEffects builds the focus effects section of the packet. func (pb *DefaultTraitPacketBuilder) buildFocusEffects(playerState *PlayerTraitState, packetData *TraitPacketData) error { focusEffects := make([]TraitInfo, 0) for _, traits := range playerState.TraitLists.FocusEffects { for _, trait := range traits { traitInfo, err := pb.buildTraitInfo(trait, playerState) if err != nil { log.Printf("Warning: Failed to build focus effect info for spell %d: %v", trait.SpellID, err) continue } focusEffects = append(focusEffects, *traitInfo) } } packetData.FocusEffects = focusEffects return nil } // buildTraitInfo creates a TraitInfo structure for a specific trait. func (pb *DefaultTraitPacketBuilder) buildTraitInfo(trait *TraitData, playerState *PlayerTraitState) (*TraitInfo, error) { if trait == nil { return nil, fmt.Errorf("trait is nil") } // Get spell information spell, err := pb.spellManager.GetSpell(trait.SpellID, trait.Tier) if err != nil { return nil, fmt.Errorf("failed to get spell %d tier %d: %w", trait.SpellID, trait.Tier, err) } traitInfo := &TraitInfo{ SpellID: trait.SpellID, Name: spell.GetName(), Icon: uint16(spell.GetIcon()), Icon2: uint16(spell.GetIconBackdrop()), Selected: playerState.HasTrait(trait.SpellID), Unknown1: DefaultUnknownField1, Unknown2: DefaultUnknownField2, } return traitInfo, nil } // calculateSelectionAvailability calculates how many trait selections are available. func (pb *DefaultTraitPacketBuilder) calculateSelectionAvailability(playerState *PlayerTraitState, packetData *TraitPacketData) { // Calculate racial trait selections racialSelectionsUsed := int8(0) for _, group := range packetData.RacialTraits { for _, trait := range group.Traits { if trait.Selected { racialSelectionsUsed++ } } } racialSelectionsAvailable := playerState.Level / 10 // Every 10 levels if racialSelectionsUsed < int8(racialSelectionsAvailable) { packetData.RacialSelectionsAvailable = int8(racialSelectionsAvailable) - racialSelectionsUsed } else { packetData.RacialSelectionsAvailable = 0 } // Calculate focus effect selections focusSelectionsUsed := int8(0) for _, trait := range packetData.FocusEffects { if trait.Selected { focusSelectionsUsed++ } } focusSelectionsAvailable := playerState.Level / 9 // Every 9 levels if focusSelectionsUsed < int8(focusSelectionsAvailable) { packetData.FocusSelectionsAvailable = int8(focusSelectionsAvailable) - focusSelectionsUsed } else { packetData.FocusSelectionsAvailable = 0 } } // BuildTraitRewardPacket builds a trait reward selection packet. func (pb *DefaultTraitPacketBuilder) BuildTraitRewardPacket(traits []*TraitData, packetType int8) (*TraitRewardPacket, error) { if len(traits) == 0 { return nil, fmt.Errorf("no traits provided") } packet := &TraitRewardPacket{ PacketType: packetType, SelectRewards: make([]TraitRewardItem, 0), ItemRewards: make([]TraitRewardItem, 0), } for _, trait := range traits { // Build reward item for trait selection rewardItem := TraitRewardItem{ SpellID: trait.SpellID, } // Get spell information for display spell, err := pb.spellManager.GetSpell(trait.SpellID, trait.Tier) if err != nil { log.Printf("Warning: Failed to get spell %d for trait reward: %v", trait.SpellID, err) rewardItem.Name = fmt.Sprintf("Unknown Trait %d", trait.SpellID) rewardItem.Icon = 0 } else { rewardItem.Name = spell.GetName() rewardItem.Icon = uint16(spell.GetIcon()) } packet.SelectRewards = append(packet.SelectRewards, rewardItem) // Add associated item reward if trait has an item if trait.ItemID > 0 { itemReward := TraitRewardItem{ SpellID: trait.SpellID, ItemID: trait.ItemID, } item, err := pb.itemManager.GetItem(trait.ItemID) if err != nil { log.Printf("Warning: Failed to get item %d for trait reward: %v", trait.ItemID, err) itemReward.Name = fmt.Sprintf("Unknown Item %d", trait.ItemID) itemReward.Icon = 0 } else { itemReward.Name = item.GetName() itemReward.Icon = uint16(item.GetIcon()) } packet.ItemRewards = append(packet.ItemRewards, itemReward) } } return packet, nil } // TraitPacketHelper provides utility functions for trait packet building. type TraitPacketHelper struct{} // NewTraitPacketHelper creates a new trait packet helper. func NewTraitPacketHelper() *TraitPacketHelper { return &TraitPacketHelper{} } // FormatTraitFieldName creates properly formatted field names for trait packets. // This matches the C++ string building logic using sprintf and strcat. func (ph *TraitPacketHelper) FormatTraitFieldName(baseField string, index int, suffix string) string { return baseField + strconv.Itoa(index) + suffix } // GetPacketTypeForTrait determines the appropriate packet type for a trait. func (ph *TraitPacketHelper) GetPacketTypeForTrait(trait *TraitData, playerClass, playerRace int8) int8 { // Character Traits if trait.ClassReq == UniversalClassReq && trait.RaceReq == UniversalRaceReq && trait.IsTrait { return PacketTypeCharacterTrait } // Class Training if trait.ClassReq == playerClass && trait.IsTraining { return PacketTypeSpecializedTraining } // Racial Abilities (both innate and non-innate) if trait.RaceReq == playerRace && (!trait.IsTraining || trait.IsInnate) { return PacketTypeRacialTradition } // Default to enemy mastery return PacketTypeEnemyMastery } // ValidateTraitPacketData validates trait packet data before sending. func (ph *TraitPacketHelper) ValidateTraitPacketData(packetData *TraitPacketData) error { if packetData == nil { return fmt.Errorf("packet data is nil") } // Validate character traits for i, levelData := range packetData.CharacterTraits { if len(levelData.Traits) > MaxTraitsPerLine { return fmt.Errorf("character trait level %d has too many traits: %d", i, len(levelData.Traits)) } } // Validate class training for i, levelData := range packetData.ClassTraining { if len(levelData.Traits) > MaxTraitsPerLine { return fmt.Errorf("class training level %d has too many traits: %d", i, len(levelData.Traits)) } } // Validate racial traits for i, group := range packetData.RacialTraits { if group.GroupName == "" { return fmt.Errorf("racial trait group %d has empty name", i) } } return nil } // CalculateAvailableSelections calculates available selections for different trait types. func (ph *TraitPacketHelper) CalculateAvailableSelections(playerLevel int16, usedSelections int8, intervalLevel int32) int8 { if intervalLevel <= 0 { return 0 } availableSelections := playerLevel / int16(intervalLevel) if usedSelections < int8(availableSelections) { return int8(availableSelections) - usedSelections } return 0 } // GetClassicLevelRequirement gets the level requirement from classic trait tables. func (ph *TraitPacketHelper) GetClassicLevelRequirement(levelLimits []int16, selectionIndex int) int16 { if selectionIndex >= 0 && selectionIndex < len(levelLimits) { return levelLimits[selectionIndex] } return 0 } // BuildEmptyTraitSlot creates an empty trait slot for packet filling. func (ph *TraitPacketHelper) BuildEmptyTraitSlot() TraitInfo { return TraitInfo{ SpellID: EmptyTraitID, Name: "", Icon: EmptyTraitIcon, Icon2: EmptyTraitIcon, Selected: false, Unknown1: EmptyTraitUnknown, Unknown2: EmptyTraitUnknown, } } // CountSelectedTraits counts how many traits are selected in a trait list. func (ph *TraitPacketHelper) CountSelectedTraits(traits []TraitInfo) int8 { count := int8(0) for _, trait := range traits { if trait.Selected { count++ } } return count } // SortTraitsByLevel sorts traits by their level requirement. func (ph *TraitPacketHelper) SortTraitsByLevel(traits []*TraitData) []*TraitData { // Simple bubble sort for trait level ordering sorted := make([]*TraitData, len(traits)) copy(sorted, traits) for i := 0; i < len(sorted)-1; i++ { for j := 0; j < len(sorted)-i-1; j++ { if sorted[j].Level > sorted[j+1].Level { sorted[j], sorted[j+1] = sorted[j+1], sorted[j] } } } return sorted } // GetTraitDisplayName gets an appropriate display name for a trait type. func (ph *TraitPacketHelper) GetTraitDisplayName(traitType int8) string { switch traitType { case PacketTypeEnemyMastery: return "Enemy Mastery" case PacketTypeSpecializedTraining: return "Specialized Training" case PacketTypeCharacterTrait: return "Character Trait" case PacketTypeRacialTradition: return "Racial Tradition" default: return "Unknown Trait Type" } }