538 lines
13 KiB
Go
538 lines
13 KiB
Go
package items
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Global unique ID counter for items
|
|
var (
|
|
nextUniqueID int64 = 1
|
|
uniqueIDMux sync.Mutex
|
|
)
|
|
|
|
// NextUniqueID generates the next unique ID for an item
|
|
func NextUniqueID() int64 {
|
|
uniqueIDMux.Lock()
|
|
defer uniqueIDMux.Unlock()
|
|
id := nextUniqueID
|
|
nextUniqueID++
|
|
return id
|
|
}
|
|
|
|
// Error handling utilities
|
|
|
|
// ItemError represents an item-specific error
|
|
type ItemError struct {
|
|
message string
|
|
}
|
|
|
|
func (e *ItemError) Error() string {
|
|
return e.message
|
|
}
|
|
|
|
// NewItemError creates a new item error
|
|
func NewItemError(message string) *ItemError {
|
|
return &ItemError{message: message}
|
|
}
|
|
|
|
// IsItemError checks if an error is an ItemError
|
|
func IsItemError(err error) bool {
|
|
_, ok := err.(*ItemError)
|
|
return ok
|
|
}
|
|
|
|
// Common item errors
|
|
var (
|
|
ErrItemNotFound = NewItemError("item not found")
|
|
ErrInvalidItem = NewItemError("invalid item")
|
|
ErrItemLocked = NewItemError("item is locked")
|
|
ErrInsufficientSpace = NewItemError("insufficient inventory space")
|
|
ErrCannotEquip = NewItemError("cannot equip item")
|
|
ErrCannotTrade = NewItemError("cannot trade item")
|
|
ErrItemExpired = NewItemError("item has expired")
|
|
)
|
|
|
|
// Item creation utilities
|
|
|
|
// NewItem creates a new item instance with default values
|
|
func NewItem() *Item {
|
|
return &Item{
|
|
Details: ItemCore{
|
|
UniqueID: NextUniqueID(),
|
|
Count: 1,
|
|
},
|
|
GenericInfo: GenericInfo{
|
|
Condition: 100, // 100% condition
|
|
},
|
|
Created: time.Now(),
|
|
GroupedCharIDs: make(map[int32]bool),
|
|
}
|
|
}
|
|
|
|
// NewItemFromTemplate creates a new item from a template item
|
|
func NewItemFromTemplate(template *Item) *Item {
|
|
if template == nil {
|
|
return NewItem()
|
|
}
|
|
|
|
item := &Item{
|
|
// Copy basic information
|
|
LowerName: template.LowerName,
|
|
Name: template.Name,
|
|
Description: template.Description,
|
|
StackCount: template.StackCount,
|
|
SellPrice: template.SellPrice,
|
|
SellStatus: template.SellStatus,
|
|
MaxSellValue: template.MaxSellValue,
|
|
BrokerPrice: template.BrokerPrice,
|
|
WeaponType: template.WeaponType,
|
|
Adornment: template.Adornment,
|
|
Creator: template.Creator,
|
|
SellerName: template.SellerName,
|
|
SellerCharID: template.SellerCharID,
|
|
SellerHouseID: template.SellerHouseID,
|
|
Created: time.Now(),
|
|
GroupedCharIDs: make(map[int32]bool),
|
|
EffectType: template.EffectType,
|
|
BookLanguage: template.BookLanguage,
|
|
SpellID: template.SpellID,
|
|
SpellTier: template.SpellTier,
|
|
ItemScript: template.ItemScript,
|
|
|
|
// Copy core data with new unique ID
|
|
Details: ItemCore{
|
|
ItemID: template.Details.ItemID,
|
|
SOEId: template.Details.SOEId,
|
|
UniqueID: NextUniqueID(),
|
|
Count: 1,
|
|
Tier: template.Details.Tier,
|
|
Icon: template.Details.Icon,
|
|
ClassicIcon: template.Details.ClassicIcon,
|
|
NumSlots: template.Details.NumSlots,
|
|
RecommendedLevel: template.Details.RecommendedLevel,
|
|
},
|
|
|
|
// Copy generic info
|
|
GenericInfo: template.GenericInfo,
|
|
}
|
|
|
|
// Copy arrays and slices
|
|
if template.Classifications != nil {
|
|
item.Classifications = make([]*Classifications, len(template.Classifications))
|
|
copy(item.Classifications, template.Classifications)
|
|
}
|
|
|
|
if template.ItemStats != nil {
|
|
item.ItemStats = make([]*ItemStat, len(template.ItemStats))
|
|
copy(item.ItemStats, template.ItemStats)
|
|
}
|
|
|
|
if template.ItemSets != nil {
|
|
item.ItemSets = make([]*ItemSet, len(template.ItemSets))
|
|
copy(item.ItemSets, template.ItemSets)
|
|
}
|
|
|
|
if template.ItemStringStats != nil {
|
|
item.ItemStringStats = make([]*ItemStatString, len(template.ItemStringStats))
|
|
copy(item.ItemStringStats, template.ItemStringStats)
|
|
}
|
|
|
|
if template.ItemLevelOverrides != nil {
|
|
item.ItemLevelOverrides = make([]*ItemLevelOverride, len(template.ItemLevelOverrides))
|
|
copy(item.ItemLevelOverrides, template.ItemLevelOverrides)
|
|
}
|
|
|
|
if template.ItemEffects != nil {
|
|
item.ItemEffects = make([]*ItemEffect, len(template.ItemEffects))
|
|
copy(item.ItemEffects, template.ItemEffects)
|
|
}
|
|
|
|
if template.BookPages != nil {
|
|
item.BookPages = make([]*BookPage, len(template.BookPages))
|
|
copy(item.BookPages, template.BookPages)
|
|
}
|
|
|
|
if template.SlotData != nil {
|
|
item.SlotData = make([]int8, len(template.SlotData))
|
|
copy(item.SlotData, template.SlotData)
|
|
}
|
|
|
|
// Copy type-specific info pointers (deep copy if needed)
|
|
if template.WeaponInfo != nil {
|
|
weaponInfo := *template.WeaponInfo
|
|
item.WeaponInfo = &weaponInfo
|
|
}
|
|
|
|
if template.RangedInfo != nil {
|
|
rangedInfo := *template.RangedInfo
|
|
item.RangedInfo = &rangedInfo
|
|
}
|
|
|
|
if template.ArmorInfo != nil {
|
|
armorInfo := *template.ArmorInfo
|
|
item.ArmorInfo = &armorInfo
|
|
}
|
|
|
|
if template.AdornmentInfo != nil {
|
|
adornmentInfo := *template.AdornmentInfo
|
|
item.AdornmentInfo = &adornmentInfo
|
|
}
|
|
|
|
if template.BagInfo != nil {
|
|
bagInfo := *template.BagInfo
|
|
item.BagInfo = &bagInfo
|
|
}
|
|
|
|
if template.FoodInfo != nil {
|
|
foodInfo := *template.FoodInfo
|
|
item.FoodInfo = &foodInfo
|
|
}
|
|
|
|
if template.BaubleInfo != nil {
|
|
baubleInfo := *template.BaubleInfo
|
|
item.BaubleInfo = &baubleInfo
|
|
}
|
|
|
|
if template.BookInfo != nil {
|
|
bookInfo := *template.BookInfo
|
|
item.BookInfo = &bookInfo
|
|
}
|
|
|
|
if template.HouseItemInfo != nil {
|
|
houseItemInfo := *template.HouseItemInfo
|
|
item.HouseItemInfo = &houseItemInfo
|
|
}
|
|
|
|
if template.HouseContainerInfo != nil {
|
|
houseContainerInfo := *template.HouseContainerInfo
|
|
item.HouseContainerInfo = &houseContainerInfo
|
|
}
|
|
|
|
if template.SkillInfo != nil {
|
|
skillInfo := *template.SkillInfo
|
|
item.SkillInfo = &skillInfo
|
|
}
|
|
|
|
if template.RecipeBookInfo != nil {
|
|
recipeBookInfo := *template.RecipeBookInfo
|
|
if template.RecipeBookInfo.Recipes != nil {
|
|
recipeBookInfo.Recipes = make([]uint32, len(template.RecipeBookInfo.Recipes))
|
|
copy(recipeBookInfo.Recipes, template.RecipeBookInfo.Recipes)
|
|
}
|
|
item.RecipeBookInfo = &recipeBookInfo
|
|
}
|
|
|
|
if template.ItemSetInfo != nil {
|
|
itemSetInfo := *template.ItemSetInfo
|
|
item.ItemSetInfo = &itemSetInfo
|
|
}
|
|
|
|
if template.ThrownInfo != nil {
|
|
thrownInfo := *template.ThrownInfo
|
|
item.ThrownInfo = &thrownInfo
|
|
}
|
|
|
|
return item
|
|
}
|
|
|
|
// Item validation utilities
|
|
|
|
// ItemValidationResult represents the result of item validation
|
|
type ItemValidationResult struct {
|
|
Valid bool `json:"valid"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
}
|
|
|
|
// Validate validates the item's data
|
|
func (item *Item) Validate() *ItemValidationResult {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
|
|
result := &ItemValidationResult{Valid: true}
|
|
|
|
if item.Details.ItemID <= 0 {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, "invalid item ID")
|
|
}
|
|
|
|
if item.Name == "" {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, "item name cannot be empty")
|
|
}
|
|
|
|
if len(item.Name) > MaxItemNameLength {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, fmt.Sprintf("item name too long: %d > %d", len(item.Name), MaxItemNameLength))
|
|
}
|
|
|
|
if len(item.Description) > MaxItemDescLength {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, fmt.Sprintf("item description too long: %d > %d", len(item.Description), MaxItemDescLength))
|
|
}
|
|
|
|
if item.Details.Count <= 0 {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, "item count must be positive")
|
|
}
|
|
|
|
if item.GenericInfo.Condition < 0 || item.GenericInfo.Condition > 100 {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, "item condition must be between 0 and 100")
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Item utility methods
|
|
|
|
// IsItemLocked checks if the item is locked for any reason
|
|
func (item *Item) IsItemLocked() bool {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
return item.Details.ItemLocked
|
|
}
|
|
|
|
// CheckClass checks if the item can be used by the given adventure/tradeskill class
|
|
func (item *Item) CheckClass(adventureClass, tradeskillClass int8) bool {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
|
|
// Check if item has no class restrictions (value of 0 means all classes)
|
|
if item.GenericInfo.AdventureClasses == 0 && item.GenericInfo.TradeskillClasses == 0 {
|
|
return true
|
|
}
|
|
|
|
// Check adventure class
|
|
if item.GenericInfo.AdventureClasses > 0 {
|
|
adventureClassFlag := int64(1 << uint(adventureClass))
|
|
if item.GenericInfo.AdventureClasses&adventureClassFlag == 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Check tradeskill class
|
|
if item.GenericInfo.TradeskillClasses > 0 {
|
|
tradeskillClassFlag := int64(1 << uint(tradeskillClass))
|
|
if item.GenericInfo.TradeskillClasses&tradeskillClassFlag == 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// CheckClassLevel checks if the item can be used by the given class at the given level
|
|
func (item *Item) CheckClassLevel(adventureClass, tradeskillClass int8, playerLevel int16) bool {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
|
|
// First check if the class can use the item
|
|
if !item.CheckClass(adventureClass, tradeskillClass) {
|
|
return false
|
|
}
|
|
|
|
// Check level requirements
|
|
requiredLevel := item.GenericInfo.AdventureDefaultLevel
|
|
if requiredLevel > playerLevel {
|
|
return false
|
|
}
|
|
|
|
// Check for level overrides specific to this class
|
|
for _, override := range item.ItemLevelOverrides {
|
|
if override.AdventureClass == adventureClass || override.TradeskillClass == tradeskillClass {
|
|
if override.Level > playerLevel {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// GetWeight returns the weight of the item in tenths
|
|
func (item *Item) GetWeight() int32 {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
return item.GenericInfo.Weight
|
|
}
|
|
|
|
// IsStackable checks if the item can be stacked
|
|
func (item *Item) IsStackable() bool {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
return item.StackCount > 1
|
|
}
|
|
|
|
// CanStack checks if this item can stack with another item
|
|
func (item *Item) CanStack(other *Item) bool {
|
|
if other == nil {
|
|
return false
|
|
}
|
|
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
other.mutex.RLock()
|
|
defer other.mutex.RUnlock()
|
|
|
|
// Items must have same ID and be stackable
|
|
if item.Details.ItemID != other.Details.ItemID || !item.IsStackable() {
|
|
return false
|
|
}
|
|
|
|
// Check if conditions are similar (within tolerance)
|
|
conditionDiff := item.GenericInfo.Condition - other.GenericInfo.Condition
|
|
if conditionDiff < 0 {
|
|
conditionDiff = -conditionDiff
|
|
}
|
|
if conditionDiff > 10 { // Allow up to 10% condition difference
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// GetSellPrice returns the sell price of the item
|
|
func (item *Item) GetSellPrice() int32 {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
if item.SellPrice > 0 {
|
|
return item.SellPrice
|
|
}
|
|
// Return a fraction of max sell value if no specific sell price is set
|
|
return item.MaxSellValue / 4
|
|
}
|
|
|
|
// HasFlag checks if the item has a specific flag
|
|
func (item *Item) HasFlag(flag int16) bool {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
return item.GenericInfo.ItemFlags&flag != 0
|
|
}
|
|
|
|
// HasFlag2 checks if the item has a specific flag2
|
|
func (item *Item) HasFlag2(flag int16) bool {
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
return item.GenericInfo.ItemFlags2&flag != 0
|
|
}
|
|
|
|
// Slot validation utilities
|
|
|
|
// IsValidSlot checks if a slot ID is valid for equipment
|
|
func IsValidSlot(slot int8) bool {
|
|
return slot >= 0 && slot < NumSlots
|
|
}
|
|
|
|
// GetSlotName returns the name of a slot based on its ID
|
|
func GetSlotName(slot int8) string {
|
|
switch slot {
|
|
case EQ2PrimarySlot:
|
|
return "Primary"
|
|
case EQ2SecondarySlot:
|
|
return "Secondary"
|
|
case EQ2HeadSlot:
|
|
return "Head"
|
|
case EQ2ChestSlot:
|
|
return "Chest"
|
|
case EQ2ShouldersSlot:
|
|
return "Shoulders"
|
|
case EQ2ForearmsSlot:
|
|
return "Forearms"
|
|
case EQ2HandsSlot:
|
|
return "Hands"
|
|
case EQ2LegsSlot:
|
|
return "Legs"
|
|
case EQ2FeetSlot:
|
|
return "Feet"
|
|
case EQ2LRingSlot:
|
|
return "Left Ring"
|
|
case EQ2RRingSlot:
|
|
return "Right Ring"
|
|
case EQ2EarsSlot1:
|
|
return "Left Ear"
|
|
case EQ2EarsSlot2:
|
|
return "Right Ear"
|
|
case EQ2NeckSlot:
|
|
return "Neck"
|
|
case EQ2LWristSlot:
|
|
return "Left Wrist"
|
|
case EQ2RWristSlot:
|
|
return "Right Wrist"
|
|
case EQ2RangeSlot:
|
|
return "Range"
|
|
case EQ2AmmoSlot:
|
|
return "Ammo"
|
|
case EQ2WaistSlot:
|
|
return "Waist"
|
|
case EQ2CloakSlot:
|
|
return "Cloak"
|
|
case EQ2CharmSlot1:
|
|
return "Charm 1"
|
|
case EQ2CharmSlot2:
|
|
return "Charm 2"
|
|
case EQ2FoodSlot:
|
|
return "Food"
|
|
case EQ2DrinkSlot:
|
|
return "Drink"
|
|
case EQ2TexturesSlot:
|
|
return "Textures"
|
|
case EQ2HairSlot:
|
|
return "Hair"
|
|
case EQ2BeardSlot:
|
|
return "Beard"
|
|
case EQ2WingsSlot:
|
|
return "Wings"
|
|
case EQ2NakedChestSlot:
|
|
return "Naked Chest"
|
|
case EQ2NakedLegsSlot:
|
|
return "Naked Legs"
|
|
case EQ2BackSlot:
|
|
return "Back"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// GetItemTypeName returns the name of an item type
|
|
func GetItemTypeName(itemType int8) string {
|
|
switch itemType {
|
|
case ItemTypeNormal:
|
|
return "Normal"
|
|
case ItemTypeWeapon:
|
|
return "Weapon"
|
|
case ItemTypeRanged:
|
|
return "Ranged"
|
|
case ItemTypeArmor:
|
|
return "Armor"
|
|
case ItemTypeShield:
|
|
return "Shield"
|
|
case ItemTypeBag:
|
|
return "Bag"
|
|
case ItemTypeSkill:
|
|
return "Skill"
|
|
case ItemTypeRecipe:
|
|
return "Recipe"
|
|
case ItemTypeFood:
|
|
return "Food"
|
|
case ItemTypeBauble:
|
|
return "Bauble"
|
|
case ItemTypeHouse:
|
|
return "House"
|
|
case ItemTypeThrown:
|
|
return "Thrown"
|
|
case ItemTypeHouseContainer:
|
|
return "House Container"
|
|
case ItemTypeBook:
|
|
return "Book"
|
|
case ItemTypeAdornment:
|
|
return "Adornment"
|
|
case ItemTypePattern:
|
|
return "Pattern"
|
|
case ItemTypeArmorset:
|
|
return "Armor Set"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
} |