582 lines
18 KiB
Go
582 lines
18 KiB
Go
package traits
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// SpellManager defines the interface for spell-related operations needed by traits.
|
|
type SpellManager interface {
|
|
// GetSpell retrieves spell information by ID and tier
|
|
GetSpell(spellID uint32, tier int8) (Spell, error)
|
|
|
|
// GetPlayerSpells gets all spells known by a player
|
|
GetPlayerSpells(playerID uint32) ([]uint32, error)
|
|
|
|
// PlayerHasSpell checks if a player has a specific spell
|
|
PlayerHasSpell(playerID uint32, spellID uint32, tier int8) (bool, error)
|
|
|
|
// GetSpellsBySkill gets spells associated with a skill
|
|
GetSpellsBySkill(playerID uint32, skillID uint32) ([]uint32, error)
|
|
}
|
|
|
|
// ItemManager defines the interface for item-related operations needed by traits.
|
|
type ItemManager interface {
|
|
// GetItem retrieves item information by ID
|
|
GetItem(itemID uint32) (Item, error)
|
|
|
|
// GetPlayerItems gets items from a player's inventory
|
|
GetPlayerItems(playerID uint32) ([]Item, error)
|
|
|
|
// GiveItemToPlayer adds an item to a player's inventory
|
|
GiveItemToPlayer(playerID uint32, itemID uint32, quantity int16) error
|
|
}
|
|
|
|
// PlayerManager defines the interface for player-related operations needed by traits.
|
|
type PlayerManager interface {
|
|
// GetPlayer retrieves player information by ID
|
|
GetPlayer(playerID uint32) (Player, error)
|
|
|
|
// GetPlayerLevel gets a player's current level
|
|
GetPlayerLevel(playerID uint32) (int16, error)
|
|
|
|
// GetPlayerClass gets a player's adventure class
|
|
GetPlayerClass(playerID uint32) (int8, error)
|
|
|
|
// GetPlayerRace gets a player's race
|
|
GetPlayerRace(playerID uint32) (int8, error)
|
|
|
|
// SendMessageToPlayer sends a message to a player
|
|
SendMessageToPlayer(playerID uint32, channel int8, message string) error
|
|
}
|
|
|
|
// PacketManager defines the interface for packet-related operations.
|
|
type PacketManager interface {
|
|
// SendPacketToPlayer sends a packet to a specific player
|
|
SendPacketToPlayer(playerID uint32, packetData []byte) error
|
|
|
|
// QueuePacketForPlayer queues a packet for delayed sending
|
|
QueuePacketForPlayer(playerID uint32, packetData []byte) error
|
|
|
|
// GetClientVersion gets the client version for a player
|
|
GetClientVersion(playerID uint32) (int16, error)
|
|
}
|
|
|
|
// RuleManager defines the interface for rules/configuration access.
|
|
type RuleManager interface {
|
|
// GetBool retrieves a boolean rule value
|
|
GetBool(category, rule string) bool
|
|
|
|
// GetInt32 retrieves an int32 rule value
|
|
GetInt32(category, rule string) int32
|
|
|
|
// GetFloat retrieves a float rule value
|
|
GetFloat(category, rule string) float32
|
|
}
|
|
|
|
// DatabaseService defines the interface for trait persistence operations.
|
|
type DatabaseService interface {
|
|
// LoadTraits loads all traits from the database
|
|
LoadTraits(masterList *MasterTraitList) error
|
|
|
|
// SaveTrait saves a trait to the database
|
|
SaveTrait(trait *TraitData) error
|
|
|
|
// DeleteTrait removes a trait from the database
|
|
DeleteTrait(spellID uint32) error
|
|
|
|
// LoadPlayerTraits loads a player's selected traits
|
|
LoadPlayerTraits(playerID uint32) (map[uint32]bool, error)
|
|
|
|
// SavePlayerTraits saves a player's selected traits
|
|
SavePlayerTraits(playerID uint32, selectedTraits map[uint32]bool) error
|
|
}
|
|
|
|
// Data structures used by the interfaces
|
|
|
|
// Spell represents a spell in the game.
|
|
type Spell interface {
|
|
GetID() uint32
|
|
GetName() string
|
|
GetIcon() uint32
|
|
GetIconBackdrop() uint32
|
|
GetTier() int8
|
|
}
|
|
|
|
// Item represents an item in the game.
|
|
type Item interface {
|
|
GetID() uint32
|
|
GetName() string
|
|
GetIcon() uint32
|
|
GetCount() int16
|
|
}
|
|
|
|
// Player represents a player in the game.
|
|
type Player interface {
|
|
GetID() uint32
|
|
GetName() string
|
|
GetLevel() int16
|
|
GetAdventureClass() int8
|
|
GetRace() int8
|
|
}
|
|
|
|
// TraitSystemAdapter provides a high-level interface to the complete trait system.
|
|
type TraitSystemAdapter struct {
|
|
masterList *MasterTraitList
|
|
traitManager *TraitManager
|
|
packetBuilder TraitPacketBuilder
|
|
spellManager SpellManager
|
|
itemManager ItemManager
|
|
playerManager PlayerManager
|
|
packetManager PacketManager
|
|
ruleManager RuleManager
|
|
databaseService DatabaseService
|
|
config *TraitSystemConfig
|
|
}
|
|
|
|
// NewTraitSystemAdapter creates a new trait system adapter with all dependencies.
|
|
func NewTraitSystemAdapter(
|
|
masterList *MasterTraitList,
|
|
traitManager *TraitManager,
|
|
packetBuilder TraitPacketBuilder,
|
|
spellManager SpellManager,
|
|
itemManager ItemManager,
|
|
playerManager PlayerManager,
|
|
packetManager PacketManager,
|
|
ruleManager RuleManager,
|
|
databaseService DatabaseService,
|
|
) *TraitSystemAdapter {
|
|
config := &TraitSystemConfig{
|
|
TieringSelection: ruleManager.GetBool("Player", RuleTraitTieringSelection),
|
|
UseClassicLevelTable: ruleManager.GetBool("Player", RuleClassicTraitLevelTable),
|
|
FocusSelectLevel: ruleManager.GetInt32("Player", RuleTraitFocusSelectLevel),
|
|
TrainingSelectLevel: ruleManager.GetInt32("Player", RuleTraitTrainingSelectLevel),
|
|
RaceSelectLevel: ruleManager.GetInt32("Player", RuleTraitRaceSelectLevel),
|
|
CharacterSelectLevel: ruleManager.GetInt32("Player", RuleTraitCharacterSelectLevel),
|
|
}
|
|
|
|
return &TraitSystemAdapter{
|
|
masterList: masterList,
|
|
traitManager: traitManager,
|
|
packetBuilder: packetBuilder,
|
|
spellManager: spellManager,
|
|
itemManager: itemManager,
|
|
playerManager: playerManager,
|
|
packetManager: packetManager,
|
|
ruleManager: ruleManager,
|
|
databaseService: databaseService,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Initialize sets up the trait system (loads traits from database, etc.).
|
|
func (tsa *TraitSystemAdapter) Initialize() error {
|
|
// Load traits from database
|
|
err := tsa.databaseService.LoadTraits(tsa.masterList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTraitListPacket builds and returns the trait list packet for a player.
|
|
func (tsa *TraitSystemAdapter) GetTraitListPacket(playerID uint32) ([]byte, error) {
|
|
// Get player information
|
|
player, err := tsa.playerManager.GetPlayer(playerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get or create player trait state
|
|
playerState := tsa.traitManager.GetPlayerState(
|
|
playerID,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
|
|
// Load player's selected traits if needed
|
|
if len(playerState.SelectedTraits) == 0 {
|
|
selectedTraits, err := tsa.databaseService.LoadPlayerTraits(playerID)
|
|
if err == nil {
|
|
playerState.SelectedTraits = selectedTraits
|
|
}
|
|
}
|
|
|
|
// Generate trait lists
|
|
if !tsa.masterList.GenerateTraitLists(playerState, playerState.Level, UnassignedGroupID) {
|
|
return nil, fmt.Errorf("failed to generate trait lists for player %d", playerID)
|
|
}
|
|
|
|
// Get client version
|
|
clientVersion, err := tsa.packetManager.GetClientVersion(playerID)
|
|
if err != nil {
|
|
clientVersion = 1200 // Default to a modern version
|
|
}
|
|
|
|
// Build packet data
|
|
packetData, err := tsa.packetBuilder.BuildTraitListPacket(playerState, clientVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to actual packet bytes (this would be implemented by the packet system)
|
|
// For now, return a placeholder
|
|
return tsa.serializeTraitPacket(packetData, clientVersion)
|
|
}
|
|
|
|
// ChooseNextTrait handles automatic trait selection for level-up rewards.
|
|
func (tsa *TraitSystemAdapter) ChooseNextTrait(playerID uint32) error {
|
|
// Get player information
|
|
player, err := tsa.playerManager.GetPlayer(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get player trait state
|
|
playerState := tsa.traitManager.GetPlayerState(
|
|
playerID,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
|
|
// Load player's selected traits
|
|
selectedTraits, err := tsa.databaseService.LoadPlayerTraits(playerID)
|
|
if err == nil {
|
|
playerState.SelectedTraits = selectedTraits
|
|
}
|
|
|
|
// Get available trait choices
|
|
availableTraits, err := tsa.traitManager.GetAvailableTraits(
|
|
playerID,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(availableTraits) == 0 {
|
|
return nil // No traits available for selection
|
|
}
|
|
|
|
// Determine packet type based on trait types
|
|
packetType := tsa.determinePacketType(availableTraits, player.GetAdventureClass(), player.GetRace())
|
|
|
|
// Build trait reward packet
|
|
rewardPacket, err := tsa.packetBuilder.BuildTraitRewardPacket(availableTraits, packetType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send reward selection packet to player
|
|
return tsa.sendTraitRewardPacket(playerID, rewardPacket)
|
|
}
|
|
|
|
// SelectTraits processes a player's trait selections.
|
|
func (tsa *TraitSystemAdapter) SelectTraits(playerID uint32, selectedSpells []uint32) error {
|
|
// Get player information
|
|
player, err := tsa.playerManager.GetPlayer(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create selection request
|
|
request := &TraitSelectionRequest{
|
|
PlayerID: playerID,
|
|
TraitSpells: selectedSpells,
|
|
PacketType: 0, // Will be determined based on traits
|
|
}
|
|
|
|
// Process the selection
|
|
err = tsa.traitManager.SelectTraits(
|
|
request,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get updated player state
|
|
playerState := tsa.traitManager.GetPlayerState(
|
|
playerID,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
|
|
// Save player's trait selections
|
|
err = tsa.databaseService.SavePlayerTraits(playerID, playerState.SelectedTraits)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send confirmation message
|
|
message := fmt.Sprintf("You have learned %d new trait(s).", len(selectedSpells))
|
|
return tsa.playerManager.SendMessageToPlayer(playerID, 4, message) // CHANNEL_NARRATIVE
|
|
}
|
|
|
|
// IsPlayerAllowedTrait checks if a player is allowed to select a specific trait.
|
|
func (tsa *TraitSystemAdapter) IsPlayerAllowedTrait(playerID uint32, spellID uint32) (bool, error) {
|
|
// Get trait
|
|
trait := tsa.masterList.GetTrait(spellID)
|
|
if trait == nil {
|
|
return false, ErrTraitNotFound
|
|
}
|
|
|
|
// Get player information
|
|
player, err := tsa.playerManager.GetPlayer(playerID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Get player trait state
|
|
playerState := tsa.traitManager.GetPlayerState(
|
|
playerID,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
|
|
// Check if allowed
|
|
return tsa.masterList.IsPlayerAllowedTrait(playerState, trait, tsa.config), nil
|
|
}
|
|
|
|
// GetPlayerTraitStats returns statistics about a player's trait selections.
|
|
func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]any, error) {
|
|
// Get player information
|
|
player, err := tsa.playerManager.GetPlayer(playerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get player trait state
|
|
playerState := tsa.traitManager.GetPlayerState(
|
|
playerID,
|
|
player.GetLevel(),
|
|
player.GetAdventureClass(),
|
|
player.GetRace(),
|
|
)
|
|
|
|
// Load selected traits if needed
|
|
if len(playerState.SelectedTraits) == 0 {
|
|
selectedTraits, err := tsa.databaseService.LoadPlayerTraits(playerID)
|
|
if err == nil {
|
|
playerState.SelectedTraits = selectedTraits
|
|
}
|
|
}
|
|
|
|
// Generate trait lists for counting
|
|
tsa.masterList.GenerateTraitLists(playerState, playerState.Level, UnassignedGroupID)
|
|
|
|
// Count trait selections by type
|
|
characterTraits := tsa.masterList.getSpellCount(playerState, playerState.TraitLists.SortedTraitList, true)
|
|
classTraining := tsa.masterList.getSpellCount(playerState, playerState.TraitLists.ClassTraining, false)
|
|
racialTraits := tsa.masterList.getSpellCount(playerState, playerState.TraitLists.RaceTraits, false) +
|
|
tsa.masterList.getSpellCount(playerState, playerState.TraitLists.InnateRaceTraits, false)
|
|
focusEffects := tsa.masterList.getSpellCount(playerState, playerState.TraitLists.FocusEffects, false)
|
|
|
|
return map[string]any{
|
|
"player_id": playerID,
|
|
"level": playerState.Level,
|
|
"character_traits": characterTraits,
|
|
"class_training": classTraining,
|
|
"racial_traits": racialTraits,
|
|
"focus_effects": focusEffects,
|
|
"total_selected": len(playerState.SelectedTraits),
|
|
"last_update": time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// GetSystemStats returns comprehensive statistics about the trait system.
|
|
func (tsa *TraitSystemAdapter) GetSystemStats() map[string]any {
|
|
masterStats := tsa.masterList.GetStats()
|
|
managerStats := tsa.traitManager.GetManagerStats()
|
|
|
|
return map[string]any{
|
|
"total_traits": masterStats.TotalTraits,
|
|
"traits_by_type": masterStats.TraitsByType,
|
|
"traits_by_group": masterStats.TraitsByGroup,
|
|
"traits_by_level": masterStats.TraitsByLevel,
|
|
"players_with_traits": managerStats.PlayersWithTraits,
|
|
"config": tsa.config,
|
|
"last_update": time.Now(),
|
|
}
|
|
}
|
|
|
|
// RefreshConfiguration reloads configuration from rules.
|
|
func (tsa *TraitSystemAdapter) RefreshConfiguration() {
|
|
tsa.config.TieringSelection = tsa.ruleManager.GetBool("Player", RuleTraitTieringSelection)
|
|
tsa.config.UseClassicLevelTable = tsa.ruleManager.GetBool("Player", RuleClassicTraitLevelTable)
|
|
tsa.config.FocusSelectLevel = tsa.ruleManager.GetInt32("Player", RuleTraitFocusSelectLevel)
|
|
tsa.config.TrainingSelectLevel = tsa.ruleManager.GetInt32("Player", RuleTraitTrainingSelectLevel)
|
|
tsa.config.RaceSelectLevel = tsa.ruleManager.GetInt32("Player", RuleTraitRaceSelectLevel)
|
|
tsa.config.CharacterSelectLevel = tsa.ruleManager.GetInt32("Player", RuleTraitCharacterSelectLevel)
|
|
|
|
// Update trait manager config
|
|
tsa.traitManager.config = tsa.config
|
|
}
|
|
|
|
// ClearPlayerData removes cached data for a player (e.g., when they log out).
|
|
func (tsa *TraitSystemAdapter) ClearPlayerData(playerID uint32) {
|
|
tsa.traitManager.ClearPlayerState(playerID)
|
|
}
|
|
|
|
// AddTrait adds a new trait to the master list.
|
|
func (tsa *TraitSystemAdapter) AddTrait(trait *TraitData) error {
|
|
err := tsa.masterList.AddTrait(trait)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save to database
|
|
return tsa.databaseService.SaveTrait(trait)
|
|
}
|
|
|
|
// RemoveTrait removes a trait from the master list.
|
|
func (tsa *TraitSystemAdapter) RemoveTrait(spellID uint32) error {
|
|
// Remove from database first
|
|
err := tsa.databaseService.DeleteTrait(spellID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reload traits from database to update master list
|
|
tsa.masterList.DestroyTraits()
|
|
return tsa.databaseService.LoadTraits(tsa.masterList)
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
// determinePacketType determines the appropriate packet type for a list of traits.
|
|
func (tsa *TraitSystemAdapter) determinePacketType(traits []*TraitData, playerClass, playerRace int8) int8 {
|
|
if len(traits) == 0 {
|
|
return PacketTypeCharacterTrait
|
|
}
|
|
|
|
// Use the first trait to determine packet type
|
|
trait := traits[0]
|
|
|
|
if trait.IsUniversalTrait() {
|
|
return PacketTypeCharacterTrait
|
|
}
|
|
|
|
if trait.ClassReq == playerClass && trait.IsTraining {
|
|
return PacketTypeSpecializedTraining
|
|
}
|
|
|
|
if trait.RaceReq == playerRace {
|
|
return PacketTypeRacialTradition
|
|
}
|
|
|
|
return PacketTypeEnemyMastery
|
|
}
|
|
|
|
// serializeTraitPacket converts trait packet data to byte array.
|
|
func (tsa *TraitSystemAdapter) serializeTraitPacket(packetData *TraitPacketData, clientVersion int16) ([]byte, error) {
|
|
// This would be implemented by the actual packet system
|
|
// For now, return a placeholder indicating successful packet creation
|
|
return []byte("TRAIT_PACKET_PLACEHOLDER"), nil
|
|
}
|
|
|
|
// sendTraitRewardPacket sends a trait reward packet to a player.
|
|
func (tsa *TraitSystemAdapter) sendTraitRewardPacket(playerID uint32, rewardPacket *TraitRewardPacket) error {
|
|
// This would serialize the reward packet and send it via the packet manager
|
|
// For now, return success
|
|
return nil
|
|
}
|
|
|
|
// TraitEventHandler handles trait-related events.
|
|
type TraitEventHandler struct {
|
|
adapter *TraitSystemAdapter
|
|
}
|
|
|
|
// NewTraitEventHandler creates a new trait event handler.
|
|
func NewTraitEventHandler(adapter *TraitSystemAdapter) *TraitEventHandler {
|
|
return &TraitEventHandler{
|
|
adapter: adapter,
|
|
}
|
|
}
|
|
|
|
// OnPlayerLevelUp handles player level up events to check for new trait availability.
|
|
func (teh *TraitEventHandler) OnPlayerLevelUp(playerID uint32, newLevel int16) error {
|
|
// Check if player should get trait selection opportunity
|
|
return teh.adapter.ChooseNextTrait(playerID)
|
|
}
|
|
|
|
// OnPlayerLogin handles player login events to refresh trait data.
|
|
func (teh *TraitEventHandler) OnPlayerLogin(playerID uint32) error {
|
|
// Refresh player's trait packet
|
|
_, err := teh.adapter.GetTraitListPacket(playerID)
|
|
return err
|
|
}
|
|
|
|
// OnPlayerLogout handles player logout events to clean up cached data.
|
|
func (teh *TraitEventHandler) OnPlayerLogout(playerID uint32) {
|
|
teh.adapter.ClearPlayerData(playerID)
|
|
}
|
|
|
|
// MockImplementations for testing
|
|
|
|
// MockSpellManager is a mock implementation of SpellManager for testing.
|
|
type MockSpellManager struct {
|
|
spells map[uint32]MockSpell
|
|
}
|
|
|
|
// MockSpell is a mock implementation of Spell for testing.
|
|
type MockSpell struct {
|
|
id uint32
|
|
name string
|
|
icon uint32
|
|
iconBackdrop uint32
|
|
tier int8
|
|
}
|
|
|
|
func (ms MockSpell) GetID() uint32 { return ms.id }
|
|
func (ms MockSpell) GetName() string { return ms.name }
|
|
func (ms MockSpell) GetIcon() uint32 { return ms.icon }
|
|
func (ms MockSpell) GetIconBackdrop() uint32 { return ms.iconBackdrop }
|
|
func (ms MockSpell) GetTier() int8 { return ms.tier }
|
|
|
|
func (msm *MockSpellManager) GetSpell(spellID uint32, tier int8) (Spell, error) {
|
|
if spell, exists := msm.spells[spellID]; exists {
|
|
return spell, nil
|
|
}
|
|
return nil, fmt.Errorf("spell not found: %d", spellID)
|
|
}
|
|
|
|
func (msm *MockSpellManager) GetPlayerSpells(playerID uint32) ([]uint32, error) {
|
|
return []uint32{}, nil
|
|
}
|
|
|
|
func (msm *MockSpellManager) PlayerHasSpell(playerID uint32, spellID uint32, tier int8) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (msm *MockSpellManager) GetSpellsBySkill(playerID uint32, skillID uint32) ([]uint32, error) {
|
|
return []uint32{}, nil
|
|
}
|
|
|
|
// NewMockSpellManager creates a new mock spell manager.
|
|
func NewMockSpellManager() *MockSpellManager {
|
|
return &MockSpellManager{
|
|
spells: make(map[uint32]MockSpell),
|
|
}
|
|
}
|
|
|
|
// AddMockSpell adds a mock spell for testing.
|
|
func (msm *MockSpellManager) AddMockSpell(id uint32, name string, icon uint32, tier int8) {
|
|
msm.spells[id] = MockSpell{
|
|
id: id,
|
|
name: name,
|
|
icon: icon,
|
|
iconBackdrop: icon + 1000,
|
|
tier: tier,
|
|
}
|
|
}
|