539 lines
16 KiB
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"
|
|
}
|
|
}
|