519 lines
16 KiB
Go

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) (any, 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)
}