eq2go/internal/traits/packets.go

539 lines
16 KiB
Go

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"
}
}