519 lines
16 KiB
Go
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)
|
|
}
|