1157 lines
30 KiB
Go
1157 lines
30 KiB
Go
package spawn
|
|
|
|
import (
|
|
"math"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"eq2emu/internal/common"
|
|
)
|
|
|
|
// Damage packet constants
|
|
const (
|
|
DamagePacketTypeSiphonSpell = 0x41
|
|
DamagePacketTypeSiphonSpell2 = 0x49
|
|
DamagePacketTypeMultipleDamage = 0x80
|
|
DamagePacketTypeSimpleDamage = 0xC0
|
|
DamagePacketTypeSpellDamage = 0xC1
|
|
DamagePacketTypeSimpleCritDmg = 0xC4
|
|
DamagePacketTypeSpellCritDmg = 0xC5
|
|
DamagePacketTypeSpellDamage2 = 0xC8
|
|
DamagePacketTypeSpellDamage3 = 0xC9
|
|
DamagePacketTypeRangeDamage = 0xE2
|
|
DamagePacketTypeRangeSpellDmg = 0xE3
|
|
DamagePacketTypeRangeSpellDmg2 = 0xEA
|
|
)
|
|
|
|
// Damage packet results
|
|
const (
|
|
DamagePacketResultNoDamage = 0
|
|
DamagePacketResultSuccessful = 1
|
|
DamagePacketResultMiss = 4
|
|
DamagePacketResultDodge = 8
|
|
DamagePacketResultParry = 12
|
|
DamagePacketResultRiposte = 16
|
|
DamagePacketResultBlock = 20
|
|
DamagePacketResultDeathBlow = 24
|
|
DamagePacketResultInvulnerable = 28
|
|
DamagePacketResultResist = 36
|
|
DamagePacketResultReflect = 40
|
|
DamagePacketResultImmune = 44
|
|
DamagePacketResultDeflect = 48
|
|
DamagePacketResultCounter = 52
|
|
DamagePacketResultFocus = 56
|
|
DamagePacketResultCounterStrike = 60
|
|
DamagePacketResultBash = 64
|
|
)
|
|
|
|
// Damage types
|
|
const (
|
|
DamagePacketDamageTypeSlash = 0
|
|
DamagePacketDamageTypeCrush = 1
|
|
DamagePacketDamageTypePierce = 2
|
|
DamagePacketDamageTypeHeat = 3
|
|
DamagePacketDamageTypeCold = 4
|
|
DamagePacketDamageTypeMagic = 5
|
|
DamagePacketDamageTypeMental = 6
|
|
DamagePacketDamageTypeDivine = 7
|
|
DamagePacketDamageTypeDisease = 8
|
|
DamagePacketDamageTypePoison = 9
|
|
DamagePacketDamageTypeDrown = 10
|
|
DamagePacketDamageTypeFalling = 11
|
|
DamagePacketDamageTypePain = 12
|
|
DamagePacketDamageTypeHit = 13
|
|
DamagePacketDamageTypeFocus = 14
|
|
)
|
|
|
|
// Activity status flags
|
|
const (
|
|
ActivityStatusRoleplaying = 1
|
|
ActivityStatusAnonymous = 2
|
|
ActivityStatusLinkdead = 4
|
|
ActivityStatusCamping = 8
|
|
ActivityStatusLFG = 16
|
|
ActivityStatusLFW = 32
|
|
ActivityStatusSolid = 64
|
|
ActivityStatusImmunityGained = 8192
|
|
ActivityStatusImmunityRemaining = 16384
|
|
ActivityStatusAFK = 32768
|
|
)
|
|
|
|
// Position states
|
|
const (
|
|
PosStateKneeling = 64
|
|
PosStateSolid = 128
|
|
PosStateNotargetCursor = 256
|
|
PosStateCrouching = 512
|
|
)
|
|
|
|
// Encounter states
|
|
const (
|
|
EncounterStateNone = 0
|
|
EncounterStateAvailable = 1
|
|
EncounterStateBroken = 2
|
|
EncounterStateLocked = 3
|
|
EncounterStateOvermatched = 4
|
|
EncounterStateNoReward = 5
|
|
)
|
|
|
|
// Loot methods and modes
|
|
type GroupLootMethod int
|
|
|
|
const (
|
|
MethodLeader GroupLootMethod = 0
|
|
MethodFFA GroupLootMethod = 1
|
|
MethodLotto GroupLootMethod = 2
|
|
MethodNeedBeforeGreed GroupLootMethod = 3
|
|
MethodRoundRobin GroupLootMethod = 4
|
|
)
|
|
|
|
type AutoLootMode int
|
|
|
|
const (
|
|
AutoLootDisabled = 0
|
|
AutoLootAccept = 1
|
|
AutoLootDecline = 2
|
|
)
|
|
|
|
type LootTier int
|
|
|
|
const (
|
|
ItemsAll LootTier = 0
|
|
ItemsTreasuredPlus LootTier = 1
|
|
ItemsLegendaryPlus LootTier = 2
|
|
ItemsFabledPlus LootTier = 3
|
|
)
|
|
|
|
// SpawnProximityType defines types of spawn proximity tracking
|
|
type SpawnProximityType int
|
|
|
|
const (
|
|
SpawnProximityDatabaseID = 0
|
|
SpawnProximityLocationID = 1
|
|
)
|
|
|
|
// BasicInfoStruct contains basic spawn statistics
|
|
type BasicInfoStruct struct {
|
|
CurHP int32
|
|
MaxHP int32
|
|
HPBase int32
|
|
HPBaseInstance int32
|
|
CurPower int32
|
|
MaxPower int32
|
|
PowerBase int32
|
|
PowerBaseInstance int32
|
|
CurSavagery int32
|
|
MaxSavagery int32
|
|
SavageryBase int32
|
|
CurDissonance int32
|
|
MaxDissonance int32
|
|
DissonanceBase int32
|
|
AssignedAA int16
|
|
UnassignedAA int16
|
|
TradeskillAA int16
|
|
UnassignedTradeskillAA int16
|
|
PrestigeAA int16
|
|
UnassignedPrestigeAA int16
|
|
TradeskillPrestigeAA int16
|
|
UnassignedTradeskillPrestigeAA int16
|
|
AAXPRewards int32
|
|
}
|
|
|
|
// MovementLocation represents a point in a movement path
|
|
type MovementLocation struct {
|
|
X float32
|
|
Y float32
|
|
Z float32
|
|
Speed float32
|
|
Attackable bool
|
|
LuaFunction string
|
|
Mapped bool
|
|
GridID int32
|
|
Stage int8
|
|
ResetHPOnRunback bool
|
|
UseNavPath bool
|
|
}
|
|
|
|
// MovementData contains movement configuration
|
|
type MovementData struct {
|
|
X float32
|
|
Y float32
|
|
Z float32
|
|
Speed float32
|
|
Delay int32
|
|
LuaFunction string
|
|
Heading float32
|
|
UseMovementLocationHeading bool
|
|
UseNavPath bool
|
|
}
|
|
|
|
// SpawnUpdate tracks what aspects of a spawn need updating
|
|
type SpawnUpdate struct {
|
|
SpawnID int32
|
|
InfoChanged bool
|
|
VisChanged bool
|
|
PosChanged bool
|
|
// TODO: Add Client reference when client system is implemented
|
|
// Client *Client
|
|
}
|
|
|
|
// SpawnData contains spawn packet data
|
|
type SpawnData struct {
|
|
// TODO: Add Spawn reference when implemented
|
|
// Spawn *Spawn
|
|
Data []byte
|
|
Size int32
|
|
}
|
|
|
|
// TimedGridData tracks movement through grid system
|
|
type TimedGridData struct {
|
|
Timestamp int32
|
|
GridID int32
|
|
X float32
|
|
Y float32
|
|
Z float32
|
|
OffsetY float32
|
|
ZoneGroundY float32
|
|
NPCSave bool
|
|
WidgetID int32
|
|
}
|
|
|
|
// EntityCommand represents an interactable command
|
|
type EntityCommand struct {
|
|
Name string
|
|
Distance float32
|
|
Command string
|
|
ErrorText string
|
|
CastTime int16
|
|
SpellVisual int32
|
|
AllowOrDeny map[int32]bool // player IDs and whether they're allowed
|
|
DefaultAllowList bool // if false, it's a deny list
|
|
}
|
|
|
|
// SpawnProximity tracks proximity-based events
|
|
type SpawnProximity struct {
|
|
X float32
|
|
Y float32
|
|
Z float32
|
|
SpawnValue int32
|
|
SpawnType int8
|
|
Distance float32
|
|
InRangeLuaFunction string
|
|
LeavingRangeLuaFunction string
|
|
SpawnsInProximity map[int32]bool
|
|
}
|
|
|
|
var nextSpawnID int32 = 1
|
|
|
|
// NextID generates the next unique spawn ID
|
|
// Handles special cases like avoiding IDs ending in 255 and wraparound
|
|
func NextID() int32 {
|
|
for {
|
|
id := atomic.AddInt32(&nextSpawnID, 1)
|
|
|
|
// Handle wraparound (using math.MaxInt32 - 1)
|
|
if id >= 2147483646 {
|
|
atomic.StoreInt32(&nextSpawnID, 1)
|
|
continue
|
|
}
|
|
|
|
// Avoid IDs ending in 255 to prevent client confusion/crashes
|
|
if (id-255)%256 == 0 {
|
|
continue
|
|
}
|
|
|
|
return id
|
|
}
|
|
}
|
|
|
|
// Spawn represents a game entity that can appear in the world
|
|
type Spawn struct {
|
|
// Basic identification
|
|
id int32
|
|
databaseID int32
|
|
|
|
// Appearance and positioning
|
|
appearance common.AppearanceData
|
|
size int16
|
|
sizeOffset int8
|
|
|
|
// State flags (using atomic for thread safety)
|
|
changed atomic.Bool
|
|
positionChanged atomic.Bool
|
|
infoChanged atomic.Bool
|
|
visChanged atomic.Bool
|
|
isRunning atomic.Bool
|
|
sizeChanged atomic.Bool
|
|
following atomic.Bool
|
|
isAlive atomic.Bool
|
|
deletedSpawn atomic.Bool
|
|
resetMovement atomic.Bool
|
|
knockedBack atomic.Bool
|
|
|
|
// Game state
|
|
basicInfo BasicInfoStruct
|
|
factionID int32
|
|
spawnType int8
|
|
target int32
|
|
lastAttacker int32
|
|
|
|
// Commands and interaction
|
|
primaryCommandListID int32
|
|
secondaryCommandListID int32
|
|
primaryCommandList []*EntityCommand
|
|
secondaryCommandList []*EntityCommand
|
|
|
|
// Movement and positioning
|
|
lastMovementUpdate int32
|
|
lastLocationUpdate int32
|
|
lastGridUpdate int32
|
|
forceMapCheck bool
|
|
movementLocations []*MovementLocation
|
|
movementLoop []*MovementData
|
|
movementIndex int16
|
|
runningTo int32
|
|
invulnerable bool
|
|
|
|
// Group and spawn relationships
|
|
groupID int32
|
|
spawnGroupList []*Spawn
|
|
|
|
// Location and respawn data
|
|
spawnLocationID int32
|
|
spawnEntryID int32
|
|
spawnLocationSpawnsID int32
|
|
respawn int32
|
|
respawnOffsetLow int32
|
|
respawnOffsetHigh int32
|
|
duplicatedSpawn bool
|
|
expireTime int32
|
|
expireOffset int32
|
|
xOffset float32
|
|
yOffset float32
|
|
zOffset float32
|
|
deviation int32
|
|
|
|
// Loot system
|
|
lootItems []*Item // TODO: Define Item type
|
|
lootCoins int32
|
|
lootGroupID int32
|
|
lootMethod GroupLootMethod
|
|
lootRarity int8
|
|
looterSpawnID int32
|
|
lootComplete map[int32]bool
|
|
isLootComplete bool
|
|
isLootDispensed bool
|
|
lootName string
|
|
lootTier int32
|
|
lootDropType int32
|
|
trapTriggered bool
|
|
trapState int32
|
|
chestDropTime int32
|
|
trapOpenedTime int32
|
|
|
|
// Merchant data
|
|
merchantID int32
|
|
merchantType int8
|
|
merchantMinLevel int32
|
|
merchantMaxLevel int32
|
|
isCollector bool
|
|
|
|
// Transportation
|
|
transporterID int32
|
|
isTransportSpawn bool
|
|
railID int64
|
|
railPassengers map[int32]bool
|
|
|
|
// Scripting and AI
|
|
spawnScript string
|
|
spawnScriptSetDB bool
|
|
questIDs []int32
|
|
|
|
// Quest and access requirements
|
|
hasQuestsRequired bool
|
|
hasHistoryRequired bool
|
|
reqQuestsPrivate bool
|
|
reqQuestsOverride int16
|
|
reqQuestsContinuedAccess bool
|
|
requiredQuests map[int32][]int16
|
|
// TODO: Add LUAHistory type
|
|
// requiredHistory map[int32]*LUAHistory
|
|
allowedAccess map[int32]int8
|
|
|
|
// Visual and display state
|
|
tmpVisualState int
|
|
tmpActionState int
|
|
illusionModel int16
|
|
|
|
// Items and pickups
|
|
pickupItemID int32
|
|
pickupUniqueItemID int32
|
|
houseCharacterID int32
|
|
|
|
// Proximity tracking
|
|
hasSpawnProximities bool
|
|
spawnProximities []*SpawnProximity
|
|
|
|
// Creature flags
|
|
isFlyingCreature bool
|
|
isWaterCreature bool
|
|
isPet bool
|
|
scaredByStrongPlayers bool
|
|
|
|
// Following system
|
|
followTarget int32
|
|
followDistance int32
|
|
|
|
// Temporary variables (for Lua scripting)
|
|
tempVariableTypes map[string]int8
|
|
tempVariables map[string]string
|
|
tempVariableSpawn map[string]int32
|
|
// TODO: Add other temp variable types when systems are implemented
|
|
// tempVariableZone map[string]*ZoneServer
|
|
// tempVariableItem map[string]*Item
|
|
// tempVariableQuest map[string]*Quest
|
|
|
|
// Animation and timing
|
|
spawnAnim int32
|
|
addedToWorldTimestamp int32
|
|
spawnAnimLeeway int16
|
|
lastHeadingAngle float32
|
|
|
|
// Region and mapping
|
|
// TODO: Add RegionMap and Map types
|
|
// regionMap *RegionMap
|
|
// currentMap *Map
|
|
// regions map[map[*RegionNode]*ZBSPNode]*RegionStatus
|
|
establishedGridID map[int32]*TimedGridData
|
|
ignoredWidgets map[int32]bool
|
|
triggerWidgetID int32
|
|
|
|
// Equipment
|
|
// TODO: Add EquipmentItemList type
|
|
// equipmentList *EquipmentItemList
|
|
// appearanceEquipmentList *EquipmentItemList
|
|
|
|
// Zone reference
|
|
// TODO: Add ZoneServer reference when implemented
|
|
// zone *ZoneServer
|
|
|
|
// Knockback physics
|
|
knockedBackTimeStep float32
|
|
knockedBackHDistance float32
|
|
knockedBackVDistance float32
|
|
knockedBackDuration float32
|
|
knockedBackStartX float32
|
|
knockedBackStartY float32
|
|
knockedBackStartZ float32
|
|
knockedBackEndTime int32
|
|
knockedAngle float32
|
|
// TODO: Add glm::vec3 equivalent
|
|
// knockedVelocity Vec3
|
|
|
|
// Database omission flag
|
|
isOmittedByDBFlag bool
|
|
|
|
// Sound settings
|
|
disableSounds bool
|
|
|
|
// Thread safety
|
|
updateMutex sync.RWMutex
|
|
spawnMutex sync.RWMutex
|
|
lootItemsMutex sync.RWMutex
|
|
commandMutex sync.Mutex
|
|
regionMutex sync.Mutex
|
|
railMutex sync.Mutex
|
|
gridMutex sync.Mutex
|
|
movementMutex sync.RWMutex
|
|
requiredQuestsMutex sync.RWMutex
|
|
requiredHistoryMutex sync.RWMutex
|
|
ignoredWidgetsMutex sync.RWMutex
|
|
}
|
|
|
|
// NewSpawn creates a new spawn instance with default values
|
|
// Initializes all fields to appropriate defaults matching the C++ constructor
|
|
func NewSpawn() *Spawn {
|
|
s := &Spawn{
|
|
id: NextID(),
|
|
size: 32,
|
|
sizeOffset: 0,
|
|
factionID: 0,
|
|
spawnType: 0,
|
|
target: 0,
|
|
lastAttacker: 0,
|
|
primaryCommandListID: 0,
|
|
secondaryCommandListID: 0,
|
|
primaryCommandList: make([]*EntityCommand, 0),
|
|
secondaryCommandList: make([]*EntityCommand, 0),
|
|
groupID: 0,
|
|
spawnLocationID: 0,
|
|
spawnEntryID: 0,
|
|
spawnLocationSpawnsID: 0,
|
|
respawn: 0,
|
|
respawnOffsetLow: 0,
|
|
respawnOffsetHigh: 0,
|
|
duplicatedSpawn: true,
|
|
expireTime: 0,
|
|
expireOffset: 0,
|
|
xOffset: 0,
|
|
yOffset: 0,
|
|
zOffset: 0,
|
|
deviation: 0,
|
|
lootItems: make([]*Item, 0),
|
|
lootCoins: 0,
|
|
lootGroupID: 0,
|
|
lootMethod: MethodFFA,
|
|
lootRarity: 0,
|
|
looterSpawnID: 0,
|
|
lootComplete: make(map[int32]bool),
|
|
isLootComplete: false,
|
|
isLootDispensed: false,
|
|
lootTier: 0,
|
|
lootDropType: 0,
|
|
trapTriggered: false,
|
|
trapState: 0,
|
|
chestDropTime: 0,
|
|
trapOpenedTime: 0,
|
|
merchantID: 0,
|
|
merchantType: 0,
|
|
merchantMinLevel: 0,
|
|
merchantMaxLevel: 0,
|
|
isCollector: false,
|
|
transporterID: 0,
|
|
isTransportSpawn: false,
|
|
railID: 0,
|
|
railPassengers: make(map[int32]bool),
|
|
questIDs: make([]int32, 0),
|
|
hasQuestsRequired: false,
|
|
hasHistoryRequired: false,
|
|
reqQuestsPrivate: false,
|
|
reqQuestsOverride: 0,
|
|
reqQuestsContinuedAccess: false,
|
|
requiredQuests: make(map[int32][]int16),
|
|
allowedAccess: make(map[int32]int8),
|
|
tmpVisualState: -1,
|
|
tmpActionState: -1,
|
|
illusionModel: 0,
|
|
pickupItemID: 0,
|
|
pickupUniqueItemID: 0,
|
|
houseCharacterID: 0,
|
|
hasSpawnProximities: false,
|
|
spawnProximities: make([]*SpawnProximity, 0),
|
|
isFlyingCreature: false,
|
|
isWaterCreature: false,
|
|
isPet: false,
|
|
scaredByStrongPlayers: false,
|
|
followTarget: 0,
|
|
followDistance: 0,
|
|
tempVariableTypes: make(map[string]int8),
|
|
tempVariables: make(map[string]string),
|
|
tempVariableSpawn: make(map[string]int32),
|
|
spawnAnim: 0,
|
|
addedToWorldTimestamp: 0,
|
|
spawnAnimLeeway: 0,
|
|
lastHeadingAngle: 0.0,
|
|
lastMovementUpdate: int32(time.Now().Unix()),
|
|
lastLocationUpdate: 0,
|
|
lastGridUpdate: 0,
|
|
forceMapCheck: false,
|
|
movementLocations: make([]*MovementLocation, 0),
|
|
movementLoop: make([]*MovementData, 0),
|
|
movementIndex: 0,
|
|
runningTo: 0,
|
|
invulnerable: false,
|
|
spawnGroupList: make([]*Spawn, 0),
|
|
establishedGridID: make(map[int32]*TimedGridData),
|
|
ignoredWidgets: make(map[int32]bool),
|
|
triggerWidgetID: 0,
|
|
isOmittedByDBFlag: false,
|
|
disableSounds: false,
|
|
}
|
|
|
|
// Initialize appearance with defaults
|
|
s.appearance.Pos.State = 0x4080
|
|
s.appearance.Difficulty = 6
|
|
s.appearance.Pos.CollisionRadius = 32
|
|
s.appearance.Pos.Speed1 = 0
|
|
|
|
// Set alive state
|
|
s.isAlive.Store(true)
|
|
|
|
// Initialize encounter state
|
|
s.SetLockedNoLoot(EncounterStateAvailable)
|
|
|
|
return s
|
|
}
|
|
|
|
// GetID returns the spawn's unique identifier
|
|
func (s *Spawn) GetID() int32 {
|
|
return s.id
|
|
}
|
|
|
|
// SetID updates the spawn's unique identifier
|
|
func (s *Spawn) SetID(id int32) {
|
|
s.id = id
|
|
}
|
|
|
|
// GetDatabaseID returns the database ID for this spawn
|
|
func (s *Spawn) GetDatabaseID() int32 {
|
|
return s.databaseID
|
|
}
|
|
|
|
// SetDatabaseID updates the database ID for this spawn
|
|
func (s *Spawn) SetDatabaseID(id int32) {
|
|
s.databaseID = id
|
|
}
|
|
|
|
// GetName returns the spawn's display name
|
|
func (s *Spawn) GetName() string {
|
|
return string(s.appearance.Name[:])
|
|
}
|
|
|
|
// SetName updates the spawn's display name and marks info as changed
|
|
func (s *Spawn) SetName(name string) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
copy(s.appearance.Name[:], name)
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetLevel returns the spawn's level
|
|
func (s *Spawn) GetLevel() int16 {
|
|
return int16(s.appearance.Level)
|
|
}
|
|
|
|
// SetLevel updates the spawn's level and marks info as changed
|
|
func (s *Spawn) SetLevel(level int16) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.appearance.Level = int8(level)
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetX returns the spawn's X coordinate
|
|
func (s *Spawn) GetX() float32 {
|
|
return s.appearance.Pos.X
|
|
}
|
|
|
|
// SetX updates the spawn's X coordinate and marks position as changed
|
|
func (s *Spawn) SetX(x float32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.appearance.Pos.X = x
|
|
s.positionChanged.Store(true)
|
|
s.infoChanged.Store(true)
|
|
s.visChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetY returns the spawn's Y coordinate
|
|
func (s *Spawn) GetY() float32 {
|
|
return s.appearance.Pos.Y
|
|
}
|
|
|
|
// SetY updates the spawn's Y coordinate with optional Y map fixing
|
|
func (s *Spawn) SetY(y float32, disableYMapFix bool) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.appearance.Pos.Y = y
|
|
s.positionChanged.Store(true)
|
|
s.infoChanged.Store(true)
|
|
s.visChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
|
|
// TODO: Implement Y map fixing when map system is available
|
|
if !disableYMapFix {
|
|
// FixZ() would be called here
|
|
}
|
|
}
|
|
|
|
// GetZ returns the spawn's Z coordinate
|
|
func (s *Spawn) GetZ() float32 {
|
|
return s.appearance.Pos.Z
|
|
}
|
|
|
|
// SetZ updates the spawn's Z coordinate and marks position as changed
|
|
func (s *Spawn) SetZ(z float32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.appearance.Pos.Z = z
|
|
s.positionChanged.Store(true)
|
|
s.infoChanged.Store(true)
|
|
s.visChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetHeading returns the spawn's heading in degrees
|
|
func (s *Spawn) GetHeading() float32 {
|
|
if s.appearance.Pos.Dir1 == 0 {
|
|
return 0
|
|
}
|
|
|
|
heading := float32(s.appearance.Pos.Dir1) / 64.0
|
|
if heading >= 180 {
|
|
heading -= 180
|
|
} else {
|
|
heading += 180
|
|
}
|
|
return heading
|
|
}
|
|
|
|
// SetHeading updates the spawn's heading using raw direction values
|
|
func (s *Spawn) SetHeading(dir1, dir2 int16) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.appearance.Pos.Dir1 = dir1
|
|
s.appearance.Pos.Dir2 = dir2
|
|
s.positionChanged.Store(true)
|
|
s.infoChanged.Store(true)
|
|
s.visChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// SetHeadingFromFloat updates the spawn's heading using degrees
|
|
func (s *Spawn) SetHeadingFromFloat(heading float32) {
|
|
s.lastHeadingAngle = heading
|
|
if heading != 180 {
|
|
heading = (heading - 180) * 64
|
|
}
|
|
s.SetHeading(int16(heading), int16(heading))
|
|
}
|
|
|
|
// GetDistance calculates distance to specific coordinates
|
|
func (s *Spawn) GetDistance(x, y, z float32, ignoreY bool) float32 {
|
|
dx := s.GetX() - x
|
|
dy := s.GetY() - y
|
|
dz := s.GetZ() - z
|
|
|
|
if ignoreY {
|
|
return float32(math.Sqrt(float64(dx*dx + dz*dz)))
|
|
}
|
|
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
|
|
}
|
|
|
|
// GetDistanceToSpawn calculates distance to another spawn
|
|
func (s *Spawn) GetDistanceToSpawn(target *Spawn, ignoreY, includeRadius bool) float32 {
|
|
if target == nil {
|
|
return 0
|
|
}
|
|
|
|
distance := s.GetDistance(target.GetX(), target.GetY(), target.GetZ(), ignoreY)
|
|
|
|
if includeRadius {
|
|
distance -= s.CalculateRadius(target)
|
|
}
|
|
|
|
return distance
|
|
}
|
|
|
|
// CalculateRadius calculates the combined collision radius for two spawns
|
|
func (s *Spawn) CalculateRadius(target *Spawn) float32 {
|
|
if target == nil {
|
|
return 0
|
|
}
|
|
|
|
myRadius := float32(s.appearance.Pos.CollisionRadius)
|
|
targetRadius := float32(target.appearance.Pos.CollisionRadius)
|
|
|
|
return myRadius + targetRadius
|
|
}
|
|
|
|
// IsAlive returns whether the spawn is currently alive
|
|
func (s *Spawn) IsAlive() bool {
|
|
return s.isAlive.Load()
|
|
}
|
|
|
|
// SetAlive updates the spawn's alive state
|
|
func (s *Spawn) SetAlive(alive bool) {
|
|
s.isAlive.Store(alive)
|
|
}
|
|
|
|
// GetHP returns the spawn's current hit points
|
|
func (s *Spawn) GetHP() int32 {
|
|
return s.basicInfo.CurHP
|
|
}
|
|
|
|
// SetHP updates the spawn's current hit points
|
|
func (s *Spawn) SetHP(hp int32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.basicInfo.CurHP = hp
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetTotalHP returns the spawn's maximum hit points
|
|
func (s *Spawn) GetTotalHP() int32 {
|
|
return s.basicInfo.MaxHP
|
|
}
|
|
|
|
// SetTotalHP updates the spawn's maximum hit points
|
|
func (s *Spawn) SetTotalHP(maxHP int32) {
|
|
s.basicInfo.MaxHP = maxHP
|
|
}
|
|
|
|
// GetPower returns the spawn's current power points
|
|
func (s *Spawn) GetPower() int32 {
|
|
return s.basicInfo.CurPower
|
|
}
|
|
|
|
// SetPower updates the spawn's current power points
|
|
func (s *Spawn) SetPower(power int32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.basicInfo.CurPower = power
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetTotalPower returns the spawn's maximum power points
|
|
func (s *Spawn) GetTotalPower() int32 {
|
|
return s.basicInfo.MaxPower
|
|
}
|
|
|
|
// SetTotalPower updates the spawn's maximum power points
|
|
func (s *Spawn) SetTotalPower(maxPower int32) {
|
|
s.basicInfo.MaxPower = maxPower
|
|
}
|
|
|
|
// GetLockedNoLoot returns the spawn's loot lock state
|
|
func (s *Spawn) GetLockedNoLoot() int8 {
|
|
return s.appearance.LockedNoLoot
|
|
}
|
|
|
|
// SetLockedNoLoot updates the spawn's loot lock state
|
|
func (s *Spawn) SetLockedNoLoot(state int8) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.appearance.LockedNoLoot = state
|
|
s.visChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// AddPrimaryEntityCommand adds a new primary command to the spawn
|
|
func (s *Spawn) AddPrimaryEntityCommand(name string, distance float32, command, errorText string, castTime int16, spellVisual int32, defaultAllowList bool) {
|
|
s.commandMutex.Lock()
|
|
defer s.commandMutex.Unlock()
|
|
|
|
entityCommand := &EntityCommand{
|
|
Name: name,
|
|
Distance: distance,
|
|
Command: command,
|
|
ErrorText: errorText,
|
|
CastTime: castTime,
|
|
SpellVisual: spellVisual,
|
|
AllowOrDeny: make(map[int32]bool),
|
|
DefaultAllowList: defaultAllowList,
|
|
}
|
|
|
|
s.primaryCommandList = append(s.primaryCommandList, entityCommand)
|
|
}
|
|
|
|
// RemovePrimaryCommands clears all primary commands
|
|
func (s *Spawn) RemovePrimaryCommands() {
|
|
s.commandMutex.Lock()
|
|
defer s.commandMutex.Unlock()
|
|
|
|
s.primaryCommandList = make([]*EntityCommand, 0)
|
|
}
|
|
|
|
// GetPrimaryCommands returns the list of primary commands
|
|
func (s *Spawn) GetPrimaryCommands() []*EntityCommand {
|
|
s.commandMutex.Lock()
|
|
defer s.commandMutex.Unlock()
|
|
|
|
// Return a copy to prevent race conditions
|
|
commands := make([]*EntityCommand, len(s.primaryCommandList))
|
|
copy(commands, s.primaryCommandList)
|
|
return commands
|
|
}
|
|
|
|
// GetSecondaryCommands returns the list of secondary commands
|
|
func (s *Spawn) GetSecondaryCommands() []*EntityCommand {
|
|
s.commandMutex.Lock()
|
|
defer s.commandMutex.Unlock()
|
|
|
|
// Return a copy to prevent race conditions
|
|
commands := make([]*EntityCommand, len(s.secondaryCommandList))
|
|
copy(commands, s.secondaryCommandList)
|
|
return commands
|
|
}
|
|
|
|
// GetFactionID returns the spawn's faction ID
|
|
func (s *Spawn) GetFactionID() int32 {
|
|
return s.factionID
|
|
}
|
|
|
|
// SetFactionID updates the spawn's faction ID
|
|
func (s *Spawn) SetFactionID(factionID int32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.factionID = factionID
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetTarget returns the spawn's current target ID
|
|
func (s *Spawn) GetTarget() int32 {
|
|
return s.target
|
|
}
|
|
|
|
// SetTarget updates the spawn's current target
|
|
func (s *Spawn) SetTarget(targetID int32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.target = targetID
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetSize returns the spawn's size
|
|
func (s *Spawn) GetSize() int16 {
|
|
return s.size
|
|
}
|
|
|
|
// SetSize updates the spawn's size
|
|
func (s *Spawn) SetSize(size int16) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.size = size
|
|
s.sizeChanged.Store(true)
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// GetSpawnType returns the spawn's type
|
|
func (s *Spawn) GetSpawnType() int8 {
|
|
return s.spawnType
|
|
}
|
|
|
|
// SetSpawnType updates the spawn's type
|
|
func (s *Spawn) SetSpawnType(spawnType int8) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.spawnType = spawnType
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
|
|
// SetSpawnScript updates the spawn's Lua script
|
|
func (s *Spawn) SetSpawnScript(script string, dbSet bool) {
|
|
s.spawnScript = script
|
|
s.spawnScriptSetDB = dbSet
|
|
}
|
|
|
|
// GetSpawnScript returns the spawn's Lua script
|
|
func (s *Spawn) GetSpawnScript() string {
|
|
return s.spawnScript
|
|
}
|
|
|
|
// AddTempVariable stores a temporary string variable for Lua scripting
|
|
func (s *Spawn) AddTempVariable(name, value string) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.tempVariables[name] = value
|
|
s.tempVariableTypes[name] = 0 // String type
|
|
}
|
|
|
|
// GetTempVariable retrieves a temporary string variable
|
|
func (s *Spawn) GetTempVariable(name string) string {
|
|
s.updateMutex.RLock()
|
|
defer s.updateMutex.RUnlock()
|
|
|
|
if value, exists := s.tempVariables[name]; exists {
|
|
return value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// DeleteTempVariable removes a temporary variable
|
|
func (s *Spawn) DeleteTempVariable(name string) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
delete(s.tempVariables, name)
|
|
delete(s.tempVariableTypes, name)
|
|
delete(s.tempVariableSpawn, name)
|
|
}
|
|
|
|
// IsNPC returns whether this spawn is an NPC (to be overridden by subclasses)
|
|
func (s *Spawn) IsNPC() bool {
|
|
return false
|
|
}
|
|
|
|
// IsPlayer returns whether this spawn is a player (to be overridden by subclasses)
|
|
func (s *Spawn) IsPlayer() bool {
|
|
return false
|
|
}
|
|
|
|
// IsObject returns whether this spawn is an object (to be overridden by subclasses)
|
|
func (s *Spawn) IsObject() bool {
|
|
return false
|
|
}
|
|
|
|
// IsWidget returns whether this spawn is a widget (to be overridden by subclasses)
|
|
func (s *Spawn) IsWidget() bool {
|
|
return false
|
|
}
|
|
|
|
// IsSign returns whether this spawn is a sign (to be overridden by subclasses)
|
|
func (s *Spawn) IsSign() bool {
|
|
return false
|
|
}
|
|
|
|
// IsGroundSpawn returns whether this spawn is a ground spawn (to be overridden by subclasses)
|
|
func (s *Spawn) IsGroundSpawn() bool {
|
|
return false
|
|
}
|
|
|
|
// IsEntity returns whether this spawn is an entity (to be overridden by subclasses)
|
|
func (s *Spawn) IsEntity() bool {
|
|
return false
|
|
}
|
|
|
|
// IsBot returns whether this spawn is a bot (to be overridden by subclasses)
|
|
func (s *Spawn) IsBot() bool {
|
|
return false
|
|
}
|
|
|
|
// GetAppearanceData returns the spawn's appearance data
|
|
func (s *Spawn) GetAppearanceData() *common.AppearanceData {
|
|
s.updateMutex.RLock()
|
|
defer s.updateMutex.RUnlock()
|
|
return &s.appearance
|
|
}
|
|
|
|
// GetBasicInfo returns the spawn's basic info structure
|
|
func (s *Spawn) GetBasicInfo() *BasicInfoStruct {
|
|
s.updateMutex.RLock()
|
|
defer s.updateMutex.RUnlock()
|
|
return &s.basicInfo
|
|
}
|
|
|
|
// SetBasicInfo updates the spawn's basic info structure
|
|
func (s *Spawn) SetBasicInfo(info *BasicInfoStruct) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
if info != nil {
|
|
s.basicInfo = *info
|
|
s.infoChanged.Store(true)
|
|
s.changed.Store(true)
|
|
s.addChangedZoneSpawn()
|
|
}
|
|
}
|
|
|
|
// GetClient returns the client associated with this spawn (overridden by Player)
|
|
func (s *Spawn) GetClient() any {
|
|
return nil // Base spawns have no client
|
|
}
|
|
|
|
// HasChanged returns whether the spawn has any pending changes
|
|
func (s *Spawn) HasChanged() bool {
|
|
return s.changed.Load()
|
|
}
|
|
|
|
// HasInfoChanged returns whether the spawn's info has changed
|
|
func (s *Spawn) HasInfoChanged() bool {
|
|
return s.infoChanged.Load()
|
|
}
|
|
|
|
// HasPositionChanged returns whether the spawn's position has changed
|
|
func (s *Spawn) HasPositionChanged() bool {
|
|
return s.positionChanged.Load()
|
|
}
|
|
|
|
// HasVisualChanged returns whether the spawn's visual appearance has changed
|
|
func (s *Spawn) HasVisualChanged() bool {
|
|
return s.visChanged.Load()
|
|
}
|
|
|
|
// ClearChanged resets all change flags
|
|
func (s *Spawn) ClearChanged() {
|
|
s.changed.Store(false)
|
|
s.infoChanged.Store(false)
|
|
s.positionChanged.Store(false)
|
|
s.visChanged.Store(false)
|
|
s.sizeChanged.Store(false)
|
|
}
|
|
|
|
// GetLastAttacker returns the ID of the spawn's last attacker
|
|
func (s *Spawn) GetLastAttacker() int32 {
|
|
return s.lastAttacker
|
|
}
|
|
|
|
// SetLastAttacker updates the spawn's last attacker
|
|
func (s *Spawn) SetLastAttacker(attackerID int32) {
|
|
s.updateMutex.Lock()
|
|
defer s.updateMutex.Unlock()
|
|
|
|
s.lastAttacker = attackerID
|
|
}
|
|
|
|
// addChangedZoneSpawn notifies the zone that this spawn has changed
|
|
// TODO: Implement when zone system is available
|
|
func (s *Spawn) addChangedZoneSpawn() {
|
|
// This would notify the zone server of spawn changes
|
|
// Implementation depends on zone system architecture
|
|
}
|
|
|
|
// TODO: Additional methods to implement:
|
|
// - Serialization methods (serialize, spawn_serialize, etc.)
|
|
// - Movement system methods
|
|
// - Combat and damage methods
|
|
// - Loot system methods
|
|
// - Group and faction methods
|
|
// - Equipment methods
|
|
// - Proximity and region methods
|
|
// - Knockback physics methods
|
|
// - Many more getter/setter methods
|
|
|
|
// Item placeholder - to be moved to appropriate package when implemented
|
|
// Basic structure to prevent compilation errors in Entity system
|
|
type Item struct {
|
|
ID int32 // Item ID
|
|
UniqueID int32 // Unique instance ID
|
|
Name string // Item name
|
|
Description string // Item description
|
|
Icon int32 // Icon ID
|
|
Count int16 // Stack count
|
|
Price int32 // Item price
|
|
Slot int8 // Equipment slot
|
|
Type int8 // Item type
|
|
SubType int8 // Item subtype
|
|
// TODO: Add complete item structure when item system is implemented
|
|
// This is a minimal placeholder to allow Entity system compilation
|
|
}
|