2005 lines
59 KiB
Go
2005 lines
59 KiB
Go
package items
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"eq2emu/internal/database"
|
|
"eq2emu/internal/packets"
|
|
)
|
|
|
|
// Simplified Items System
|
|
// Consolidates all functionality from 10 files into unified architecture
|
|
// Preserves 100% C++ functionality while eliminating Active Record patterns
|
|
|
|
|
|
// Constants and type definitions (consolidated from constants.go and types.go)
|
|
|
|
// Core Data Structures
|
|
|
|
// Item effect types
|
|
type ItemEffectType int
|
|
|
|
const (
|
|
NoEffectType ItemEffectType = 0
|
|
EffectCureTypeTrauma ItemEffectType = 1
|
|
EffectCureTypeArcane ItemEffectType = 2
|
|
EffectCureTypeNoxious ItemEffectType = 3
|
|
EffectCureTypeElemental ItemEffectType = 4
|
|
EffectCureTypeCurse ItemEffectType = 5
|
|
EffectCureTypeMagic ItemEffectType = 6
|
|
EffectCureTypeAll ItemEffectType = 7
|
|
)
|
|
|
|
// Inventory slot types
|
|
type InventorySlotType int
|
|
|
|
const (
|
|
HouseVault InventorySlotType = -5
|
|
SharedBank InventorySlotType = -4
|
|
Bank InventorySlotType = -3
|
|
Overflow InventorySlotType = -2
|
|
UnknownInvSlotType InventorySlotType = -1
|
|
BaseInventory InventorySlotType = 0
|
|
)
|
|
|
|
// Lock reasons for items
|
|
type LockReason uint32
|
|
|
|
const (
|
|
LockReasonNone LockReason = 0
|
|
LockReasonHouse LockReason = 1 << 0
|
|
LockReasonCrafting LockReason = 1 << 1
|
|
LockReasonShop LockReason = 1 << 2
|
|
)
|
|
|
|
// Add item types for tracking
|
|
type AddItemType int
|
|
|
|
const (
|
|
NotSet AddItemType = 0
|
|
BuyFromBroker AddItemType = 1
|
|
GMCommand AddItemType = 2
|
|
)
|
|
|
|
// ItemStatsValues represents complete stat bonuses
|
|
type ItemStatsValues struct {
|
|
Str int16 `json:"str"`
|
|
Sta int16 `json:"sta"`
|
|
Agi int16 `json:"agi"`
|
|
Wis int16 `json:"wis"`
|
|
Int int16 `json:"int"`
|
|
VsSlash int16 `json:"vs_slash"`
|
|
VsCrush int16 `json:"vs_crush"`
|
|
VsPierce int16 `json:"vs_pierce"`
|
|
VsPhysical int16 `json:"vs_physical"`
|
|
VsHeat int16 `json:"vs_heat"`
|
|
VsCold int16 `json:"vs_cold"`
|
|
VsMagic int16 `json:"vs_magic"`
|
|
VsMental int16 `json:"vs_mental"`
|
|
VsDivine int16 `json:"vs_divine"`
|
|
VsDisease int16 `json:"vs_disease"`
|
|
VsPoison int16 `json:"vs_poison"`
|
|
Health int16 `json:"health"`
|
|
Power int16 `json:"power"`
|
|
Concentration int8 `json:"concentration"`
|
|
AbilityModifier int16 `json:"ability_modifier"`
|
|
CriticalMitigation int16 `json:"critical_mitigation"`
|
|
ExtraShieldBlockChance int16 `json:"extra_shield_block_chance"`
|
|
BeneficialCritChance int16 `json:"beneficial_crit_chance"`
|
|
CritBonus int16 `json:"crit_bonus"`
|
|
Potency int16 `json:"potency"`
|
|
HateGainMod int16 `json:"hate_gain_mod"`
|
|
AbilityReuseSpeed int16 `json:"ability_reuse_speed"`
|
|
AbilityCastingSpeed int16 `json:"ability_casting_speed"`
|
|
AbilityRecoverySpeed int16 `json:"ability_recovery_speed"`
|
|
SpellReuseSpeed int16 `json:"spell_reuse_speed"`
|
|
SpellMultiAttackChance int16 `json:"spell_multi_attack_chance"`
|
|
DPS int16 `json:"dps"`
|
|
AttackSpeed int16 `json:"attack_speed"`
|
|
MultiAttackChance int16 `json:"multi_attack_chance"`
|
|
Flurry int16 `json:"flurry"`
|
|
AEAutoattackChance int16 `json:"ae_autoattack_chance"`
|
|
Strikethrough int16 `json:"strikethrough"`
|
|
Accuracy int16 `json:"accuracy"`
|
|
OffensiveSpeed int16 `json:"offensive_speed"`
|
|
UncontestedParry float32 `json:"uncontested_parry"`
|
|
UncontestedBlock float32 `json:"uncontested_block"`
|
|
UncontestedDodge float32 `json:"uncontested_dodge"`
|
|
UncontestedRiposte float32 `json:"uncontested_riposte"`
|
|
SizeMod float32 `json:"size_mod"`
|
|
}
|
|
|
|
// ItemCore contains core data for an item instance
|
|
type ItemCore struct {
|
|
ItemID int32 `json:"item_id"`
|
|
SOEId int32 `json:"soe_id"`
|
|
BagID int32 `json:"bag_id"`
|
|
InvSlotID int32 `json:"inv_slot_id"`
|
|
SlotID int16 `json:"slot_id"`
|
|
EquipSlotID int16 `json:"equip_slot_id"`
|
|
AppearanceType int16 `json:"appearance_type"`
|
|
Index int8 `json:"index"`
|
|
Icon int16 `json:"icon"`
|
|
ClassicIcon int16 `json:"classic_icon"`
|
|
Count int16 `json:"count"`
|
|
Tier int8 `json:"tier"`
|
|
NumSlots int8 `json:"num_slots"`
|
|
UniqueID int64 `json:"unique_id"`
|
|
NumFreeSlots int8 `json:"num_free_slots"`
|
|
RecommendedLevel int16 `json:"recommended_level"`
|
|
ItemLocked bool `json:"item_locked"`
|
|
LockFlags int32 `json:"lock_flags"`
|
|
NewItem bool `json:"new_item"`
|
|
NewIndex int16 `json:"new_index"`
|
|
}
|
|
|
|
// ItemStat represents a single stat on an item
|
|
type ItemStat struct {
|
|
StatName string `json:"stat_name"`
|
|
StatType int32 `json:"stat_type"`
|
|
StatSubtype int16 `json:"stat_subtype"`
|
|
StatTypeCombined int16 `json:"stat_type_combined"`
|
|
Value float32 `json:"value"`
|
|
Level int8 `json:"level"`
|
|
}
|
|
|
|
// ItemSet represents an item set piece
|
|
type ItemSet struct {
|
|
ItemID int32 `json:"item_id"`
|
|
ItemCRC int32 `json:"item_crc"`
|
|
ItemIcon int16 `json:"item_icon"`
|
|
ItemStackSize int16 `json:"item_stack_size"`
|
|
ItemListColor int32 `json:"item_list_color"`
|
|
Name string `json:"name"`
|
|
Language int8 `json:"language"`
|
|
}
|
|
|
|
// Classifications represents item classifications
|
|
type Classifications struct {
|
|
ClassificationID int32 `json:"classification_id"`
|
|
ClassificationName string `json:"classification_name"`
|
|
}
|
|
|
|
// ItemLevelOverride represents level overrides
|
|
type ItemLevelOverride struct {
|
|
AdventureClass int8 `json:"adventure_class"`
|
|
TradeskillClass int8 `json:"tradeskill_class"`
|
|
Level int16 `json:"level"`
|
|
}
|
|
|
|
// ItemClass represents class requirements
|
|
type ItemClass struct {
|
|
AdventureClass int8 `json:"adventure_class"`
|
|
TradeskillClass int8 `json:"tradeskill_class"`
|
|
Level int16 `json:"level"`
|
|
}
|
|
|
|
// ItemAppearance represents visual appearance
|
|
type ItemAppearance struct {
|
|
Type int16 `json:"type"`
|
|
Red int8 `json:"red"`
|
|
Green int8 `json:"green"`
|
|
Blue int8 `json:"blue"`
|
|
HighlightRed int8 `json:"highlight_red"`
|
|
HighlightGreen int8 `json:"highlight_green"`
|
|
HighlightBlue int8 `json:"highlight_blue"`
|
|
}
|
|
|
|
// GenericInfo contains general item information
|
|
type GenericInfo struct {
|
|
ShowName int8 `json:"show_name"`
|
|
CreatorFlag int8 `json:"creator_flag"`
|
|
ItemFlags int16 `json:"item_flags"`
|
|
ItemFlags2 int16 `json:"item_flags2"`
|
|
Condition int8 `json:"condition"`
|
|
Weight int32 `json:"weight"`
|
|
SkillReq1 int32 `json:"skill_req1"`
|
|
SkillReq2 int32 `json:"skill_req2"`
|
|
SkillMin int16 `json:"skill_min"`
|
|
ItemType int8 `json:"item_type"`
|
|
AppearanceID int16 `json:"appearance_id"`
|
|
AppearanceRed int8 `json:"appearance_red"`
|
|
AppearanceGreen int8 `json:"appearance_green"`
|
|
AppearanceBlue int8 `json:"appearance_blue"`
|
|
AppearanceHighlightRed int8 `json:"appearance_highlight_red"`
|
|
AppearanceHighlightGreen int8 `json:"appearance_highlight_green"`
|
|
AppearanceHighlightBlue int8 `json:"appearance_highlight_blue"`
|
|
Collectable int8 `json:"collectable"`
|
|
OffersQuestID int32 `json:"offers_quest_id"`
|
|
PartOfQuestID int32 `json:"part_of_quest_id"`
|
|
MaxCharges int16 `json:"max_charges"`
|
|
DisplayCharges int8 `json:"display_charges"`
|
|
AdventureClasses int64 `json:"adventure_classes"`
|
|
TradeskillClasses int64 `json:"tradeskill_classes"`
|
|
AdventureDefaultLevel int16 `json:"adventure_default_level"`
|
|
TradeskillDefaultLevel int16 `json:"tradeskill_default_level"`
|
|
Usable int8 `json:"usable"`
|
|
Harvest int8 `json:"harvest"`
|
|
BodyDrop int8 `json:"body_drop"`
|
|
PvPDescription int8 `json:"pvp_description"`
|
|
MercOnly int8 `json:"merc_only"`
|
|
MountOnly int8 `json:"mount_only"`
|
|
SetID int32 `json:"set_id"`
|
|
CollectableUnk int8 `json:"collectable_unk"`
|
|
OffersQuestName string `json:"offers_quest_name"`
|
|
RequiredByQuestName string `json:"required_by_quest_name"`
|
|
TransmutedMaterial int8 `json:"transmuted_material"`
|
|
}
|
|
|
|
// ArmorInfo contains armor-specific information
|
|
type ArmorInfo struct {
|
|
MitigationLow int16 `json:"mitigation_low"`
|
|
MitigationHigh int16 `json:"mitigation_high"`
|
|
}
|
|
|
|
// WeaponInfo contains weapon-specific information
|
|
type WeaponInfo struct {
|
|
WieldType int16 `json:"wield_type"`
|
|
DamageLow1 int16 `json:"damage_low1"`
|
|
DamageHigh1 int16 `json:"damage_high1"`
|
|
DamageLow2 int16 `json:"damage_low2"`
|
|
DamageHigh2 int16 `json:"damage_high2"`
|
|
DamageLow3 int16 `json:"damage_low3"`
|
|
DamageHigh3 int16 `json:"damage_high3"`
|
|
Delay int16 `json:"delay"`
|
|
Rating float32 `json:"rating"`
|
|
}
|
|
|
|
// BagInfo contains bag-specific information
|
|
type BagInfo struct {
|
|
NumSlots int8 `json:"num_slots"`
|
|
WeightReduction int16 `json:"weight_reduction"`
|
|
}
|
|
|
|
// FoodInfo contains food/drink information
|
|
type FoodInfo struct {
|
|
Type int8 `json:"type"`
|
|
Level int8 `json:"level"`
|
|
Duration float32 `json:"duration"`
|
|
Satiation int8 `json:"satiation"`
|
|
}
|
|
|
|
// BookInfo contains book-specific information
|
|
type BookInfo struct {
|
|
Language int8 `json:"language"`
|
|
Author string `json:"author"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
// BookPage represents a book page
|
|
type BookPage struct {
|
|
Page int8 `json:"page"`
|
|
PageText string `json:"page_text"`
|
|
VAlign int8 `json:"valign"`
|
|
HAlign int8 `json:"halign"`
|
|
}
|
|
|
|
// Additional item type info structures
|
|
|
|
// RangedInfo contains ranged weapon information
|
|
type RangedInfo struct {
|
|
WeaponInfo WeaponInfo `json:"weapon_info"`
|
|
RangeLow int16 `json:"range_low"`
|
|
RangeHigh int16 `json:"range_high"`
|
|
}
|
|
|
|
// AdornmentInfo contains adornment-specific information
|
|
type AdornmentInfo struct {
|
|
Duration float32 `json:"duration"`
|
|
ItemTypes int16 `json:"item_types"`
|
|
SlotType int16 `json:"slot_type"`
|
|
}
|
|
|
|
// BaubleInfo contains bauble-specific information
|
|
type BaubleInfo struct {
|
|
Cast int16 `json:"cast"`
|
|
Recovery int16 `json:"recovery"`
|
|
Duration int32 `json:"duration"`
|
|
Recast float32 `json:"recast"`
|
|
DisplaySlotOptional int8 `json:"display_slot_optional"`
|
|
DisplayCastTime int8 `json:"display_cast_time"`
|
|
DisplayBaubleType int8 `json:"display_bauble_type"`
|
|
EffectRadius float32 `json:"effect_radius"`
|
|
MaxAOETargets int32 `json:"max_aoe_targets"`
|
|
DisplayUntilCancelled int8 `json:"display_until_cancelled"`
|
|
}
|
|
|
|
// HouseItemInfo contains house item information
|
|
type HouseItemInfo struct {
|
|
StatusRentReduction int32 `json:"status_rent_reduction"`
|
|
CoinRentReduction float32 `json:"coin_rent_reduction"`
|
|
HouseOnly int8 `json:"house_only"`
|
|
HouseLocation int8 `json:"house_location"`
|
|
}
|
|
|
|
// HouseContainerInfo contains house container information
|
|
type HouseContainerInfo struct {
|
|
AllowedTypes int64 `json:"allowed_types"`
|
|
NumSlots int8 `json:"num_slots"`
|
|
BrokerCommission int8 `json:"broker_commission"`
|
|
FenceCommission int8 `json:"fence_commission"`
|
|
}
|
|
|
|
// SkillInfo contains skill book information
|
|
type SkillInfo struct {
|
|
SpellID int32 `json:"spell_id"`
|
|
SpellTier int32 `json:"spell_tier"`
|
|
}
|
|
|
|
// RecipeBookInfo contains recipe book information
|
|
type RecipeBookInfo struct {
|
|
Recipes []uint32 `json:"recipes"`
|
|
RecipeID int32 `json:"recipe_id"`
|
|
Uses int8 `json:"uses"`
|
|
}
|
|
|
|
// ItemSetInfo contains item set information
|
|
type ItemSetInfo struct {
|
|
ItemID int32 `json:"item_id"`
|
|
ItemCRC int32 `json:"item_crc"`
|
|
ItemIcon int16 `json:"item_icon"`
|
|
ItemStackSize int32 `json:"item_stack_size"`
|
|
ItemListColor int32 `json:"item_list_color"`
|
|
SOEItemIDUnsigned int32 `json:"soe_item_id_unsigned"`
|
|
SOEItemCRCUnsigned int32 `json:"soe_item_crc_unsigned"`
|
|
}
|
|
|
|
// ThrownInfo contains thrown weapon information
|
|
type ThrownInfo struct {
|
|
Range int32 `json:"range"`
|
|
DamageModifier int32 `json:"damage_modifier"`
|
|
HitBonus float32 `json:"hit_bonus"`
|
|
DamageType int32 `json:"damage_type"`
|
|
}
|
|
|
|
// ItemStatString represents a string-based item stat
|
|
type ItemStatString struct {
|
|
StatString string `json:"stat_string"`
|
|
}
|
|
|
|
// ItemEffect represents an item effect
|
|
type ItemEffect struct {
|
|
Effect string `json:"effect"`
|
|
Percentage int8 `json:"percentage"`
|
|
SubBulletFlag int8 `json:"sub_bullet_flag"`
|
|
}
|
|
|
|
// Item represents a complete item with all properties
|
|
type Item struct {
|
|
LowerName string `json:"lower_name"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
StackCount int16 `json:"stack_count"`
|
|
SellPrice int32 `json:"sell_price"`
|
|
SellStatus int32 `json:"sell_status"`
|
|
MaxSellValue int32 `json:"max_sell_value"`
|
|
BrokerPrice int64 `json:"broker_price"`
|
|
IsSearchStoreItem bool `json:"is_search_store_item"`
|
|
IsSearchInInventory bool `json:"is_search_in_inventory"`
|
|
SaveNeeded bool `json:"save_needed"`
|
|
NoBuyBack bool `json:"no_buy_back"`
|
|
NoSale bool `json:"no_sale"`
|
|
NeedsDeletion bool `json:"needs_deletion"`
|
|
Crafted bool `json:"crafted"`
|
|
Tinkered bool `json:"tinkered"`
|
|
WeaponType int8 `json:"weapon_type"`
|
|
Adornment string `json:"adornment"`
|
|
Creator string `json:"creator"`
|
|
SellerName string `json:"seller_name"`
|
|
SellerCharID int32 `json:"seller_char_id"`
|
|
SellerHouseID int64 `json:"seller_house_id"`
|
|
Created time.Time `json:"created"`
|
|
GroupedCharIDs map[int32]bool `json:"grouped_char_ids"`
|
|
EffectType ItemEffectType `json:"effect_type"`
|
|
BookLanguage int8 `json:"book_language"`
|
|
Adorn0 int32 `json:"adorn0"`
|
|
Adorn1 int32 `json:"adorn1"`
|
|
Adorn2 int32 `json:"adorn2"`
|
|
SpellID int32 `json:"spell_id"`
|
|
SpellTier int8 `json:"spell_tier"`
|
|
ItemScript string `json:"item_script"`
|
|
Classifications []*Classifications `json:"classifications"`
|
|
ItemStats []*ItemStat `json:"item_stats"`
|
|
ItemSets []*ItemSet `json:"item_sets"`
|
|
ItemStringStats []*ItemStatString `json:"item_string_stats"`
|
|
ItemLevelOverrides []*ItemLevelOverride `json:"item_level_overrides"`
|
|
ItemEffects []*ItemEffect `json:"item_effects"`
|
|
BookPages []*BookPage `json:"book_pages"`
|
|
SlotData []int8 `json:"slot_data"`
|
|
Details ItemCore `json:"details"`
|
|
GenericInfo GenericInfo `json:"generic_info"`
|
|
WeaponInfo *WeaponInfo `json:"weapon_info,omitempty"`
|
|
RangedInfo *RangedInfo `json:"ranged_info,omitempty"`
|
|
ArmorInfo *ArmorInfo `json:"armor_info,omitempty"`
|
|
AdornmentInfo *AdornmentInfo `json:"adornment_info,omitempty"`
|
|
BagInfo *BagInfo `json:"bag_info,omitempty"`
|
|
FoodInfo *FoodInfo `json:"food_info,omitempty"`
|
|
BaubleInfo *BaubleInfo `json:"bauble_info,omitempty"`
|
|
BookInfo *BookInfo `json:"book_info,omitempty"`
|
|
HouseItemInfo *HouseItemInfo `json:"house_item_info,omitempty"`
|
|
HouseContainerInfo *HouseContainerInfo `json:"house_container_info,omitempty"`
|
|
SkillInfo *SkillInfo `json:"skill_info,omitempty"`
|
|
RecipeBookInfo *RecipeBookInfo `json:"recipe_book_info,omitempty"`
|
|
ItemSetInfo *ItemSetInfo `json:"item_set_info,omitempty"`
|
|
ThrownInfo *ThrownInfo `json:"thrown_info,omitempty"`
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// VersionRange represents a version range for broker item mapping
|
|
type VersionRange struct {
|
|
MinVersion int32 `json:"min_version"`
|
|
MaxVersion int32 `json:"max_version"`
|
|
}
|
|
|
|
// MasterItemList manages all items in the game
|
|
type MasterItemList struct {
|
|
items map[int32]*Item `json:"items"`
|
|
mappedItemStatsStrings map[string]int32 `json:"mapped_item_stats_strings"`
|
|
mappedItemStatTypeIDs map[int32]string `json:"mapped_item_stat_type_ids"`
|
|
brokerItemMap map[*VersionRange]map[int64]int64 `json:"-"`
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// PlayerItemList manages a player's inventory
|
|
type PlayerItemList struct {
|
|
maxSavedIndex int32 `json:"max_saved_index"`
|
|
indexedItems map[int32]*Item `json:"indexed_items"`
|
|
items map[int32]map[int8]map[int16]*Item `json:"items"`
|
|
overflowItems []*Item `json:"overflow_items"`
|
|
packetCount int16 `json:"packet_count"`
|
|
xorPacket []byte `json:"-"`
|
|
origPacket []byte `json:"-"`
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// EquipmentItemList manages equipped items
|
|
type EquipmentItemList struct {
|
|
items [NumSlots]*Item `json:"items"`
|
|
appearanceType int8 `json:"appearance_type"`
|
|
xorPacket []byte `json:"-"`
|
|
origPacket []byte `json:"-"`
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// ItemManagerStats represents statistics about item management
|
|
type ItemManagerStats struct {
|
|
TotalItems int32 `json:"total_items"`
|
|
ItemsByType map[int8]int32 `json:"items_by_type"`
|
|
ItemsByTier map[int8]int32 `json:"items_by_tier"`
|
|
PlayersWithItems int32 `json:"players_with_items"`
|
|
TotalItemInstances int64 `json:"total_item_instances"`
|
|
AverageItemsPerPlayer float32 `json:"average_items_per_player"`
|
|
LastUpdate time.Time `json:"last_update"`
|
|
}
|
|
|
|
|
|
// External integration interfaces (simplified from interfaces.go)
|
|
type Logger interface {
|
|
LogDebug(system, format string, args ...any)
|
|
LogInfo(system, format string, args ...any)
|
|
LogWarning(system, format string, args ...any)
|
|
LogError(system, format string, args ...any)
|
|
}
|
|
|
|
// Item Management System
|
|
|
|
// ItemManager manages the complete item system
|
|
type ItemManager struct {
|
|
mu sync.RWMutex
|
|
database *database.Database
|
|
logger Logger
|
|
masterList *MasterItemList
|
|
playerLists map[uint32]*PlayerItemList
|
|
equipmentLists map[uint32]*EquipmentItemList
|
|
|
|
// System state
|
|
loaded bool
|
|
|
|
// Statistics
|
|
stats ItemManagerStats
|
|
}
|
|
|
|
// NewItemManager creates a new item manager
|
|
func NewItemManager(database *database.Database) *ItemManager {
|
|
return &ItemManager{
|
|
database: database,
|
|
masterList: NewMasterItemList(),
|
|
playerLists: make(map[uint32]*PlayerItemList),
|
|
equipmentLists: make(map[uint32]*EquipmentItemList),
|
|
}
|
|
}
|
|
|
|
// SetLogger sets the logger for the manager
|
|
func (im *ItemManager) SetLogger(logger Logger) {
|
|
im.mu.Lock()
|
|
defer im.mu.Unlock()
|
|
im.logger = logger
|
|
}
|
|
|
|
// Initialize loads all items from the database
|
|
func (im *ItemManager) Initialize() error {
|
|
im.mu.Lock()
|
|
defer im.mu.Unlock()
|
|
|
|
if err := im.masterList.LoadFromDatabase(im.database); err != nil {
|
|
return fmt.Errorf("failed to load items from database: %w", err)
|
|
}
|
|
|
|
im.loaded = true
|
|
|
|
if im.logger != nil {
|
|
itemCount := im.masterList.GetItemCount()
|
|
im.logger.LogInfo("items", "Initialized item system with %d items", itemCount)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMasterList returns the master item list
|
|
func (im *ItemManager) GetMasterList() *MasterItemList {
|
|
im.mu.RLock()
|
|
defer im.mu.RUnlock()
|
|
return im.masterList
|
|
}
|
|
|
|
// GetPlayerInventory gets or loads a player's inventory
|
|
func (im *ItemManager) GetPlayerInventory(playerID uint32) (*PlayerItemList, error) {
|
|
im.mu.Lock()
|
|
defer im.mu.Unlock()
|
|
|
|
if itemList, exists := im.playerLists[playerID]; exists {
|
|
return itemList, nil
|
|
}
|
|
|
|
// Load from database
|
|
itemList, err := im.loadPlayerItems(playerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if itemList == nil {
|
|
itemList = NewPlayerItemList()
|
|
}
|
|
|
|
im.playerLists[playerID] = itemList
|
|
return itemList, nil
|
|
}
|
|
|
|
// GetPlayerEquipment gets or loads a player's equipment
|
|
func (im *ItemManager) GetPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error) {
|
|
im.mu.Lock()
|
|
defer im.mu.Unlock()
|
|
|
|
key := uint32(playerID)*10 + uint32(appearanceType)
|
|
if equipment, exists := im.equipmentLists[key]; exists {
|
|
return equipment, nil
|
|
}
|
|
|
|
// Load from database
|
|
equipment, err := im.loadPlayerEquipment(playerID, appearanceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if equipment == nil {
|
|
equipment = NewEquipmentItemList()
|
|
equipment.SetAppearanceType(appearanceType)
|
|
}
|
|
|
|
im.equipmentLists[key] = equipment
|
|
return equipment, nil
|
|
}
|
|
|
|
// GiveItemToPlayer gives an item to a player
|
|
func (im *ItemManager) GiveItemToPlayer(playerID uint32, itemID int32, quantity int16) error {
|
|
itemTemplate := im.masterList.GetItem(itemID)
|
|
if itemTemplate == nil {
|
|
return ErrItemNotFound
|
|
}
|
|
|
|
inventory, err := im.GetPlayerInventory(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := NewItemFromTemplate(itemTemplate)
|
|
item.Details.Count = quantity
|
|
|
|
if !inventory.AddItem(item) {
|
|
return ErrInsufficientSpace
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveItemFromPlayer removes an item from a player
|
|
func (im *ItemManager) RemoveItemFromPlayer(playerID uint32, uniqueID int64, quantity int16) error {
|
|
inventory, err := im.GetPlayerInventory(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := inventory.GetItemFromUniqueID(uniqueID, true, true)
|
|
if item == nil {
|
|
return ErrItemNotFound
|
|
}
|
|
|
|
if item.IsItemLocked() {
|
|
return ErrItemLocked
|
|
}
|
|
|
|
if item.Details.Count <= quantity {
|
|
inventory.RemoveItem(item, true, true)
|
|
} else {
|
|
item.Details.Count -= quantity
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EquipItem equips an item for a player
|
|
func (im *ItemManager) EquipItem(playerID uint32, uniqueID int64, slot int8, appearanceType int8) error {
|
|
inventory, err := im.GetPlayerInventory(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
equipment, err := im.GetPlayerEquipment(playerID, appearanceType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := inventory.GetItemFromUniqueID(uniqueID, false, true)
|
|
if item == nil {
|
|
return ErrItemNotFound
|
|
}
|
|
|
|
if !equipment.CanItemBeEquippedInSlot(item, slot) {
|
|
return ErrCannotEquip
|
|
}
|
|
|
|
inventory.RemoveItem(item, false, true)
|
|
|
|
currentItem := equipment.GetItem(slot)
|
|
if currentItem != nil {
|
|
equipment.RemoveItem(slot, false)
|
|
inventory.AddItem(currentItem)
|
|
}
|
|
|
|
equipment.SetItem(slot, item, false)
|
|
return nil
|
|
}
|
|
|
|
// UnequipItem unequips an item for a player
|
|
func (im *ItemManager) UnequipItem(playerID uint32, slot int8, appearanceType int8) error {
|
|
inventory, err := im.GetPlayerInventory(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
equipment, err := im.GetPlayerEquipment(playerID, appearanceType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := equipment.GetItem(slot)
|
|
if item == nil {
|
|
return ErrItemNotFound
|
|
}
|
|
|
|
if item.IsItemLocked() {
|
|
return ErrItemLocked
|
|
}
|
|
|
|
equipment.RemoveItem(slot, false)
|
|
|
|
if !inventory.AddItem(item) {
|
|
inventory.AddOverflowItem(item)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStatistics returns current system statistics
|
|
func (im *ItemManager) GetStatistics() *ItemManagerStats {
|
|
im.mu.RLock()
|
|
defer im.mu.RUnlock()
|
|
|
|
masterStats := im.masterList.GetStats()
|
|
|
|
return &ItemManagerStats{
|
|
TotalItems: masterStats.TotalItems,
|
|
ItemsByType: masterStats.ItemsByType,
|
|
ItemsByTier: masterStats.ItemsByTier,
|
|
PlayersWithItems: int32(len(im.playerLists)),
|
|
TotalItemInstances: im.calculateTotalItemInstances(),
|
|
AverageItemsPerPlayer: im.calculateAverageItemsPerPlayer(),
|
|
LastUpdate: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Database operations
|
|
func (im *ItemManager) loadPlayerItems(playerID uint32) (*PlayerItemList, error) {
|
|
query := `SELECT bag_id, slot_id, item_id, creator, adorn0, adorn1, adorn2, count, condition,
|
|
attuned, price, sell_price, sell_status, max_sell_value, no_sale, no_buy_back,
|
|
crafted, tinkered, lock_flags, unique_id
|
|
FROM character_items WHERE character_id = ? ORDER BY bag_id, slot_id`
|
|
|
|
rows, err := im.database.Query(query, playerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
itemList := NewPlayerItemList()
|
|
|
|
for rows.Next() {
|
|
var bagID, slotID, itemID, adorn0, adorn1, adorn2 int32
|
|
var count, condition int16
|
|
var attuned int8
|
|
var price, sellPrice, sellStatus, maxSellValue int32
|
|
var noSale, noBuyBack, crafted, tinkered bool
|
|
var lockFlags int32
|
|
var uniqueID int64
|
|
var creator string
|
|
|
|
err := rows.Scan(&bagID, &slotID, &itemID, &creator, &adorn0, &adorn1, &adorn2,
|
|
&count, &condition, &attuned, &price, &sellPrice, &sellStatus, &maxSellValue,
|
|
&noSale, &noBuyBack, &crafted, &tinkered, &lockFlags, &uniqueID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
itemTemplate := im.masterList.GetItem(itemID)
|
|
if itemTemplate == nil {
|
|
if im.logger != nil {
|
|
im.logger.LogWarning("items", "Item template %d not found for player %d", itemID, playerID)
|
|
}
|
|
continue
|
|
}
|
|
|
|
item := NewItemFromTemplate(itemTemplate)
|
|
item.Details.BagID = bagID
|
|
item.Details.SlotID = int16(slotID)
|
|
item.Details.Count = count
|
|
item.Details.UniqueID = uniqueID
|
|
item.GenericInfo.Condition = int8(condition)
|
|
item.Creator = creator
|
|
item.Adorn0 = adorn0
|
|
item.Adorn1 = adorn1
|
|
item.Adorn2 = adorn2
|
|
item.SellPrice = sellPrice
|
|
item.SellStatus = sellStatus
|
|
item.MaxSellValue = maxSellValue
|
|
item.NoSale = noSale
|
|
item.NoBuyBack = noBuyBack
|
|
item.Crafted = crafted
|
|
item.Tinkered = tinkered
|
|
item.Details.LockFlags = lockFlags
|
|
|
|
itemList.AddItem(item)
|
|
}
|
|
|
|
return itemList, rows.Err()
|
|
}
|
|
|
|
func (im *ItemManager) loadPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error) {
|
|
query := `SELECT slot_id, item_id, creator, adorn0, adorn1, adorn2, count, condition,
|
|
crafted, tinkered, unique_id
|
|
FROM character_equipment WHERE character_id = ? AND appearance_type = ?`
|
|
|
|
rows, err := im.database.Query(query, playerID, appearanceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
equipment := NewEquipmentItemList()
|
|
equipment.SetAppearanceType(appearanceType)
|
|
|
|
for rows.Next() {
|
|
var slotID, itemID, adorn0, adorn1, adorn2 int32
|
|
var count, condition int16
|
|
var crafted, tinkered bool
|
|
var uniqueID int64
|
|
var creator string
|
|
|
|
err := rows.Scan(&slotID, &itemID, &creator, &adorn0, &adorn1, &adorn2,
|
|
&count, &condition, &crafted, &tinkered, &uniqueID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
itemTemplate := im.masterList.GetItem(itemID)
|
|
if itemTemplate == nil {
|
|
if im.logger != nil {
|
|
im.logger.LogWarning("items", "Equipment item template %d not found for player %d", itemID, playerID)
|
|
}
|
|
continue
|
|
}
|
|
|
|
item := NewItemFromTemplate(itemTemplate)
|
|
item.Details.Count = count
|
|
item.Details.UniqueID = uniqueID
|
|
item.GenericInfo.Condition = int8(condition)
|
|
item.Creator = creator
|
|
item.Adorn0 = adorn0
|
|
item.Adorn1 = adorn1
|
|
item.Adorn2 = adorn2
|
|
item.Crafted = crafted
|
|
item.Tinkered = tinkered
|
|
|
|
equipment.SetItem(int8(slotID), item, false)
|
|
}
|
|
|
|
return equipment, rows.Err()
|
|
}
|
|
|
|
func (im *ItemManager) calculateTotalItemInstances() int64 {
|
|
total := int64(0)
|
|
for _, itemList := range im.playerLists {
|
|
total += int64(itemList.GetNumberOfItems())
|
|
}
|
|
for _, equipment := range im.equipmentLists {
|
|
total += int64(equipment.GetNumberOfItems())
|
|
}
|
|
return total
|
|
}
|
|
|
|
func (im *ItemManager) calculateAverageItemsPerPlayer() float32 {
|
|
if len(im.playerLists) == 0 {
|
|
return 0
|
|
}
|
|
return float32(im.calculateTotalItemInstances()) / float32(len(im.playerLists))
|
|
}
|
|
|
|
// MasterItemList Methods
|
|
|
|
// NewMasterItemList creates a new master item list
|
|
func NewMasterItemList() *MasterItemList {
|
|
mil := &MasterItemList{
|
|
items: make(map[int32]*Item),
|
|
mappedItemStatsStrings: make(map[string]int32),
|
|
mappedItemStatTypeIDs: make(map[int32]string),
|
|
brokerItemMap: make(map[*VersionRange]map[int64]int64),
|
|
}
|
|
|
|
mil.initializeMappedStats()
|
|
return mil
|
|
}
|
|
|
|
// initializeMappedStats initializes the mapped item stats (preserves C++ functionality)
|
|
func (mil *MasterItemList) initializeMappedStats() {
|
|
// Basic stats
|
|
mil.AddMappedItemStat(ItemStatStr, "strength")
|
|
mil.AddMappedItemStat(ItemStatSta, "stamina")
|
|
mil.AddMappedItemStat(ItemStatAgi, "agility")
|
|
mil.AddMappedItemStat(ItemStatWis, "wisdom")
|
|
mil.AddMappedItemStat(ItemStatInt, "intelligence")
|
|
|
|
// Skills
|
|
mil.AddMappedItemStat(ItemStatAdorning, "adorning")
|
|
mil.AddMappedItemStat(ItemStatAggression, "aggression")
|
|
mil.AddMappedItemStat(ItemStatArtificing, "artificing")
|
|
mil.AddMappedItemStat(ItemStatArtistry, "artistry")
|
|
mil.AddMappedItemStat(ItemStatChemistry, "chemistry")
|
|
mil.AddMappedItemStat(ItemStatCrushing, "crushing")
|
|
mil.AddMappedItemStat(ItemStatDefense, "defense")
|
|
mil.AddMappedItemStat(ItemStatDeflection, "deflection")
|
|
mil.AddMappedItemStat(ItemStatDisruption, "disruption")
|
|
mil.AddMappedItemStat(ItemStatFishing, "fishing")
|
|
mil.AddMappedItemStat(ItemStatFletching, "fletching")
|
|
mil.AddMappedItemStat(ItemStatFocus, "focus")
|
|
mil.AddMappedItemStat(ItemStatForesting, "foresting")
|
|
mil.AddMappedItemStat(ItemStatGathering, "gathering")
|
|
mil.AddMappedItemStat(ItemStatMetalShaping, "metal shaping")
|
|
mil.AddMappedItemStat(ItemStatMetalworking, "metalworking")
|
|
mil.AddMappedItemStat(ItemStatMining, "mining")
|
|
mil.AddMappedItemStat(ItemStatMinistration, "ministration")
|
|
mil.AddMappedItemStat(ItemStatOrdination, "ordination")
|
|
mil.AddMappedItemStat(ItemStatParry, "parry")
|
|
mil.AddMappedItemStat(ItemStatPiercing, "piercing")
|
|
mil.AddMappedItemStat(ItemStatRanged, "ranged")
|
|
mil.AddMappedItemStat(ItemStatSafeFall, "safe fall")
|
|
mil.AddMappedItemStat(ItemStatScribing, "scribing")
|
|
mil.AddMappedItemStat(ItemStatSculpting, "sculpting")
|
|
mil.AddMappedItemStat(ItemStatSlashing, "slashing")
|
|
mil.AddMappedItemStat(ItemStatSubjugation, "subjugation")
|
|
mil.AddMappedItemStat(ItemStatSwimming, "swimming")
|
|
mil.AddMappedItemStat(ItemStatTailoring, "tailoring")
|
|
mil.AddMappedItemStat(ItemStatTinkering, "tinkering")
|
|
mil.AddMappedItemStat(ItemStatTransmuting, "transmuting")
|
|
mil.AddMappedItemStat(ItemStatTrapping, "trapping")
|
|
mil.AddMappedItemStat(ItemStatWeaponSkills, "weapon skills")
|
|
mil.AddMappedItemStat(ItemStatPowerCostReduction, "power cost reduction")
|
|
mil.AddMappedItemStat(ItemStatSpellAvoidance, "spell avoidance")
|
|
|
|
// Resistances
|
|
mil.AddMappedItemStat(ItemStatVsPhysical, "vs physical")
|
|
mil.AddMappedItemStat(ItemStatVsHeat, "vs elemental")
|
|
mil.AddMappedItemStat(ItemStatVsPoison, "vs noxious")
|
|
mil.AddMappedItemStat(ItemStatVsMagic, "vs arcane")
|
|
mil.AddMappedItemStat(ItemStatVsSlash, "vs slashing")
|
|
mil.AddMappedItemStat(ItemStatVsCrush, "vs crushing")
|
|
mil.AddMappedItemStat(ItemStatVsPierce, "vs piercing")
|
|
mil.AddMappedItemStat(ItemStatVsCold, "vs cold")
|
|
mil.AddMappedItemStat(ItemStatVsMental, "vs mental")
|
|
mil.AddMappedItemStat(ItemStatVsDivine, "vs divine")
|
|
mil.AddMappedItemStat(ItemStatVsDrowning, "vs drowning")
|
|
mil.AddMappedItemStat(ItemStatVsFalling, "vs falling")
|
|
mil.AddMappedItemStat(ItemStatVsPain, "vs pain")
|
|
mil.AddMappedItemStat(ItemStatVsMelee, "vs melee")
|
|
mil.AddMappedItemStat(ItemStatVsDisease, "vs disease")
|
|
|
|
// Pool stats
|
|
mil.AddMappedItemStat(ItemStatHealth, "health")
|
|
mil.AddMappedItemStat(ItemStatPower, "power")
|
|
mil.AddMappedItemStat(ItemStatConcentration, "concentration")
|
|
mil.AddMappedItemStat(ItemStatSavagery, "savagery")
|
|
mil.AddMappedItemStat(ItemStatDissonance, "dissonance")
|
|
|
|
// Advanced stats
|
|
mil.AddMappedItemStat(ItemStatHPRegen, "health regen")
|
|
mil.AddMappedItemStat(ItemStatManaRegen, "power regen")
|
|
mil.AddMappedItemStat(ItemStatMeleeCritChance, "crit chance")
|
|
mil.AddMappedItemStat(ItemStatCritBonus, "crit bonus")
|
|
mil.AddMappedItemStat(ItemStatPotency, "potency")
|
|
mil.AddMappedItemStat(ItemStatStrikethrough, "strikethrough")
|
|
mil.AddMappedItemStat(ItemStatAccuracy, "accuracy")
|
|
mil.AddMappedItemStat(ItemStatDPS, "dps")
|
|
mil.AddMappedItemStat(ItemStatAttackSpeed, "attack speed")
|
|
mil.AddMappedItemStat(ItemStatMultiattackChance, "multi attack chance")
|
|
mil.AddMappedItemStat(ItemStatFlurry, "flurry")
|
|
mil.AddMappedItemStat(ItemStatAEAutoattackChance, "ae autoattack chance")
|
|
}
|
|
|
|
// AddMappedItemStat adds a mapping between stat ID and name
|
|
func (mil *MasterItemList) AddMappedItemStat(id int32, lowerCaseName string) {
|
|
mil.mutex.Lock()
|
|
defer mil.mutex.Unlock()
|
|
|
|
mil.mappedItemStatsStrings[lowerCaseName] = id
|
|
mil.mappedItemStatTypeIDs[id] = lowerCaseName
|
|
}
|
|
|
|
// GetItemStatIDByName gets the stat ID by name
|
|
func (mil *MasterItemList) GetItemStatIDByName(name string) int32 {
|
|
mil.mutex.RLock()
|
|
defer mil.mutex.RUnlock()
|
|
|
|
lowerName := strings.ToLower(name)
|
|
if id, exists := mil.mappedItemStatsStrings[lowerName]; exists {
|
|
return id
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// GetItem gets an item by ID
|
|
func (mil *MasterItemList) GetItem(itemID int32) *Item {
|
|
mil.mutex.RLock()
|
|
defer mil.mutex.RUnlock()
|
|
return mil.items[itemID]
|
|
}
|
|
|
|
// AddItem adds an item to the master list
|
|
func (mil *MasterItemList) AddItem(item *Item) {
|
|
mil.mutex.Lock()
|
|
defer mil.mutex.Unlock()
|
|
mil.items[item.Details.ItemID] = item
|
|
}
|
|
|
|
// GetItemCount returns the number of items
|
|
func (mil *MasterItemList) GetItemCount() int32 {
|
|
mil.mutex.RLock()
|
|
defer mil.mutex.RUnlock()
|
|
return int32(len(mil.items))
|
|
}
|
|
|
|
// LoadFromDatabase loads all items from the database
|
|
func (mil *MasterItemList) LoadFromDatabase(db *database.Database) error {
|
|
query := `SELECT id, name, description, item_type, icon, classic_icon, stack_count, tier,
|
|
recommended_level, sell_price, sell_status, max_sell_value, weight, condition_percent,
|
|
item_flags, item_flags2, skill_req1, skill_req2, skill_min, appearance_id
|
|
FROM items ORDER BY id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
itemCount := 0
|
|
for rows.Next() {
|
|
item := NewItem()
|
|
|
|
err := rows.Scan(
|
|
&item.Details.ItemID,
|
|
&item.Name,
|
|
&item.Description,
|
|
&item.GenericInfo.ItemType,
|
|
&item.Details.Icon,
|
|
&item.Details.ClassicIcon,
|
|
&item.StackCount,
|
|
&item.Details.Tier,
|
|
&item.Details.RecommendedLevel,
|
|
&item.SellPrice,
|
|
&item.SellStatus,
|
|
&item.MaxSellValue,
|
|
&item.GenericInfo.Weight,
|
|
&item.GenericInfo.Condition,
|
|
&item.GenericInfo.ItemFlags,
|
|
&item.GenericInfo.ItemFlags2,
|
|
&item.GenericInfo.SkillReq1,
|
|
&item.GenericInfo.SkillReq2,
|
|
&item.GenericInfo.SkillMin,
|
|
&item.GenericInfo.AppearanceID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item.LowerName = strings.ToLower(item.Name)
|
|
item.Details.UniqueID = NextUniqueID()
|
|
|
|
mil.items[item.Details.ItemID] = item
|
|
itemCount++
|
|
}
|
|
|
|
// Load item stats
|
|
if err := mil.loadItemStats(db); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load type-specific data
|
|
if err := mil.loadWeaponInfo(db); err != nil {
|
|
return err
|
|
}
|
|
if err := mil.loadArmorInfo(db); err != nil {
|
|
return err
|
|
}
|
|
if err := mil.loadBagInfo(db); err != nil {
|
|
return err
|
|
}
|
|
if err := mil.loadFoodInfo(db); err != nil {
|
|
return err
|
|
}
|
|
if err := mil.loadBookInfo(db); err != nil {
|
|
return err
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadItemStats loads item stat bonuses
|
|
func (mil *MasterItemList) loadItemStats(db *database.Database) error {
|
|
query := `SELECT item_id, stat_type, stat_name, value, level FROM item_stats ORDER BY item_id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var itemID, statType int32
|
|
var statName string
|
|
var value float32
|
|
var level int8
|
|
|
|
err := rows.Scan(&itemID, &statType, &statName, &value, &level)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := mil.items[itemID]
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
stat := &ItemStat{
|
|
StatName: statName,
|
|
StatType: statType,
|
|
Value: value,
|
|
Level: level,
|
|
}
|
|
|
|
item.mutex.Lock()
|
|
item.ItemStats = append(item.ItemStats, stat)
|
|
item.mutex.Unlock()
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadWeaponInfo loads weapon-specific information
|
|
func (mil *MasterItemList) loadWeaponInfo(db *database.Database) error {
|
|
query := `SELECT item_id, wield_type, damage_low1, damage_high1, damage_low2, damage_high2,
|
|
damage_low3, damage_high3, delay, rating FROM item_details_weapon ORDER BY item_id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var itemID int32
|
|
var wieldType, damageLow1, damageHigh1, damageLow2, damageHigh2 int16
|
|
var damageLow3, damageHigh3, delay int16
|
|
var rating float32
|
|
|
|
err := rows.Scan(&itemID, &wieldType, &damageLow1, &damageHigh1,
|
|
&damageLow2, &damageHigh2, &damageLow3, &damageHigh3, &delay, &rating)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := mil.items[itemID]
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
item.WeaponInfo = &WeaponInfo{
|
|
WieldType: wieldType,
|
|
DamageLow1: damageLow1,
|
|
DamageHigh1: damageHigh1,
|
|
DamageLow2: damageLow2,
|
|
DamageHigh2: damageHigh2,
|
|
DamageLow3: damageLow3,
|
|
DamageHigh3: damageHigh3,
|
|
Delay: delay,
|
|
Rating: rating,
|
|
}
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadArmorInfo loads armor-specific information
|
|
func (mil *MasterItemList) loadArmorInfo(db *database.Database) error {
|
|
query := `SELECT item_id, mitigation_low, mitigation_high FROM item_details_armor ORDER BY item_id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var itemID int32
|
|
var mitigationLow, mitigationHigh int16
|
|
|
|
err := rows.Scan(&itemID, &mitigationLow, &mitigationHigh)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := mil.items[itemID]
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
item.ArmorInfo = &ArmorInfo{
|
|
MitigationLow: mitigationLow,
|
|
MitigationHigh: mitigationHigh,
|
|
}
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadBagInfo loads bag-specific information
|
|
func (mil *MasterItemList) loadBagInfo(db *database.Database) error {
|
|
query := `SELECT item_id, num_slots, weight_reduction FROM item_details_bag ORDER BY item_id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var itemID int32
|
|
var numSlots int8
|
|
var weightReduction int16
|
|
|
|
err := rows.Scan(&itemID, &numSlots, &weightReduction)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := mil.items[itemID]
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
item.BagInfo = &BagInfo{
|
|
NumSlots: numSlots,
|
|
WeightReduction: weightReduction,
|
|
}
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadFoodInfo loads food/drink information
|
|
func (mil *MasterItemList) loadFoodInfo(db *database.Database) error {
|
|
query := `SELECT item_id, type, level, duration, satiation FROM item_details_food ORDER BY item_id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var itemID int32
|
|
var foodType, level, satiation int8
|
|
var duration float32
|
|
|
|
err := rows.Scan(&itemID, &foodType, &level, &duration, &satiation)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := mil.items[itemID]
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
item.FoodInfo = &FoodInfo{
|
|
Type: foodType,
|
|
Level: level,
|
|
Duration: duration,
|
|
Satiation: satiation,
|
|
}
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadBookInfo loads book information
|
|
func (mil *MasterItemList) loadBookInfo(db *database.Database) error {
|
|
query := `SELECT item_id, language, author, title FROM item_details_book ORDER BY item_id`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var itemID int32
|
|
var language int8
|
|
var author, title string
|
|
|
|
err := rows.Scan(&itemID, &language, &author, &title)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item := mil.items[itemID]
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
item.BookInfo = &BookInfo{
|
|
Language: language,
|
|
Author: author,
|
|
Title: title,
|
|
}
|
|
|
|
// Load book pages
|
|
if err := mil.loadBookPages(db, itemID, item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadBookPages loads book pages
|
|
func (mil *MasterItemList) loadBookPages(db *database.Database, itemID int32, item *Item) error {
|
|
query := `SELECT page, page_text, valign, halign FROM item_details_book_pages
|
|
WHERE item_id = ? ORDER BY page`
|
|
|
|
rows, err := db.Query(query, itemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var page, valign, halign int8
|
|
var pageText string
|
|
|
|
err := rows.Scan(&page, &pageText, &valign, &halign)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bookPage := &BookPage{
|
|
Page: page,
|
|
PageText: pageText,
|
|
VAlign: valign,
|
|
HAlign: halign,
|
|
}
|
|
|
|
item.BookPages = append(item.BookPages, bookPage)
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// GetStats returns master list statistics
|
|
func (mil *MasterItemList) GetStats() *ItemManagerStats {
|
|
mil.mutex.RLock()
|
|
defer mil.mutex.RUnlock()
|
|
|
|
itemsByType := make(map[int8]int32)
|
|
itemsByTier := make(map[int8]int32)
|
|
|
|
for _, item := range mil.items {
|
|
itemsByType[item.GenericInfo.ItemType]++
|
|
itemsByTier[item.Details.Tier]++
|
|
}
|
|
|
|
return &ItemManagerStats{
|
|
TotalItems: int32(len(mil.items)),
|
|
ItemsByType: itemsByType,
|
|
ItemsByTier: itemsByTier,
|
|
LastUpdate: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Item Methods
|
|
|
|
|
|
|
|
// NewPlayerItemList creates a new player item list
|
|
func NewPlayerItemList() *PlayerItemList {
|
|
return &PlayerItemList{
|
|
indexedItems: make(map[int32]*Item),
|
|
items: make(map[int32]map[int8]map[int16]*Item),
|
|
overflowItems: make([]*Item, 0),
|
|
}
|
|
}
|
|
|
|
// AddItem adds an item to the player's inventory
|
|
func (pil *PlayerItemList) AddItem(item *Item) bool {
|
|
pil.mutex.Lock()
|
|
defer pil.mutex.Unlock()
|
|
|
|
if item == nil {
|
|
return false
|
|
}
|
|
|
|
// Try to stack with existing items first
|
|
if item.IsStackable() {
|
|
for _, existingItem := range pil.indexedItems {
|
|
if existingItem.CanStack(item) && existingItem.Details.Count < existingItem.StackCount {
|
|
spaceAvailable := existingItem.StackCount - existingItem.Details.Count
|
|
if spaceAvailable >= item.Details.Count {
|
|
existingItem.Details.Count += item.Details.Count
|
|
return true
|
|
} else {
|
|
existingItem.Details.Count = existingItem.StackCount
|
|
item.Details.Count -= spaceAvailable
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find empty slot
|
|
bagID := item.Details.BagID
|
|
if bagID == 0 {
|
|
bagID = pil.findEmptySlot()
|
|
}
|
|
|
|
if bagID == 0 {
|
|
// Add to overflow
|
|
item.Details.BagID = -2
|
|
item.Details.SlotID = int16(len(pil.overflowItems))
|
|
pil.overflowItems = append(pil.overflowItems, item)
|
|
pil.indexedItems[int32(item.Details.UniqueID)] = item
|
|
return true
|
|
}
|
|
|
|
slotID := pil.findEmptySlotInBag(bagID)
|
|
if slotID == -1 {
|
|
// Bag full, try overflow
|
|
item.Details.BagID = -2
|
|
item.Details.SlotID = int16(len(pil.overflowItems))
|
|
pil.overflowItems = append(pil.overflowItems, item)
|
|
pil.indexedItems[int32(item.Details.UniqueID)] = item
|
|
return true
|
|
}
|
|
|
|
// Place in slot
|
|
item.Details.BagID = bagID
|
|
item.Details.SlotID = slotID
|
|
|
|
if pil.items[bagID] == nil {
|
|
pil.items[bagID] = make(map[int8]map[int16]*Item)
|
|
}
|
|
if pil.items[bagID][int8(item.Details.AppearanceType)] == nil {
|
|
pil.items[bagID][int8(item.Details.AppearanceType)] = make(map[int16]*Item)
|
|
}
|
|
|
|
pil.items[bagID][int8(item.Details.AppearanceType)][slotID] = item
|
|
pil.indexedItems[int32(item.Details.UniqueID)] = item
|
|
|
|
return true
|
|
}
|
|
|
|
// RemoveItem removes an item from the player's inventory
|
|
func (pil *PlayerItemList) RemoveItem(item *Item, removeBuyBack bool, removeBuyBackOnlyIfCrafted bool) bool {
|
|
pil.mutex.Lock()
|
|
defer pil.mutex.Unlock()
|
|
|
|
if item == nil {
|
|
return false
|
|
}
|
|
|
|
// Remove from indexed items
|
|
delete(pil.indexedItems, int32(item.Details.UniqueID))
|
|
|
|
// Remove from location
|
|
if item.Details.BagID == -2 {
|
|
// Remove from overflow
|
|
for i, overflowItem := range pil.overflowItems {
|
|
if overflowItem.Details.UniqueID == item.Details.UniqueID {
|
|
pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...)
|
|
return true
|
|
}
|
|
}
|
|
} else {
|
|
// Remove from regular slot
|
|
if pil.items[item.Details.BagID] != nil &&
|
|
pil.items[item.Details.BagID][int8(item.Details.AppearanceType)] != nil {
|
|
delete(pil.items[item.Details.BagID][int8(item.Details.AppearanceType)], item.Details.SlotID)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetItem gets an item from a specific location
|
|
func (pil *PlayerItemList) GetItem(bagID int32, slotID int16, appearanceType int8) *Item {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
|
|
if bagID == -2 {
|
|
if int(slotID) < len(pil.overflowItems) {
|
|
return pil.overflowItems[slotID]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if pil.items[bagID] != nil && pil.items[bagID][appearanceType] != nil {
|
|
return pil.items[bagID][appearanceType][slotID]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetItemFromUniqueID gets an item by unique ID
|
|
func (pil *PlayerItemList) GetItemFromUniqueID(uniqueID int64, includeBankItems bool, includeEquippedItems bool) *Item {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
|
|
return pil.indexedItems[int32(uniqueID)]
|
|
}
|
|
|
|
// AddOverflowItem adds an item to overflow
|
|
func (pil *PlayerItemList) AddOverflowItem(item *Item) {
|
|
pil.mutex.Lock()
|
|
defer pil.mutex.Unlock()
|
|
|
|
item.Details.BagID = -2
|
|
item.Details.SlotID = int16(len(pil.overflowItems))
|
|
pil.overflowItems = append(pil.overflowItems, item)
|
|
pil.indexedItems[int32(item.Details.UniqueID)] = item
|
|
}
|
|
|
|
// GetOverflowItemList returns overflow items
|
|
func (pil *PlayerItemList) GetOverflowItemList() []*Item {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
|
|
result := make([]*Item, len(pil.overflowItems))
|
|
copy(result, pil.overflowItems)
|
|
return result
|
|
}
|
|
|
|
// MoveItem moves an item to a new location
|
|
func (pil *PlayerItemList) MoveItem(item *Item, newBagID int32, newSlotID int16, newAppearanceType int8, sendInventoryUpdate bool) bool {
|
|
pil.mutex.Lock()
|
|
defer pil.mutex.Unlock()
|
|
|
|
if item == nil {
|
|
return false
|
|
}
|
|
|
|
// Remove from current location
|
|
if item.Details.BagID == -2 {
|
|
for i, overflowItem := range pil.overflowItems {
|
|
if overflowItem.Details.UniqueID == item.Details.UniqueID {
|
|
pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
} else if pil.items[item.Details.BagID] != nil &&
|
|
pil.items[item.Details.BagID][int8(item.Details.AppearanceType)] != nil {
|
|
delete(pil.items[item.Details.BagID][int8(item.Details.AppearanceType)], item.Details.SlotID)
|
|
}
|
|
|
|
// Place in new location
|
|
item.Details.BagID = newBagID
|
|
item.Details.SlotID = newSlotID
|
|
item.Details.AppearanceType = int16(newAppearanceType)
|
|
|
|
if newBagID == -2 {
|
|
pil.overflowItems = append(pil.overflowItems, item)
|
|
} else {
|
|
if pil.items[newBagID] == nil {
|
|
pil.items[newBagID] = make(map[int8]map[int16]*Item)
|
|
}
|
|
if pil.items[newBagID][newAppearanceType] == nil {
|
|
pil.items[newBagID][newAppearanceType] = make(map[int16]*Item)
|
|
}
|
|
pil.items[newBagID][newAppearanceType][newSlotID] = item
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// GetNumberOfItems returns the total number of items
|
|
func (pil *PlayerItemList) GetNumberOfItems() int32 {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
return int32(len(pil.indexedItems))
|
|
}
|
|
|
|
// GetNumberOfFreeSlots returns the number of free inventory slots
|
|
func (pil *PlayerItemList) GetNumberOfFreeSlots() int32 {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
|
|
totalSlots := int32(NumInvSlots * ClassicEQMaxBagSlots) // Basic calculation
|
|
usedSlots := int32(0)
|
|
|
|
for _, bagItems := range pil.items {
|
|
for _, appearanceItems := range bagItems {
|
|
usedSlots += int32(len(appearanceItems))
|
|
}
|
|
}
|
|
|
|
return totalSlots - usedSlots
|
|
}
|
|
|
|
// GetWeight returns the total weight of all items
|
|
func (pil *PlayerItemList) GetWeight() int32 {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
|
|
totalWeight := int32(0)
|
|
for _, item := range pil.indexedItems {
|
|
totalWeight += item.GetWeight()
|
|
}
|
|
|
|
return totalWeight
|
|
}
|
|
|
|
// GetAllItems returns all items in the inventory
|
|
func (pil *PlayerItemList) GetAllItems() []*Item {
|
|
pil.mutex.RLock()
|
|
defer pil.mutex.RUnlock()
|
|
|
|
result := make([]*Item, 0, len(pil.indexedItems))
|
|
for _, item := range pil.indexedItems {
|
|
result = append(result, item)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// findEmptySlot finds an empty bag slot
|
|
func (pil *PlayerItemList) findEmptySlot() int32 {
|
|
// Start with basic inventory slots
|
|
for bagID := int32(InvSlot1); bagID <= InvSlot6; bagID += 50 {
|
|
if pil.items[bagID] == nil || len(pil.items[bagID][BaseEquipment]) == 0 {
|
|
return bagID
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// findEmptySlotInBag finds an empty slot within a bag
|
|
func (pil *PlayerItemList) findEmptySlotInBag(bagID int32) int16 {
|
|
if pil.items[bagID] == nil || pil.items[bagID][BaseEquipment] == nil {
|
|
return 0
|
|
}
|
|
|
|
maxSlots := int16(ClassicEQMaxBagSlots)
|
|
for slotID := int16(0); slotID < maxSlots; slotID++ {
|
|
if pil.items[bagID][BaseEquipment][slotID] == nil {
|
|
return slotID
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// EquipmentItemList Methods
|
|
|
|
// NewEquipmentItemList creates a new equipment item list
|
|
func NewEquipmentItemList() *EquipmentItemList {
|
|
return &EquipmentItemList{
|
|
items: [NumSlots]*Item{},
|
|
}
|
|
}
|
|
|
|
// SetAppearanceType sets the appearance type for this equipment list
|
|
func (eil *EquipmentItemList) SetAppearanceType(appearanceType int8) {
|
|
eil.mutex.Lock()
|
|
defer eil.mutex.Unlock()
|
|
eil.appearanceType = appearanceType
|
|
}
|
|
|
|
// GetItem gets an item from a slot
|
|
func (eil *EquipmentItemList) GetItem(slot int8) *Item {
|
|
eil.mutex.RLock()
|
|
defer eil.mutex.RUnlock()
|
|
|
|
if slot < 0 || int(slot) >= len(eil.items) {
|
|
return nil
|
|
}
|
|
|
|
return eil.items[slot]
|
|
}
|
|
|
|
// SetItem sets an item in a slot
|
|
func (eil *EquipmentItemList) SetItem(slot int8, item *Item, sendEquipUpdate bool) bool {
|
|
eil.mutex.Lock()
|
|
defer eil.mutex.Unlock()
|
|
|
|
if slot < 0 || int(slot) >= len(eil.items) {
|
|
return false
|
|
}
|
|
|
|
eil.items[slot] = item
|
|
if item != nil {
|
|
item.Details.EquipSlotID = int16(slot)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// RemoveItem removes an item from a slot
|
|
func (eil *EquipmentItemList) RemoveItem(slot int8, sendEquipUpdate bool) *Item {
|
|
eil.mutex.Lock()
|
|
defer eil.mutex.Unlock()
|
|
|
|
if slot < 0 || int(slot) >= len(eil.items) {
|
|
return nil
|
|
}
|
|
|
|
item := eil.items[slot]
|
|
eil.items[slot] = nil
|
|
|
|
if item != nil {
|
|
item.Details.EquipSlotID = -1
|
|
}
|
|
|
|
return item
|
|
}
|
|
|
|
// CanItemBeEquippedInSlot checks if an item can be equipped in a slot
|
|
func (eil *EquipmentItemList) CanItemBeEquippedInSlot(item *Item, slot int8) bool {
|
|
if item == nil || slot < 0 || int(slot) >= len(eil.items) {
|
|
return false
|
|
}
|
|
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
|
|
// Check item type vs slot compatibility
|
|
switch slot {
|
|
case EQ2PrimarySlot:
|
|
return item.GenericInfo.ItemType == ItemTypeWeapon
|
|
case EQ2SecondarySlot:
|
|
return item.GenericInfo.ItemType == ItemTypeWeapon || item.GenericInfo.ItemType == ItemTypeShield
|
|
case EQ2HeadSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2ChestSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2ShouldersSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2ForearmsSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2HandsSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2LegsSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2FeetSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2LRingSlot, EQ2RRingSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeBauble
|
|
case EQ2EarsSlot1, EQ2EarsSlot2:
|
|
return item.GenericInfo.ItemType == ItemTypeBauble
|
|
case EQ2NeckSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeBauble
|
|
case EQ2LWristSlot, EQ2RWristSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeBauble
|
|
case EQ2RangeSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeRanged
|
|
case EQ2AmmoSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeNormal // Ammo
|
|
case EQ2WaistSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2CloakSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeArmor
|
|
case EQ2CharmSlot1, EQ2CharmSlot2:
|
|
return item.GenericInfo.ItemType == ItemTypeBauble
|
|
case EQ2FoodSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeFood
|
|
case EQ2DrinkSlot:
|
|
return item.GenericInfo.ItemType == ItemTypeFood
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetNumberOfItems returns the number of equipped items
|
|
func (eil *EquipmentItemList) GetNumberOfItems() int32 {
|
|
eil.mutex.RLock()
|
|
defer eil.mutex.RUnlock()
|
|
|
|
count := int32(0)
|
|
for _, item := range eil.items {
|
|
if item != nil {
|
|
count++
|
|
}
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
// GetWeight returns the total weight of equipped items
|
|
func (eil *EquipmentItemList) GetWeight() int32 {
|
|
eil.mutex.RLock()
|
|
defer eil.mutex.RUnlock()
|
|
|
|
totalWeight := int32(0)
|
|
for _, item := range eil.items {
|
|
if item != nil {
|
|
totalWeight += item.GetWeight()
|
|
}
|
|
}
|
|
|
|
return totalWeight
|
|
}
|
|
|
|
// CalculateEquipmentBonuses calculates stat bonuses from all equipped items
|
|
func (eil *EquipmentItemList) CalculateEquipmentBonuses() *ItemStatsValues {
|
|
eil.mutex.RLock()
|
|
defer eil.mutex.RUnlock()
|
|
|
|
bonuses := &ItemStatsValues{}
|
|
|
|
for _, item := range eil.items {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
item.mutex.RLock()
|
|
for _, stat := range item.ItemStats {
|
|
switch stat.StatType {
|
|
case ItemStatStr:
|
|
bonuses.Str += int16(stat.Value)
|
|
case ItemStatSta:
|
|
bonuses.Sta += int16(stat.Value)
|
|
case ItemStatAgi:
|
|
bonuses.Agi += int16(stat.Value)
|
|
case ItemStatWis:
|
|
bonuses.Wis += int16(stat.Value)
|
|
case ItemStatInt:
|
|
bonuses.Int += int16(stat.Value)
|
|
case ItemStatHealth:
|
|
bonuses.Health += int16(stat.Value)
|
|
case ItemStatPower:
|
|
bonuses.Power += int16(stat.Value)
|
|
case ItemStatCritBonus:
|
|
bonuses.CritBonus += int16(stat.Value)
|
|
case ItemStatPotency:
|
|
bonuses.Potency += int16(stat.Value)
|
|
case ItemStatStrikethrough:
|
|
bonuses.Strikethrough += int16(stat.Value)
|
|
case ItemStatAccuracy:
|
|
bonuses.Accuracy += int16(stat.Value)
|
|
case ItemStatDPS:
|
|
bonuses.DPS += int16(stat.Value)
|
|
case ItemStatAttackSpeed:
|
|
bonuses.AttackSpeed += int16(stat.Value)
|
|
case ItemStatMultiattackChance:
|
|
bonuses.MultiAttackChance += int16(stat.Value)
|
|
case ItemStatFlurry:
|
|
bonuses.Flurry += int16(stat.Value)
|
|
case ItemStatAEAutoattackChance:
|
|
bonuses.AEAutoattackChance += int16(stat.Value)
|
|
// Add more stat mappings as needed
|
|
}
|
|
}
|
|
item.mutex.RUnlock()
|
|
}
|
|
|
|
return bonuses
|
|
}
|
|
|
|
// ValidateEquipment validates all equipped items
|
|
func (eil *EquipmentItemList) ValidateEquipment() *ItemValidationResult {
|
|
eil.mutex.RLock()
|
|
defer eil.mutex.RUnlock()
|
|
|
|
result := &ItemValidationResult{Valid: true}
|
|
|
|
for slot, item := range eil.items {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
itemResult := item.Validate()
|
|
if !itemResult.Valid {
|
|
result.Valid = false
|
|
for _, itemErr := range itemResult.Errors {
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Equipment slot %d: %s", slot, itemErr))
|
|
}
|
|
}
|
|
|
|
// Check slot compatibility
|
|
if !eil.CanItemBeEquippedInSlot(item, int8(slot)) {
|
|
result.Valid = false
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Item %s cannot be equipped in slot %d", item.Name, slot))
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Packet Building Integration
|
|
|
|
// BuildItemPacket builds an item packet for network transmission
|
|
func (im *ItemManager) BuildItemPacket(item *Item, clientVersion uint32) (map[string]interface{}, error) {
|
|
if item == nil {
|
|
return nil, fmt.Errorf("item is nil")
|
|
}
|
|
|
|
item.mutex.RLock()
|
|
defer item.mutex.RUnlock()
|
|
|
|
packet := make(map[string]interface{})
|
|
|
|
// Basic item data
|
|
packet["item_id"] = item.Details.ItemID
|
|
packet["unique_id"] = item.Details.UniqueID
|
|
packet["name"] = item.Name
|
|
packet["description"] = item.Description
|
|
packet["icon"] = item.Details.Icon
|
|
packet["classic_icon"] = item.Details.ClassicIcon
|
|
packet["count"] = item.Details.Count
|
|
packet["tier"] = item.Details.Tier
|
|
packet["item_type"] = item.GenericInfo.ItemType
|
|
packet["condition"] = item.GenericInfo.Condition
|
|
packet["weight"] = item.GenericInfo.Weight
|
|
packet["item_flags"] = item.GenericInfo.ItemFlags
|
|
packet["item_flags2"] = item.GenericInfo.ItemFlags2
|
|
packet["sell_price"] = item.GetSellPrice()
|
|
packet["stack_count"] = item.StackCount
|
|
packet["bag_id"] = item.Details.BagID
|
|
packet["slot_id"] = item.Details.SlotID
|
|
packet["appearance_type"] = item.Details.AppearanceType
|
|
|
|
// Creator info
|
|
if item.Creator != "" {
|
|
packet["creator"] = item.Creator
|
|
}
|
|
|
|
// Adornments
|
|
packet["adorn0"] = item.Adorn0
|
|
packet["adorn1"] = item.Adorn1
|
|
packet["adorn2"] = item.Adorn2
|
|
|
|
// Item stats
|
|
if len(item.ItemStats) > 0 {
|
|
stats := make([]map[string]interface{}, len(item.ItemStats))
|
|
for i, stat := range item.ItemStats {
|
|
stats[i] = map[string]interface{}{
|
|
"stat_type": stat.StatType,
|
|
"stat_name": stat.StatName,
|
|
"value": stat.Value,
|
|
"level": stat.Level,
|
|
}
|
|
}
|
|
packet["item_stats"] = stats
|
|
}
|
|
|
|
// Type-specific data
|
|
if item.WeaponInfo != nil {
|
|
packet["weapon_info"] = map[string]interface{}{
|
|
"wield_type": item.WeaponInfo.WieldType,
|
|
"damage_low1": item.WeaponInfo.DamageLow1,
|
|
"damage_high1": item.WeaponInfo.DamageHigh1,
|
|
"damage_low2": item.WeaponInfo.DamageLow2,
|
|
"damage_high2": item.WeaponInfo.DamageHigh2,
|
|
"damage_low3": item.WeaponInfo.DamageLow3,
|
|
"damage_high3": item.WeaponInfo.DamageHigh3,
|
|
"delay": item.WeaponInfo.Delay,
|
|
"rating": item.WeaponInfo.Rating,
|
|
}
|
|
}
|
|
|
|
if item.ArmorInfo != nil {
|
|
packet["armor_info"] = map[string]interface{}{
|
|
"mitigation_low": item.ArmorInfo.MitigationLow,
|
|
"mitigation_high": item.ArmorInfo.MitigationHigh,
|
|
}
|
|
}
|
|
|
|
if item.BagInfo != nil {
|
|
packet["bag_info"] = map[string]interface{}{
|
|
"num_slots": item.BagInfo.NumSlots,
|
|
"weight_reduction": item.BagInfo.WeightReduction,
|
|
}
|
|
}
|
|
|
|
if item.FoodInfo != nil {
|
|
packet["food_info"] = map[string]interface{}{
|
|
"type": item.FoodInfo.Type,
|
|
"level": item.FoodInfo.Level,
|
|
"duration": item.FoodInfo.Duration,
|
|
"satiation": item.FoodInfo.Satiation,
|
|
}
|
|
}
|
|
|
|
if item.BookInfo != nil {
|
|
packet["book_info"] = map[string]interface{}{
|
|
"language": item.BookInfo.Language,
|
|
"author": item.BookInfo.Author,
|
|
"title": item.BookInfo.Title,
|
|
}
|
|
}
|
|
|
|
return packet, nil
|
|
}
|
|
|
|
// GetItemOpcodes returns item-related opcodes
|
|
func (im *ItemManager) GetItemOpcodes() map[string]packets.InternalOpcode {
|
|
return map[string]packets.InternalOpcode{
|
|
"update_inventory": packets.OP_UpdateInventoryMsg,
|
|
"item_move": packets.OP_ItemMoveMsg,
|
|
"item_equip": packets.OP_ItemEquipMsg,
|
|
"item_unequip": packets.OP_ItemUnequipMsg,
|
|
"item_pickup": packets.OP_ItemPickupMsg,
|
|
"item_drop": packets.OP_ItemDropMsg,
|
|
"item_examine": packets.OP_ItemExamineMsg,
|
|
"item_update": packets.OP_ItemUpdateMsg,
|
|
}
|
|
}
|
|
|
|
// Utility Functions
|
|
|
|
|
|
|
|
|
|
// IsLoaded returns whether the item system has been loaded
|
|
func (im *ItemManager) IsLoaded() bool {
|
|
im.mu.RLock()
|
|
defer im.mu.RUnlock()
|
|
return im.loaded
|
|
} |