package loot import ( "fmt" "log" "time" "eq2emu/internal/items" ) // ChestInteraction represents the different ways a player can interact with a chest type ChestInteraction int8 const ( ChestInteractionView ChestInteraction = iota ChestInteractionLoot ChestInteractionLootAll ChestInteractionDisarm ChestInteractionLockpick ChestInteractionClose ) // String returns the string representation of ChestInteraction func (ci ChestInteraction) String() string { switch ci { case ChestInteractionView: return "view" case ChestInteractionLoot: return "loot" case ChestInteractionLootAll: return "loot_all" case ChestInteractionDisarm: return "disarm" case ChestInteractionLockpick: return "lockpick" case ChestInteractionClose: return "close" default: return "unknown" } } // ChestInteractionResult represents the result of a chest interaction type ChestInteractionResult struct { Success bool `json:"success"` Result int8 `json:"result"` // ChestResult constant Message string `json:"message"` // Message to display to player Items []*items.Item `json:"items"` // Items received Coins int32 `json:"coins"` // Coins received Experience int32 `json:"experience"` // Experience gained (for disarming/lockpicking) ChestEmpty bool `json:"chest_empty"` // Whether chest is now empty ChestClosed bool `json:"chest_closed"` // Whether chest should be closed } // ChestService handles treasure chest interactions and management type ChestService struct { lootManager *LootManager playerService PlayerService zoneService ZoneService } // PlayerService interface for player-related operations type PlayerService interface { GetPlayerPosition(playerID uint32) (x, y, z, heading float32, zoneID int32, err error) IsPlayerInCombat(playerID uint32) bool CanPlayerCarryItems(playerID uint32, itemCount int) bool AddItemsToPlayer(playerID uint32, items []*items.Item) error AddCoinsToPlayer(playerID uint32, coins int32) error GetPlayerSkillValue(playerID uint32, skillName string) int32 AddPlayerExperience(playerID uint32, experience int32, skillName string) error SendMessageToPlayer(playerID uint32, message string) error } // ZoneService interface for zone-related operations type ZoneService interface { GetZoneRule(zoneID int32, ruleName string) (interface{}, error) SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error) RemoveObjectFromZone(zoneID int32, objectID int32) error GetDistanceBetweenPoints(x1, y1, z1, x2, y2, z2 float32) float32 } // NewChestService creates a new chest service func NewChestService(lootManager *LootManager, playerService PlayerService, zoneService ZoneService) *ChestService { return &ChestService{ lootManager: lootManager, playerService: playerService, zoneService: zoneService, } } // CreateTreasureChestFromLoot creates a treasure chest at the specified location with the given loot func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32, x, y, z, heading float32, lootResult *LootResult, lootRights []uint32) (*TreasureChest, error) { // Check if treasure chests are enabled in this zone enabled, err := cs.zoneService.GetZoneRule(zoneID, ConfigTreasureChestEnabled) if err != nil { log.Printf("%s Failed to check treasure chest rule for zone %d: %v", LogPrefixChest, zoneID, err) } else if enabled == false { log.Printf("%s Treasure chests disabled in zone %d", LogPrefixChest, zoneID) return nil, nil // Not an error, just disabled } // Don't create chest if no loot if lootResult.IsEmpty() { log.Printf("%s No loot to put in treasure chest for spawn %d", LogPrefixChest, spawnID) return nil, nil } // Filter items by tier (only common+ items go in chests, matching C++ ITEM_TAG_COMMON) filteredItems := make([]*items.Item, 0) for _, item := range lootResult.GetItems() { if item.Details.Tier >= LootTierCommon { filteredItems = append(filteredItems, item) } } // Update loot result with filtered items filteredResult := &LootResult{ Items: filteredItems, Coins: lootResult.GetCoins(), } // Don't create chest if no qualifying items and no coins if filteredResult.IsEmpty() { log.Printf("%s No qualifying loot for treasure chest (tier >= %d) for spawn %d", LogPrefixChest, LootTierCommon, spawnID) return nil, nil } // Create the chest chest, err := cs.lootManager.CreateTreasureChest(spawnID, zoneID, x, y, z, heading, filteredResult, lootRights) if err != nil { return nil, fmt.Errorf("failed to create treasure chest: %v", err) } // Spawn the chest object in the zone chestCommands := []string{"loot", "disarm"} // TODO: Add "lockpick" if chest is locked objectID, err := cs.zoneService.SpawnObjectInZone(zoneID, chest.AppearanceID, x, y, z, heading, "Treasure Chest", chestCommands) if err != nil { log.Printf("%s Failed to spawn chest object in zone: %v", LogPrefixChest, err) // Continue anyway, chest exists in memory } else { log.Printf("%s Spawned treasure chest object %d in zone %d", LogPrefixChest, objectID, zoneID) } return chest, nil } // HandleChestInteraction processes a player's interaction with a treasure chest func (cs *ChestService) HandleChestInteraction(chestID int32, playerID uint32, interaction ChestInteraction, itemUniqueID int64) *ChestInteractionResult { result := &ChestInteractionResult{ Success: false, Items: make([]*items.Item, 0), } // Get the chest chest := cs.lootManager.GetTreasureChest(chestID) if chest == nil { result.Result = ChestResultFailed result.Message = "Treasure chest not found" return result } // Basic validation if validationResult := cs.validateChestInteraction(chest, playerID); validationResult != nil { return validationResult } // Process the specific interaction switch interaction { case ChestInteractionView: return cs.handleViewChest(chest, playerID) case ChestInteractionLoot: return cs.handleLootItem(chest, playerID, itemUniqueID) case ChestInteractionLootAll: return cs.handleLootAll(chest, playerID) case ChestInteractionDisarm: return cs.handleDisarmChest(chest, playerID) case ChestInteractionLockpick: return cs.handleLockpickChest(chest, playerID) case ChestInteractionClose: return cs.handleCloseChest(chest, playerID) default: result.Result = ChestResultFailed result.Message = "Unknown chest interaction" return result } } // validateChestInteraction performs basic validation for chest interactions func (cs *ChestService) validateChestInteraction(chest *TreasureChest, playerID uint32) *ChestInteractionResult { // Check loot rights if !chest.HasLootRights(playerID) { return &ChestInteractionResult{ Success: false, Result: ChestResultNoRights, Message: "You do not have rights to loot this chest", } } // Check if player is in combat if cs.playerService.IsPlayerInCombat(playerID) { return &ChestInteractionResult{ Success: false, Result: ChestResultInCombat, Message: "You cannot loot while in combat", } } // Check distance px, py, pz, _, pZoneID, err := cs.playerService.GetPlayerPosition(playerID) if err != nil { return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "Failed to get player position", } } if pZoneID != chest.ZoneID { return &ChestInteractionResult{ Success: false, Result: ChestResultTooFar, Message: "You are too far from the chest", } } distance := cs.zoneService.GetDistanceBetweenPoints(px, py, pz, chest.X, chest.Y, chest.Z) if distance > 10.0 { // TODO: Make this configurable return &ChestInteractionResult{ Success: false, Result: ChestResultTooFar, Message: "You are too far from the chest", } } // Check if chest is locked if chest.IsLocked { return &ChestInteractionResult{ Success: false, Result: ChestResultLocked, Message: "The chest is locked", } } // Check if chest is trapped if chest.IsDisarmable { return &ChestInteractionResult{ Success: false, Result: ChestResultTrapped, Message: "The chest appears to be trapped", } } return nil // Validation passed } // handleViewChest handles viewing chest contents func (cs *ChestService) handleViewChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult { if chest.LootResult.IsEmpty() { return &ChestInteractionResult{ Success: true, Result: ChestResultEmpty, Message: "The chest is empty", ChestEmpty: true, } } return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, Message: fmt.Sprintf("The chest contains %d items and %d coins", len(chest.LootResult.GetItems()), chest.LootResult.GetCoins()), Items: chest.LootResult.GetItems(), Coins: chest.LootResult.GetCoins(), } } // handleLootItem handles looting a specific item from the chest func (cs *ChestService) handleLootItem(chest *TreasureChest, playerID uint32, itemUniqueID int64) *ChestInteractionResult { // Check if player can carry more items if !cs.playerService.CanPlayerCarryItems(playerID, 1) { return &ChestInteractionResult{ Success: false, Result: ChestResultCantCarry, Message: "Your inventory is full", } } // Loot the specific item item, err := cs.lootManager.LootChestItem(chest.ID, playerID, itemUniqueID) if err != nil { return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: fmt.Sprintf("Failed to loot item: %v", err), } } // Add item to player's inventory if err := cs.playerService.AddItemsToPlayer(playerID, []*items.Item{item}); err != nil { log.Printf("%s Failed to add looted item to player %d: %v", LogPrefixChest, playerID, err) // TODO: Put item back in chest? return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "Failed to add item to inventory", } } // Send message to player message := fmt.Sprintf("You looted %s", item.Name) cs.playerService.SendMessageToPlayer(playerID, message) return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, Message: message, Items: []*items.Item{item}, ChestEmpty: cs.lootManager.IsChestEmpty(chest.ID), } } // handleLootAll handles looting all items and coins from the chest func (cs *ChestService) handleLootAll(chest *TreasureChest, playerID uint32) *ChestInteractionResult { lootResult, err := cs.lootManager.LootChestAll(chest.ID, playerID) if err != nil { return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: fmt.Sprintf("Failed to loot chest: %v", err), } } if lootResult.IsEmpty() { return &ChestInteractionResult{ Success: true, Result: ChestResultEmpty, Message: "The chest is empty", ChestEmpty: true, } } // Check if player can carry all items if !cs.playerService.CanPlayerCarryItems(playerID, len(lootResult.Items)) { // TODO: Partial loot or put items back? return &ChestInteractionResult{ Success: false, Result: ChestResultCantCarry, Message: "Your inventory is full", } } // Add items to player's inventory if len(lootResult.Items) > 0 { if err := cs.playerService.AddItemsToPlayer(playerID, lootResult.Items); err != nil { log.Printf("%s Failed to add looted items to player %d: %v", LogPrefixChest, playerID, err) return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "Failed to add items to inventory", } } } // Add coins to player if lootResult.Coins > 0 { if err := cs.playerService.AddCoinsToPlayer(playerID, lootResult.Coins); err != nil { log.Printf("%s Failed to add looted coins to player %d: %v", LogPrefixChest, playerID, err) } } // Send message to player message := fmt.Sprintf("You looted %d items and %d coins", len(lootResult.Items), lootResult.Coins) cs.playerService.SendMessageToPlayer(playerID, message) return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, Message: message, Items: lootResult.Items, Coins: lootResult.Coins, ChestEmpty: true, } } // handleDisarmChest handles disarming a trapped chest func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult { if !chest.IsDisarmable { return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "This chest is not trapped", } } // Get player's disarm skill disarmSkill := cs.playerService.GetPlayerSkillValue(playerID, "Disarm Trap") // Calculate success chance (simplified) successChance := float32(disarmSkill) - float32(chest.DisarmDifficulty) if successChance < 0 { successChance = 0 } else if successChance > 95 { successChance = 95 } // Roll for success roll := float32(time.Now().UnixNano()%100) // Simple random if roll > successChance { // Failed disarm - could trigger trap effects here return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "You failed to disarm the trap", } } // Success - disarm the trap chest.IsDisarmable = false // Give experience experience := int32(chest.DisarmDifficulty * 10) // 10 exp per difficulty point cs.playerService.AddPlayerExperience(playerID, experience, "Disarm Trap") message := "You successfully disarmed the trap" cs.playerService.SendMessageToPlayer(playerID, message) return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, Message: message, Experience: experience, } } // handleLockpickChest handles picking a locked chest func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult { if !chest.IsLocked { return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "This chest is not locked", } } // Get player's lockpicking skill lockpickSkill := cs.playerService.GetPlayerSkillValue(playerID, "Pick Lock") // Calculate success chance (simplified) successChance := float32(lockpickSkill) - float32(chest.LockpickDifficulty) if successChance < 0 { successChance = 0 } else if successChance > 95 { successChance = 95 } // Roll for success roll := float32(time.Now().UnixNano()%100) // Simple random if roll > successChance { return &ChestInteractionResult{ Success: false, Result: ChestResultFailed, Message: "You failed to pick the lock", } } // Success - unlock the chest chest.IsLocked = false // Give experience experience := int32(chest.LockpickDifficulty * 10) // 10 exp per difficulty point cs.playerService.AddPlayerExperience(playerID, experience, "Pick Lock") message := "You successfully picked the lock" cs.playerService.SendMessageToPlayer(playerID, message) return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, Message: message, Experience: experience, } } // handleCloseChest handles closing the chest interface func (cs *ChestService) handleCloseChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult { return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, Message: "Closed chest", ChestClosed: true, } } // CleanupEmptyChests removes empty chests from zones func (cs *ChestService) CleanupEmptyChests(zoneID int32) { chests := cs.lootManager.GetZoneChests(zoneID) for _, chest := range chests { if chest.LootResult.IsEmpty() { // Remove from zone cs.zoneService.RemoveObjectFromZone(zoneID, chest.ID) // Remove from loot manager cs.lootManager.RemoveTreasureChest(chest.ID) } } } // GetPlayerChestList returns a list of chests a player can access func (cs *ChestService) GetPlayerChestList(playerID uint32) []*TreasureChest { return cs.lootManager.GetPlayerChests(playerID) }