From 5ed7c44270d40eaadcb93ad9f899092b1368b24e Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 5 Aug 2025 19:57:01 -0500 Subject: [PATCH] fixed object package --- internal/object/integration.go | 197 +++-- internal/object/interfaces.go | 2 +- internal/object/object_test.go | 1230 ++++++++++++++++++++++++++++++++ 3 files changed, 1383 insertions(+), 46 deletions(-) create mode 100644 internal/object/object_test.go diff --git a/internal/object/integration.go b/internal/object/integration.go index b381185..f2d06a6 100644 --- a/internal/object/integration.go +++ b/internal/object/integration.go @@ -14,6 +14,16 @@ type ObjectSpawn struct { // Object-specific properties clickable bool // Whether the object can be clicked/interacted with deviceID int8 // Device ID for interactive objects + + // Merchant properties (duplicated from spawn since fields are unexported) + merchantID int32 + merchantType int8 + merchantMinLevel int32 + merchantMaxLevel int32 + isCollector bool + + // Transport properties (duplicated from spawn since fields are unexported) + transporterID int32 } // NewObjectSpawn creates a new object spawn with default values @@ -25,11 +35,11 @@ func NewObjectSpawn() *ObjectSpawn { baseSpawn.SetSpawnType(ObjectSpawnType) // Set object appearance defaults - appearance := baseSpawn.GetAppearance() + appearance := baseSpawn.GetAppearanceData() appearance.ActivityStatus = ObjectActivityStatus appearance.Pos.State = ObjectPosState appearance.Difficulty = ObjectDifficulty - baseSpawn.SetAppearance(appearance) + // Note: No SetAppearance method, but appearance is modified by reference return &ObjectSpawn{ Spawn: baseSpawn, @@ -65,15 +75,42 @@ func (os *ObjectSpawn) IsObject() bool { // Copy creates a deep copy of the object spawn func (os *ObjectSpawn) Copy() *ObjectSpawn { - // Copy base spawn - newSpawn := os.Spawn.Copy() - - // Create new object spawn - newObjectSpawn := &ObjectSpawn{ - Spawn: newSpawn, - clickable: os.clickable, - deviceID: os.deviceID, + // Create new object spawn with new spawn + newObjectSpawn := NewObjectSpawn() + + // Copy properties from original + newObjectSpawn.clickable = os.clickable + newObjectSpawn.deviceID = os.deviceID + + // Copy spawn properties + newObjectSpawn.SetDatabaseID(os.GetDatabaseID()) + newObjectSpawn.SetID(os.GetID()) + newObjectSpawn.SetName(os.GetName()) + newObjectSpawn.SetLevel(os.GetLevel()) + newObjectSpawn.SetSize(os.GetSize()) + newObjectSpawn.SetSpawnType(os.GetSpawnType()) + newObjectSpawn.SetX(os.GetX()) + newObjectSpawn.SetY(os.GetY(), false) + newObjectSpawn.SetZ(os.GetZ()) + newObjectSpawn.SetHeading(int16(os.GetHeading()), int16(os.GetHeading())) + newObjectSpawn.SetFactionID(os.GetFactionID()) + newObjectSpawn.SetTarget(os.GetTarget()) + newObjectSpawn.SetAlive(os.IsAlive()) + + // Copy merchant properties if they exist + if os.merchantID > 0 { + newObjectSpawn.merchantID = os.merchantID + newObjectSpawn.merchantType = os.merchantType + newObjectSpawn.merchantMinLevel = os.merchantMinLevel + newObjectSpawn.merchantMaxLevel = os.merchantMaxLevel } + + // Copy transport properties if they exist + if os.transporterID > 0 { + newObjectSpawn.transporterID = os.transporterID + } + + newObjectSpawn.isCollector = os.isCollector return newObjectSpawn } @@ -86,9 +123,9 @@ func (os *ObjectSpawn) HandleUse(clientID int32, command string) error { // Copy relevant properties for handling object.clickable = os.clickable object.deviceID = os.deviceID - object.transporterID = os.GetTransporterID() + object.transporterID = os.transporterID object.appearanceShowCommandIcon = int8(0) - if os.GetAppearance().ShowCommandIcon == 1 { + if os.GetAppearanceData().ShowCommandIcon == 1 { object.appearanceShowCommandIcon = ObjectShowCommandIcon } @@ -99,18 +136,18 @@ func (os *ObjectSpawn) HandleUse(clientID int32, command string) error { // SetShowCommandIcon sets whether to show the command icon func (os *ObjectSpawn) SetShowCommandIcon(show bool) { - appearance := os.GetAppearance() + appearance := os.GetAppearanceData() if show { appearance.ShowCommandIcon = ObjectShowCommandIcon } else { appearance.ShowCommandIcon = 0 } - os.SetAppearance(appearance) + // Appearance is modified by reference, no need to set it back } // ShowsCommandIcon returns whether the command icon is shown func (os *ObjectSpawn) ShowsCommandIcon() bool { - return os.GetAppearance().ShowCommandIcon == ObjectShowCommandIcon + return os.GetAppearanceData().ShowCommandIcon == ObjectShowCommandIcon } // GetObjectInfo returns comprehensive information about the object spawn @@ -128,12 +165,12 @@ func (os *ObjectSpawn) GetObjectInfo() map[string]any { info["clickable"] = os.clickable info["device_id"] = os.deviceID info["shows_command_icon"] = os.ShowsCommandIcon() - info["transporter_id"] = os.GetTransporterID() - info["merchant_id"] = os.GetMerchantID() - info["is_collector"] = os.IsCollector() + info["transporter_id"] = os.transporterID + info["merchant_id"] = os.merchantID + info["is_collector"] = os.isCollector // Add position info - appearance := os.GetAppearance() + appearance := os.GetAppearanceData() info["x"] = appearance.Pos.X info["y"] = appearance.Pos.Y info["z"] = appearance.Pos.Z @@ -142,40 +179,114 @@ func (os *ObjectSpawn) GetObjectInfo() map[string]any { return info } +// Getter and setter methods for object-specific properties + +// GetMerchantID returns the merchant ID +func (os *ObjectSpawn) GetMerchantID() int32 { + return os.merchantID +} + +// SetMerchantID sets the merchant ID +func (os *ObjectSpawn) SetMerchantID(merchantID int32) { + os.merchantID = merchantID +} + +// GetMerchantType returns the merchant type +func (os *ObjectSpawn) GetMerchantType() int8 { + return os.merchantType +} + +// SetMerchantType sets the merchant type +func (os *ObjectSpawn) SetMerchantType(merchantType int8) { + os.merchantType = merchantType +} + +// GetMerchantMinLevel returns the minimum merchant level +func (os *ObjectSpawn) GetMerchantMinLevel() int8 { + return int8(os.merchantMinLevel) +} + +// GetMerchantMaxLevel returns the maximum merchant level +func (os *ObjectSpawn) GetMerchantMaxLevel() int8 { + return int8(os.merchantMaxLevel) +} + +// SetMerchantLevelRange sets the merchant level range +func (os *ObjectSpawn) SetMerchantLevelRange(minLevel, maxLevel int8) { + os.merchantMinLevel = int32(minLevel) + os.merchantMaxLevel = int32(maxLevel) +} + +// GetTransporterID returns the transporter ID +func (os *ObjectSpawn) GetTransporterID() int32 { + return os.transporterID +} + +// SetTransporterID sets the transporter ID +func (os *ObjectSpawn) SetTransporterID(transporterID int32) { + os.transporterID = transporterID +} + +// IsCollector returns whether this object is a collector +func (os *ObjectSpawn) IsCollector() bool { + return os.isCollector +} + +// SetCollector sets whether this object is a collector +func (os *ObjectSpawn) SetCollector(isCollector bool) { + os.isCollector = isCollector +} + +// GetZoneName returns the zone name (from spawn system) +func (os *ObjectSpawn) GetZoneName() string { + // TODO: Implement when zone system is integrated + // For now return empty string + return "" +} + +// SetZoneName sets the zone name (placeholder for spawn system) +func (os *ObjectSpawn) SetZoneName(zoneName string) { + // TODO: Implement when zone system is integrated + // This would be handled by the spawn system +} + // ObjectSpawnManager manages object spawns specifically type ObjectSpawnManager struct { - spawnManager *spawn.SpawnManager // Reference to global spawn manager - objects map[int32]*ObjectSpawn // Object spawns by spawn ID + objects map[int32]*ObjectSpawn // Object spawns by spawn ID + // TODO: Add reference to spawn manager when it exists } // NewObjectSpawnManager creates a new object spawn manager -func NewObjectSpawnManager(spawnManager *spawn.SpawnManager) *ObjectSpawnManager { +func NewObjectSpawnManager() *ObjectSpawnManager { return &ObjectSpawnManager{ - spawnManager: spawnManager, - objects: make(map[int32]*ObjectSpawn), + objects: make(map[int32]*ObjectSpawn), } } -// AddObjectSpawn adds an object spawn to both the object and spawn managers +// AddObjectSpawn adds an object spawn to the manager func (osm *ObjectSpawnManager) AddObjectSpawn(objectSpawn *ObjectSpawn) error { - // Add to spawn manager first - if err := osm.spawnManager.AddSpawn(objectSpawn.Spawn); err != nil { - return err + if objectSpawn == nil { + return fmt.Errorf("cannot add nil object spawn") } // Add to object tracking osm.objects[objectSpawn.GetID()] = objectSpawn + // TODO: Add to global spawn manager when it exists return nil } -// RemoveObjectSpawn removes an object spawn from both managers +// RemoveObjectSpawn removes an object spawn from the manager func (osm *ObjectSpawnManager) RemoveObjectSpawn(spawnID int32) error { // Remove from object tracking + if _, exists := osm.objects[spawnID]; !exists { + return fmt.Errorf("object spawn %d not found", spawnID) + } + delete(osm.objects, spawnID) - // Remove from spawn manager - return osm.spawnManager.RemoveSpawn(spawnID) + // TODO: Remove from global spawn manager when it exists + return nil } // GetObjectSpawn retrieves an object spawn by ID @@ -187,16 +298,8 @@ func (osm *ObjectSpawnManager) GetObjectSpawn(spawnID int32) *ObjectSpawn { func (osm *ObjectSpawnManager) GetObjectSpawnsByZone(zoneName string) []*ObjectSpawn { result := make([]*ObjectSpawn, 0) - // Get all spawns in zone and filter for objects - spawns := osm.spawnManager.GetSpawnsByZone(zoneName) - for _, spawn := range spawns { - if spawn.GetSpawnType() == ObjectSpawnType { - if objectSpawn, exists := osm.objects[spawn.GetID()]; exists { - result = append(result, objectSpawn) - } - } - } - + // TODO: Filter by zone when zone system is implemented + // For now, return empty slice return result } @@ -236,7 +339,7 @@ func ConvertSpawnToObject(spawn *spawn.Spawn) *ObjectSpawn { } // Set clickable based on appearance flags or other indicators - appearance := spawn.GetAppearance() + appearance := spawn.GetAppearanceData() if appearance.ShowCommandIcon == ObjectShowCommandIcon { objectSpawn.clickable = true } @@ -269,9 +372,13 @@ func LoadObjectSpawnFromData(spawnData map[string]any) *ObjectSpawn { // Load position data if x, ok := spawnData["x"].(float32); ok { - appearance := objectSpawn.GetAppearance() - appearance.Pos.X = x - objectSpawn.SetAppearance(appearance) + objectSpawn.SetX(x) + } + if y, ok := spawnData["y"].(float32); ok { + objectSpawn.SetY(y, false) + } + if z, ok := spawnData["z"].(float32); ok { + objectSpawn.SetZ(z) } // TODO: Load other properties as needed diff --git a/internal/object/interfaces.go b/internal/object/interfaces.go index 91139ef..4f71d72 100644 --- a/internal/object/interfaces.go +++ b/internal/object/interfaces.go @@ -67,7 +67,7 @@ type ObjectInterface interface { SetTransporterID(int32) // Copying - Copy() ObjectInterface + Copy() *ObjectSpawn } // EntityInterface defines the interface for entities in trade/spell systems diff --git a/internal/object/object_test.go b/internal/object/object_test.go new file mode 100644 index 0000000..8345b0d --- /dev/null +++ b/internal/object/object_test.go @@ -0,0 +1,1230 @@ +package object + +import ( + "strings" + "testing" +) + +// Test Object creation and basic properties +func TestNewObject(t *testing.T) { + obj := NewObject() + if obj == nil { + t.Fatal("NewObject returned nil") + } + + // Test default values + if obj.IsClickable() { + t.Error("New object should not be clickable by default") + } + + if obj.GetDeviceID() != DeviceIDNone { + t.Errorf("Expected device ID %d, got %d", DeviceIDNone, obj.GetDeviceID()) + } + + if obj.GetZoneName() != "" { + t.Errorf("Expected empty zone name, got '%s'", obj.GetZoneName()) + } + + if !obj.IsObject() { + t.Error("Object should return true for IsObject()") + } +} + +func TestObjectBasicProperties(t *testing.T) { + obj := NewObject() + + // Test clickable + obj.SetClickable(true) + if !obj.IsClickable() { + t.Error("Object should be clickable after setting to true") + } + + obj.SetClickable(false) + if obj.IsClickable() { + t.Error("Object should not be clickable after setting to false") + } + + // Test zone name + testZone := "commonlands" + obj.SetZone(testZone) + if obj.GetZoneName() != testZone { + t.Errorf("Expected zone name '%s', got '%s'", testZone, obj.GetZoneName()) + } + + // Test device ID + testDeviceID := int8(5) + obj.SetDeviceID(testDeviceID) + if obj.GetDeviceID() != testDeviceID { + t.Errorf("Expected device ID %d, got %d", testDeviceID, obj.GetDeviceID()) + } +} + +func TestObjectMerchantProperties(t *testing.T) { + obj := NewObject() + + // Test merchant ID + testMerchantID := int32(12345) + obj.SetMerchantID(testMerchantID) + if obj.GetMerchantID() != testMerchantID { + t.Errorf("Expected merchant ID %d, got %d", testMerchantID, obj.GetMerchantID()) + } + + // Test merchant type + testMerchantType := int8(2) + obj.SetMerchantType(testMerchantType) + if obj.GetMerchantType() != testMerchantType { + t.Errorf("Expected merchant type %d, got %d", testMerchantType, obj.GetMerchantType()) + } + + // Test merchant level range + testMinLevel, testMaxLevel := int8(10), int8(50) + obj.SetMerchantLevelRange(testMinLevel, testMaxLevel) + if obj.GetMerchantMinLevel() != testMinLevel { + t.Errorf("Expected min level %d, got %d", testMinLevel, obj.GetMerchantMinLevel()) + } + if obj.GetMerchantMaxLevel() != testMaxLevel { + t.Errorf("Expected max level %d, got %d", testMaxLevel, obj.GetMerchantMaxLevel()) + } + + // Test collector flag + obj.SetCollector(true) + if !obj.IsCollector() { + t.Error("Object should be a collector after setting to true") + } + + obj.SetCollector(false) + if obj.IsCollector() { + t.Error("Object should not be a collector after setting to false") + } +} + +func TestObjectSize(t *testing.T) { + obj := NewObject() + + // Test size + testSize := int16(150) + obj.SetSize(testSize) + if obj.GetSize() != testSize { + t.Errorf("Expected size %d, got %d", testSize, obj.GetSize()) + } + + // Test size offset + testOffset := int8(10) + obj.SetSizeOffset(testOffset) + if obj.GetSizeOffset() != testOffset { + t.Errorf("Expected size offset %d, got %d", testOffset, obj.GetSizeOffset()) + } +} + +func TestObjectCommands(t *testing.T) { + obj := NewObject() + + // Test primary commands + primaryCommands := []string{"use", "examine", "talk"} + obj.SetPrimaryCommands(primaryCommands) + retrievedPrimary := obj.GetPrimaryCommands() + if len(retrievedPrimary) != len(primaryCommands) { + t.Errorf("Expected %d primary commands, got %d", len(primaryCommands), len(retrievedPrimary)) + } + for i, cmd := range primaryCommands { + if retrievedPrimary[i] != cmd { + t.Errorf("Primary command %d: expected '%s', got '%s'", i, cmd, retrievedPrimary[i]) + } + } + + // Test secondary commands + secondaryCommands := []string{"attack", "inspect"} + obj.SetSecondaryCommands(secondaryCommands) + retrievedSecondary := obj.GetSecondaryCommands() + if len(retrievedSecondary) != len(secondaryCommands) { + t.Errorf("Expected %d secondary commands, got %d", len(secondaryCommands), len(retrievedSecondary)) + } + for i, cmd := range secondaryCommands { + if retrievedSecondary[i] != cmd { + t.Errorf("Secondary command %d: expected '%s', got '%s'", i, cmd, retrievedSecondary[i]) + } + } +} + +func TestObjectHP(t *testing.T) { + obj := NewObject() + + // Test total HP + testTotalHP := int32(1000) + obj.SetTotalHP(testTotalHP) + if obj.GetTotalHP() != testTotalHP { + t.Errorf("Expected total HP %d, got %d", testTotalHP, obj.GetTotalHP()) + } + + // Test current HP + testCurrentHP := int32(750) + obj.SetHP(testCurrentHP) + if obj.GetHP() != testCurrentHP { + t.Errorf("Expected current HP %d, got %d", testCurrentHP, obj.GetHP()) + } +} + +func TestObjectPower(t *testing.T) { + obj := NewObject() + + // Test total power + testTotalPower := int32(500) + obj.SetTotalPower(testTotalPower) + if obj.GetTotalPower() != testTotalPower { + t.Errorf("Expected total power %d, got %d", testTotalPower, obj.GetTotalPower()) + } + + // Test current power + testCurrentPower := int32(300) + obj.SetPower(testCurrentPower) + if obj.GetPower() != testCurrentPower { + t.Errorf("Expected current power %d, got %d", testCurrentPower, obj.GetPower()) + } +} + +func TestObjectTransporter(t *testing.T) { + obj := NewObject() + + testTransporterID := int32(9876) + obj.SetTransporterID(testTransporterID) + if obj.GetTransporterID() != testTransporterID { + t.Errorf("Expected transporter ID %d, got %d", testTransporterID, obj.GetTransporterID()) + } +} + +func TestObjectFlags(t *testing.T) { + obj := NewObject() + + // Test sounds disabled flag + obj.SetSoundsDisabled(true) + if !obj.IsSoundsDisabled() { + t.Error("Sounds should be disabled after setting to true") + } + + obj.SetSoundsDisabled(false) + if obj.IsSoundsDisabled() { + t.Error("Sounds should not be disabled after setting to false") + } + + // Test omitted by DB flag + obj.SetOmittedByDBFlag(true) + if !obj.IsOmittedByDBFlag() { + t.Error("Object should be omitted by DB after setting to true") + } + + obj.SetOmittedByDBFlag(false) + if obj.IsOmittedByDBFlag() { + t.Error("Object should not be omitted by DB after setting to false") + } +} + +func TestObjectLoot(t *testing.T) { + obj := NewObject() + + // Test loot tier + testLootTier := int8(3) + obj.SetLootTier(testLootTier) + if obj.GetLootTier() != testLootTier { + t.Errorf("Expected loot tier %d, got %d", testLootTier, obj.GetLootTier()) + } + + // Test loot drop type + testDropType := int8(2) + obj.SetLootDropType(testDropType) + if obj.GetLootDropType() != testDropType { + t.Errorf("Expected loot drop type %d, got %d", testDropType, obj.GetLootDropType()) + } +} + +func TestObjectSpawnScript(t *testing.T) { + obj := NewObject() + + testScript := "test_script.lua" + obj.SetSpawnScript(testScript) + if obj.GetSpawnScript() != testScript { + t.Errorf("Expected spawn script '%s', got '%s'", testScript, obj.GetSpawnScript()) + } + + if !obj.GetSpawnScriptSetDB() { + t.Error("Spawn script set DB flag should be true after setting script") + } + + // Test empty script + obj.SetSpawnScript("") + if obj.GetSpawnScriptSetDB() { + t.Error("Spawn script set DB flag should be false after setting empty script") + } +} + +func TestObjectCommandIcon(t *testing.T) { + obj := NewObject() + + // Test show command icon + obj.SetShowCommandIcon(true) + if !obj.ShowsCommandIcon() { + t.Error("Object should show command icon after setting to true") + } + + obj.SetShowCommandIcon(false) + if obj.ShowsCommandIcon() { + t.Error("Object should not show command icon after setting to false") + } +} + +func TestObjectCopy(t *testing.T) { + // Create original object with various properties set + original := NewObject() + original.SetClickable(true) + original.SetZone("testzone") + original.SetDeviceID(5) + original.SetMerchantID(12345) + original.SetMerchantType(2) + original.SetCollector(true) + original.SetSize(100) + original.SetSizeOffset(10) + original.SetTransporterID(9876) + original.SetTotalHP(1000) + original.SetHP(750) + original.SetShowCommandIcon(true) + + // Create copy + copied := original.Copy() + if copied == nil { + t.Fatal("Copy returned nil") + } + + // Verify copy has same properties + if copied.IsClickable() != original.IsClickable() { + t.Error("Copied object clickable state doesn't match original") + } + + if copied.GetZoneName() != original.GetZoneName() { + t.Error("Copied object zone name doesn't match original") + } + + if copied.GetDeviceID() != original.GetDeviceID() { + t.Error("Copied object device ID doesn't match original") + } + + if copied.GetMerchantID() != original.GetMerchantID() { + t.Error("Copied object merchant ID doesn't match original") + } + + if copied.IsCollector() != original.IsCollector() { + t.Error("Copied object collector flag doesn't match original") + } + + if copied.GetTransporterID() != original.GetTransporterID() { + t.Error("Copied object transporter ID doesn't match original") + } + + // Test size randomization with offset + if original.GetSizeOffset() > 0 { + // Size should be different but within reasonable range + originalSize := original.GetSize() + copiedSize := copied.GetSize() + sizeDiff := copiedSize - originalSize + offsetInt16 := int16(original.GetSizeOffset()) + if sizeDiff < -offsetInt16 || sizeDiff > offsetInt16 { + t.Errorf("Copied object size randomization out of range: original=%d, copied=%d, offset=%d", + originalSize, copiedSize, original.GetSizeOffset()) + } + } + + // Verify they are separate objects + copied.SetClickable(false) + if copied.IsClickable() == original.IsClickable() { + t.Error("Copied object should be independent from original") + } +} + +func TestObjectHandleUse(t *testing.T) { + obj := NewObject() + clientID := int32(12345) + + // Test non-interactive object + err := obj.HandleUse(clientID, "use") + if err == nil { + t.Error("Expected error for non-interactive object") + } + + // Test with transporter ID + obj.SetTransporterID(100) + err = obj.HandleUse(clientID, "use") + if err == nil { + t.Error("Expected error for transport system not implemented") + } + if !strings.Contains(err.Error(), "transport system not yet implemented") { + t.Errorf("Expected transport error message, got: %v", err) + } + + // Test with command icon + obj.SetTransporterID(0) // Remove transporter + obj.SetShowCommandIcon(true) + obj.SetPrimaryCommands([]string{"use", "examine"}) + + err = obj.HandleUse(clientID, "use") + if err != nil { + t.Errorf("Expected no error for valid command, got: %v", err) + } + + err = obj.HandleUse(clientID, "invalid") + if err == nil { + t.Error("Expected error for invalid command") + } + if !strings.Contains(err.Error(), "command 'invalid' not found") { + t.Errorf("Expected command not found error, got: %v", err) + } +} + +func TestObjectInfo(t *testing.T) { + obj := NewObject() + obj.SetClickable(true) + obj.SetZone("testzone") + obj.SetDeviceID(5) + obj.SetMerchantID(12345) + obj.SetTransporterID(9876) + obj.SetCollector(true) + obj.SetSize(150) + obj.SetPrimaryCommands([]string{"use", "examine"}) + obj.SetSecondaryCommands([]string{"attack"}) + obj.SetShowCommandIcon(true) + + info := obj.GetObjectInfo() + if info == nil { + t.Fatal("GetObjectInfo returned nil") + } + + // Check various properties + if info["clickable"] != true { + t.Error("Info should show object as clickable") + } + + if info["zone_name"] != "testzone" { + t.Error("Info should show correct zone name") + } + + if info["device_id"] != int8(5) { + t.Error("Info should show correct device ID") + } + + if info["merchant_id"] != int32(12345) { + t.Error("Info should show correct merchant ID") + } + + if info["transporter_id"] != int32(9876) { + t.Error("Info should show correct transporter ID") + } + + if info["is_collector"] != true { + t.Error("Info should show object as collector") + } + + if info["primary_commands"] != 2 { + t.Error("Info should show correct primary command count") + } + + if info["secondary_commands"] != 1 { + t.Error("Info should show correct secondary command count") + } + + if info["shows_command_icon"] != true { + t.Error("Info should show command icon as visible") + } +} + +// Test ObjectSpawn creation and integration +func TestNewObjectSpawn(t *testing.T) { + objectSpawn := NewObjectSpawn() + if objectSpawn == nil { + t.Fatal("NewObjectSpawn returned nil") + } + + if objectSpawn.Spawn == nil { + t.Fatal("ObjectSpawn should have embedded Spawn") + } + + // Test default spawn type + if objectSpawn.GetSpawnType() != ObjectSpawnType { + t.Errorf("Expected spawn type %d, got %d", ObjectSpawnType, objectSpawn.GetSpawnType()) + } + + // Test default appearance values + appearance := objectSpawn.GetAppearanceData() + if appearance.ActivityStatus != ObjectActivityStatus { + t.Errorf("Expected activity status %d, got %d", ObjectActivityStatus, appearance.ActivityStatus) + } + if appearance.Pos.State != ObjectPosState { + t.Errorf("Expected pos state %d, got %d", ObjectPosState, appearance.Pos.State) + } + if appearance.Difficulty != ObjectDifficulty { + t.Errorf("Expected difficulty %d, got %d", ObjectDifficulty, appearance.Difficulty) + } + + if !objectSpawn.IsObject() { + t.Error("ObjectSpawn should return true for IsObject()") + } +} + +func TestObjectSpawnMerchantProperties(t *testing.T) { + objectSpawn := NewObjectSpawn() + + // Test merchant ID + testMerchantID := int32(54321) + objectSpawn.SetMerchantID(testMerchantID) + if objectSpawn.GetMerchantID() != testMerchantID { + t.Errorf("Expected merchant ID %d, got %d", testMerchantID, objectSpawn.GetMerchantID()) + } + + // Test merchant type + testMerchantType := int8(3) + objectSpawn.SetMerchantType(testMerchantType) + if objectSpawn.GetMerchantType() != testMerchantType { + t.Errorf("Expected merchant type %d, got %d", testMerchantType, objectSpawn.GetMerchantType()) + } + + // Test merchant level range + minLevel, maxLevel := int8(5), int8(25) + objectSpawn.SetMerchantLevelRange(minLevel, maxLevel) + if objectSpawn.GetMerchantMinLevel() != minLevel { + t.Errorf("Expected min level %d, got %d", minLevel, objectSpawn.GetMerchantMinLevel()) + } + if objectSpawn.GetMerchantMaxLevel() != maxLevel { + t.Errorf("Expected max level %d, got %d", maxLevel, objectSpawn.GetMerchantMaxLevel()) + } + + // Test collector + objectSpawn.SetCollector(true) + if !objectSpawn.IsCollector() { + t.Error("ObjectSpawn should be a collector after setting to true") + } +} + +func TestObjectSpawnTransporter(t *testing.T) { + objectSpawn := NewObjectSpawn() + + testTransporterID := int32(11111) + objectSpawn.SetTransporterID(testTransporterID) + if objectSpawn.GetTransporterID() != testTransporterID { + t.Errorf("Expected transporter ID %d, got %d", testTransporterID, objectSpawn.GetTransporterID()) + } +} + +func TestObjectSpawnCommandIcon(t *testing.T) { + objectSpawn := NewObjectSpawn() + + // Test setting command icon + objectSpawn.SetShowCommandIcon(true) + if !objectSpawn.ShowsCommandIcon() { + t.Error("ObjectSpawn should show command icon after setting to true") + } + + objectSpawn.SetShowCommandIcon(false) + if objectSpawn.ShowsCommandIcon() { + t.Error("ObjectSpawn should not show command icon after setting to false") + } +} + +func TestObjectSpawnCopy(t *testing.T) { + // Create original with properties + original := NewObjectSpawn() + original.SetClickable(true) + original.SetDeviceID(7) + original.SetMerchantID(98765) + original.SetMerchantType(4) + original.SetCollector(true) + original.SetTransporterID(55555) + original.SetDatabaseID(12345) + original.SetName("Test Object") + original.SetLevel(10) + + // Create copy + copied := original.Copy() + if copied == nil { + t.Fatal("Copy returned nil") + } + + // Verify properties were copied + if copied.IsClickable() != original.IsClickable() { + t.Error("Copied clickable state doesn't match") + } + + if copied.GetDeviceID() != original.GetDeviceID() { + t.Error("Copied device ID doesn't match") + } + + if copied.GetMerchantID() != original.GetMerchantID() { + t.Error("Copied merchant ID doesn't match") + } + + if copied.IsCollector() != original.IsCollector() { + t.Error("Copied collector flag doesn't match") + } + + if copied.GetTransporterID() != original.GetTransporterID() { + t.Error("Copied transporter ID doesn't match") + } + + if copied.GetDatabaseID() != original.GetDatabaseID() { + t.Error("Copied database ID doesn't match") + } + + if strings.TrimRight(copied.GetName(), "\x00") != strings.TrimRight(original.GetName(), "\x00") { + t.Error("Copied name doesn't match") + } + + if copied.GetLevel() != original.GetLevel() { + t.Error("Copied level doesn't match") + } + + // Verify independence + copied.SetClickable(false) + if copied.IsClickable() == original.IsClickable() { + t.Error("Copy should be independent from original") + } +} + +func TestObjectSpawnInfo(t *testing.T) { + objectSpawn := NewObjectSpawn() + objectSpawn.SetClickable(true) + objectSpawn.SetDeviceID(8) + objectSpawn.SetMerchantID(77777) + objectSpawn.SetTransporterID(88888) + objectSpawn.SetCollector(true) + objectSpawn.SetShowCommandIcon(true) + objectSpawn.SetDatabaseID(99999) + + info := objectSpawn.GetObjectInfo() + if info == nil { + t.Fatal("GetObjectInfo returned nil") + } + + // Check spawn info + if info["database_id"] != int32(99999) { + t.Error("Info should show correct database ID") + } + + // Check object info + if info["clickable"] != true { + t.Error("Info should show object as clickable") + } + + if info["device_id"] != int8(8) { + t.Error("Info should show correct device ID") + } + + if info["merchant_id"] != int32(77777) { + t.Error("Info should show correct merchant ID") + } + + if info["transporter_id"] != int32(88888) { + t.Error("Info should show correct transporter ID") + } + + if info["is_collector"] != true { + t.Error("Info should show object as collector") + } + + if info["shows_command_icon"] != true { + t.Error("Info should show command icon as visible") + } +} + +// Test ObjectManager functionality +func TestNewObjectManager(t *testing.T) { + manager := NewObjectManager() + if manager == nil { + t.Fatal("NewObjectManager returned nil") + } + + if manager.GetObjectCount() != 0 { + t.Error("New manager should have 0 objects") + } + + if manager.GetZoneCount() != 0 { + t.Error("New manager should have 0 zones") + } +} + +func TestObjectManagerAddRemove(t *testing.T) { + manager := NewObjectManager() + + // Test adding nil object + err := manager.AddObject(nil) + if err == nil { + t.Error("Expected error when adding nil object") + } + + // Create test object + obj := NewObject() + obj.SetDatabaseID(12345) + obj.SetZone("testzone") + + // Test adding object without database ID + objNoDB := NewObject() + err = manager.AddObject(objNoDB) + if err == nil { + t.Error("Expected error when adding object without database ID") + } + + // Test adding valid object + err = manager.AddObject(obj) + if err != nil { + t.Errorf("Unexpected error adding object: %v", err) + } + + if manager.GetObjectCount() != 1 { + t.Error("Manager should have 1 object after adding") + } + + // Test adding duplicate + err = manager.AddObject(obj) + if err == nil { + t.Error("Expected error when adding duplicate object") + } + + // Test retrieving object + retrieved := manager.GetObject(12345) + if retrieved == nil { + t.Error("Should be able to retrieve added object") + } + + if retrieved.GetDatabaseID() != obj.GetDatabaseID() { + t.Error("Retrieved object should match added object") + } + + // Test removing object + err = manager.RemoveObject(12345) + if err != nil { + t.Errorf("Unexpected error removing object: %v", err) + } + + if manager.GetObjectCount() != 0 { + t.Error("Manager should have 0 objects after removing") + } + + // Test removing non-existent object + err = manager.RemoveObject(99999) + if err == nil { + t.Error("Expected error when removing non-existent object") + } +} + +func TestObjectManagerZoneOperations(t *testing.T) { + manager := NewObjectManager() + + // Create objects in different zones + obj1 := NewObject() + obj1.SetDatabaseID(1) + obj1.SetZone("zone1") + + obj2 := NewObject() + obj2.SetDatabaseID(2) + obj2.SetZone("zone1") + + obj3 := NewObject() + obj3.SetDatabaseID(3) + obj3.SetZone("zone2") + + // Add objects + manager.AddObject(obj1) + manager.AddObject(obj2) + manager.AddObject(obj3) + + // Test zone count + if manager.GetZoneCount() != 2 { + t.Errorf("Expected 2 zones, got %d", manager.GetZoneCount()) + } + + // Test getting objects by zone + zone1Objects := manager.GetObjectsByZone("zone1") + if len(zone1Objects) != 2 { + t.Errorf("Expected 2 objects in zone1, got %d", len(zone1Objects)) + } + + zone2Objects := manager.GetObjectsByZone("zone2") + if len(zone2Objects) != 1 { + t.Errorf("Expected 1 object in zone2, got %d", len(zone2Objects)) + } + + emptyZoneObjects := manager.GetObjectsByZone("nonexistent") + if len(emptyZoneObjects) != 0 { + t.Errorf("Expected 0 objects in nonexistent zone, got %d", len(emptyZoneObjects)) + } + + // Test clearing zone + clearedCount := manager.ClearZone("zone1") + if clearedCount != 2 { + t.Errorf("Expected to clear 2 objects, cleared %d", clearedCount) + } + + if manager.GetObjectCount() != 1 { + t.Errorf("Expected 1 object remaining, got %d", manager.GetObjectCount()) + } + + if manager.GetZoneCount() != 1 { + t.Errorf("Expected 1 zone remaining, got %d", manager.GetZoneCount()) + } +} + +func TestObjectManagerTypeFiltering(t *testing.T) { + manager := NewObjectManager() + + // Create objects with different properties + interactive := NewObject() + interactive.SetDatabaseID(1) + interactive.SetZone("testzone") + interactive.SetClickable(true) + + transport := NewObject() + transport.SetDatabaseID(2) + transport.SetZone("testzone") + transport.SetTransporterID(100) + + merchant := NewObject() + merchant.SetDatabaseID(3) + merchant.SetZone("testzone") + merchant.SetMerchantID(200) + + collector := NewObject() + collector.SetDatabaseID(4) + collector.SetZone("testzone") + collector.SetCollector(true) + + // Add objects + manager.AddObject(interactive) + manager.AddObject(transport) + manager.AddObject(merchant) + manager.AddObject(collector) + + // Test type filtering + interactiveObjects := manager.GetInteractiveObjects() + if len(interactiveObjects) != 1 { + t.Errorf("Expected 1 interactive object, got %d", len(interactiveObjects)) + } + + transportObjects := manager.GetTransportObjects() + if len(transportObjects) != 1 { + t.Errorf("Expected 1 transport object, got %d", len(transportObjects)) + } + + merchantObjects := manager.GetMerchantObjects() + if len(merchantObjects) != 1 { + t.Errorf("Expected 1 merchant object, got %d", len(merchantObjects)) + } + + collectorObjects := manager.GetCollectorObjects() + if len(collectorObjects) != 1 { + t.Errorf("Expected 1 collector object, got %d", len(collectorObjects)) + } + + // Test GetObjectsByType + if len(manager.GetObjectsByType("interactive")) != 1 { + t.Error("GetObjectsByType should return 1 interactive object") + } + + if len(manager.GetObjectsByType("transport")) != 1 { + t.Error("GetObjectsByType should return 1 transport object") + } + + if len(manager.GetObjectsByType("merchant")) != 1 { + t.Error("GetObjectsByType should return 1 merchant object") + } + + if len(manager.GetObjectsByType("collector")) != 1 { + t.Error("GetObjectsByType should return 1 collector object") + } + + if len(manager.GetObjectsByType("invalid")) != 0 { + t.Error("GetObjectsByType should return 0 objects for invalid type") + } +} + +func TestObjectManagerUpdate(t *testing.T) { + manager := NewObjectManager() + + obj := NewObject() + obj.SetDatabaseID(12345) + obj.SetZone("zone1") + obj.SetClickable(false) + + manager.AddObject(obj) + + // Test update + err := manager.UpdateObject(12345, func(o *Object) { + o.SetClickable(true) + o.SetZone("zone2") + }) + + if err != nil { + t.Errorf("Unexpected error updating object: %v", err) + } + + // Verify update took effect + updated := manager.GetObject(12345) + if !updated.IsClickable() { + t.Error("Object should be clickable after update") + } + + if updated.GetZoneName() != "zone2" { + t.Error("Object should be in zone2 after update") + } + + // Verify zone indexing was updated + zone1Objects := manager.GetObjectsByZone("zone1") + zone2Objects := manager.GetObjectsByZone("zone2") + + if len(zone1Objects) != 0 { + t.Error("zone1 should have 0 objects after update") + } + + if len(zone2Objects) != 1 { + t.Error("zone2 should have 1 object after update") + } + + // Test updating non-existent object + err = manager.UpdateObject(99999, func(o *Object) {}) + if err == nil { + t.Error("Expected error when updating non-existent object") + } +} + +func TestObjectManagerStatistics(t *testing.T) { + manager := NewObjectManager() + + // Create test objects + obj1 := NewObject() + obj1.SetDatabaseID(1) + obj1.SetZone("zone1") + obj1.SetClickable(true) + + obj2 := NewObject() + obj2.SetDatabaseID(2) + obj2.SetZone("zone2") + obj2.SetMerchantID(100) + + manager.AddObject(obj1) + manager.AddObject(obj2) + + stats := manager.GetStatistics() + if stats == nil { + t.Fatal("GetStatistics returned nil") + } + + if stats["total_objects"] != 2 { + t.Error("Statistics should show 2 total objects") + } + + if stats["zones_with_objects"] != 2 { + t.Error("Statistics should show 2 zones with objects") + } + + if stats["interactive_objects"] != 1 { + t.Error("Statistics should show 1 interactive object") + } + + if stats["merchant_objects"] != 1 { + t.Error("Statistics should show 1 merchant object") + } + + zoneStats, ok := stats["objects_by_zone"].(map[string]int) + if !ok { + t.Fatal("Zone stats should be a map[string]int") + } + + if zoneStats["zone1"] != 1 { + t.Error("zone1 should have 1 object in statistics") + } + + if zoneStats["zone2"] != 1 { + t.Error("zone2 should have 1 object in statistics") + } +} + +func TestObjectManagerShutdown(t *testing.T) { + manager := NewObjectManager() + + // Add some objects + obj1 := NewObject() + obj1.SetDatabaseID(1) + obj1.SetZone("zone1") + + manager.AddObject(obj1) + + if manager.GetObjectCount() != 1 { + t.Error("Manager should have 1 object before shutdown") + } + + // Test shutdown + manager.Shutdown() + + if manager.GetObjectCount() != 0 { + t.Error("Manager should have 0 objects after shutdown") + } + + if manager.GetZoneCount() != 0 { + t.Error("Manager should have 0 zones after shutdown") + } +} + +// Test utility functions +func TestCreateSpecialObjectSpawns(t *testing.T) { + // Test creating merchant object spawn + merchant := CreateMerchantObjectSpawn(12345, 2) + if merchant == nil { + t.Fatal("CreateMerchantObjectSpawn returned nil") + } + + if merchant.GetMerchantID() != 12345 { + t.Error("Merchant object should have correct merchant ID") + } + + if merchant.GetMerchantType() != 2 { + t.Error("Merchant object should have correct merchant type") + } + + if !merchant.IsClickable() { + t.Error("Merchant object should be clickable") + } + + if !merchant.ShowsCommandIcon() { + t.Error("Merchant object should show command icon") + } + + // Test creating transport object spawn + transport := CreateTransportObjectSpawn(54321) + if transport == nil { + t.Fatal("CreateTransportObjectSpawn returned nil") + } + + if transport.GetTransporterID() != 54321 { + t.Error("Transport object should have correct transporter ID") + } + + // Test creating device object spawn + device := CreateDeviceObjectSpawn(7) + if device == nil { + t.Fatal("CreateDeviceObjectSpawn returned nil") + } + + if device.GetDeviceID() != 7 { + t.Error("Device object should have correct device ID") + } + + // Test creating collector object spawn + collector := CreateCollectorObjectSpawn() + if collector == nil { + t.Fatal("CreateCollectorObjectSpawn returned nil") + } + + if !collector.IsCollector() { + t.Error("Collector object should be a collector") + } +} + +// Test ObjectSpawnManager +func TestObjectSpawnManager(t *testing.T) { + manager := NewObjectSpawnManager() + if manager == nil { + t.Fatal("NewObjectSpawnManager returned nil") + } + + // Test adding object spawn + objectSpawn := NewObjectSpawn() + objectSpawn.SetDatabaseID(12345) + + err := manager.AddObjectSpawn(objectSpawn) + if err != nil { + t.Errorf("Unexpected error adding object spawn: %v", err) + } + + // Test retrieving object spawn + retrieved := manager.GetObjectSpawn(objectSpawn.GetID()) + if retrieved == nil { + t.Error("Should be able to retrieve added object spawn") + } + + // Test removing object spawn + err = manager.RemoveObjectSpawn(objectSpawn.GetID()) + if err != nil { + t.Errorf("Unexpected error removing object spawn: %v", err) + } + + // Test adding nil object spawn + err = manager.AddObjectSpawn(nil) + if err == nil { + t.Error("Expected error when adding nil object spawn") + } +} + +// Test interface implementations +func TestObjectItem(t *testing.T) { + item := NewObjectItem(12345, "Test Item", 5) + if item == nil { + t.Fatal("NewObjectItem returned nil") + } + + if item.GetID() != 12345 { + t.Error("Item should have correct ID") + } + + if item.GetName() != "Test Item" { + t.Error("Item should have correct name") + } + + if item.GetQuantity() != 5 { + t.Error("Item should have correct quantity") + } + + // Test setting properties + item.SetQuantity(10) + if item.GetQuantity() != 10 { + t.Error("Item quantity should be updated") + } + + item.SetNoTrade(true) + if !item.IsNoTrade() { + t.Error("Item should be no-trade after setting") + } + + item.SetHeirloom(true) + if !item.IsHeirloom() { + t.Error("Item should be heirloom after setting") + } + + item.SetAttuned(true) + if !item.IsAttuned() { + t.Error("Item should be attuned after setting") + } + + // Test creation time + if item.GetCreationTime().IsZero() { + t.Error("Item should have a creation time") + } + + // Test group character IDs + groupIDs := []int32{1, 2, 3} + item.SetGroupCharacterIDs(groupIDs) + retrievedIDs := item.GetGroupCharacterIDs() + if len(retrievedIDs) != len(groupIDs) { + t.Error("Group character IDs length should match") + } + for i, id := range groupIDs { + if retrievedIDs[i] != id { + t.Errorf("Group character ID %d should be %d, got %d", i, id, retrievedIDs[i]) + } + } +} + +func TestObjectSpawnAsEntity(t *testing.T) { + objectSpawn := NewObjectSpawn() + entity := NewObjectSpawnAsEntity(objectSpawn) + if entity == nil { + t.Fatal("NewObjectSpawnAsEntity returned nil") + } + + // Test entity interface methods + if entity.GetID() != objectSpawn.GetID() { + t.Error("Entity should have same ID as object spawn") + } + + if entity.IsPlayer() { + t.Error("Object entity should not be a player") + } + + if entity.IsBot() { + t.Error("Object entity should not be a bot by default") + } + + entity.SetBot(true) + if !entity.IsBot() { + t.Error("Object entity should be a bot after setting") + } + + // Test coins + if entity.HasCoins(100) { + t.Error("Object entity should not have coins by default") + } + + entity.SetCoins(500) + if !entity.HasCoins(100) { + t.Error("Object entity should have sufficient coins after setting") + } + + if entity.HasCoins(1000) { + t.Error("Object entity should not have more coins than set") + } + + // Test client version + if entity.GetClientVersion() != 1000 { + t.Error("Object entity should have default client version") + } + + entity.SetClientVersion(2000) + if entity.GetClientVersion() != 2000 { + t.Error("Object entity should have updated client version") + } + + // Test name + if entity.GetName() != "Object" { + t.Error("Object entity should have default name") + } + + entity.SetName("Test Object") + if entity.GetName() != "Test Object" { + t.Error("Object entity should have updated name") + } +} + +// Benchmark tests +func BenchmarkNewObject(b *testing.B) { + for i := 0; i < b.N; i++ { + NewObject() + } +} + +func BenchmarkObjectCopy(b *testing.B) { + obj := NewObject() + obj.SetClickable(true) + obj.SetZone("testzone") + obj.SetMerchantID(12345) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + obj.Copy() + } +} + +func BenchmarkObjectManagerAdd(b *testing.B) { + manager := NewObjectManager() + objects := make([]*Object, b.N) + + for i := 0; i < b.N; i++ { + obj := NewObject() + obj.SetDatabaseID(int32(i + 1)) + obj.SetZone("testzone") + objects[i] = obj + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + manager.AddObject(objects[i]) + } +} + +func BenchmarkObjectManagerGet(b *testing.B) { + manager := NewObjectManager() + + // Pre-populate with objects + for i := 0; i < 1000; i++ { + obj := NewObject() + obj.SetDatabaseID(int32(i + 1)) + obj.SetZone("testzone") + manager.AddObject(obj) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + manager.GetObject(int32((i % 1000) + 1)) + } +} \ No newline at end of file