447 lines
12 KiB
Go
447 lines
12 KiB
Go
package spawn
|
|
|
|
import (
|
|
"sync"
|
|
)
|
|
|
|
// Spawn entry types
|
|
const (
|
|
SpawnEntryTypeNPC = 0
|
|
SpawnEntryTypeObject = 1
|
|
SpawnEntryTypeWidget = 2
|
|
SpawnEntryTypeSign = 3
|
|
SpawnEntryTypeGroundSpawn = 4
|
|
)
|
|
|
|
// SpawnEntry represents a possible spawn at a location with its configuration
|
|
type SpawnEntry struct {
|
|
SpawnEntryID int32 // Unique identifier for this spawn entry
|
|
SpawnLocationID int32 // ID of the location this entry belongs to
|
|
SpawnType int8 // Type of spawn (NPC, Object, Widget, etc.)
|
|
SpawnID int32 // ID of the actual spawn template
|
|
SpawnPercentage float32 // Chance this spawn will appear
|
|
Respawn int32 // Base respawn time in seconds
|
|
RespawnOffsetLow int32 // Minimum random offset for respawn
|
|
RespawnOffsetHigh int32 // Maximum random offset for respawn
|
|
DuplicatedSpawn bool // Whether this spawn can appear multiple times
|
|
ExpireTime int32 // Time before spawn expires (0 = permanent)
|
|
ExpireOffset int32 // Random offset for expire time
|
|
|
|
// Spawn location overrides - these override the base spawn's stats
|
|
LevelOverride int32 // Override spawn level
|
|
HPOverride int32 // Override spawn HP
|
|
MPOverride int32 // Override spawn MP/Power
|
|
StrengthOverride int32 // Override strength stat
|
|
StaminaOverride int32 // Override stamina stat
|
|
WisdomOverride int32 // Override wisdom stat
|
|
IntelligenceOverride int32 // Override intelligence stat
|
|
AgilityOverride int32 // Override agility stat
|
|
HeatOverride int32 // Override heat resistance
|
|
ColdOverride int32 // Override cold resistance
|
|
MagicOverride int32 // Override magic resistance
|
|
MentalOverride int32 // Override mental resistance
|
|
DivineOverride int32 // Override divine resistance
|
|
DiseaseOverride int32 // Override disease resistance
|
|
PoisonOverride int32 // Override poison resistance
|
|
DifficultyOverride int32 // Override encounter level/difficulty
|
|
}
|
|
|
|
// NewSpawnEntry creates a new spawn entry with default values
|
|
func NewSpawnEntry() *SpawnEntry {
|
|
return &SpawnEntry{
|
|
SpawnEntryID: 0,
|
|
SpawnLocationID: 0,
|
|
SpawnType: SpawnEntryTypeNPC,
|
|
SpawnID: 0,
|
|
SpawnPercentage: 100.0,
|
|
Respawn: 600, // 10 minutes default
|
|
RespawnOffsetLow: 0,
|
|
RespawnOffsetHigh: 0,
|
|
DuplicatedSpawn: true,
|
|
ExpireTime: 0,
|
|
ExpireOffset: 0,
|
|
LevelOverride: 0,
|
|
HPOverride: 0,
|
|
MPOverride: 0,
|
|
StrengthOverride: 0,
|
|
StaminaOverride: 0,
|
|
WisdomOverride: 0,
|
|
IntelligenceOverride: 0,
|
|
AgilityOverride: 0,
|
|
HeatOverride: 0,
|
|
ColdOverride: 0,
|
|
MagicOverride: 0,
|
|
MentalOverride: 0,
|
|
DivineOverride: 0,
|
|
DiseaseOverride: 0,
|
|
PoisonOverride: 0,
|
|
DifficultyOverride: 0,
|
|
}
|
|
}
|
|
|
|
// GetSpawnTypeName returns a human-readable name for the spawn type
|
|
func (se *SpawnEntry) GetSpawnTypeName() string {
|
|
switch se.SpawnType {
|
|
case SpawnEntryTypeNPC:
|
|
return "NPC"
|
|
case SpawnEntryTypeObject:
|
|
return "Object"
|
|
case SpawnEntryTypeWidget:
|
|
return "Widget"
|
|
case SpawnEntryTypeSign:
|
|
return "Sign"
|
|
case SpawnEntryTypeGroundSpawn:
|
|
return "GroundSpawn"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// HasStatOverrides returns true if this entry has any stat overrides configured
|
|
func (se *SpawnEntry) HasStatOverrides() bool {
|
|
return se.LevelOverride != 0 || se.HPOverride != 0 || se.MPOverride != 0 ||
|
|
se.StrengthOverride != 0 || se.StaminaOverride != 0 || se.WisdomOverride != 0 ||
|
|
se.IntelligenceOverride != 0 || se.AgilityOverride != 0 || se.HeatOverride != 0 ||
|
|
se.ColdOverride != 0 || se.MagicOverride != 0 || se.MentalOverride != 0 ||
|
|
se.DivineOverride != 0 || se.DiseaseOverride != 0 || se.PoisonOverride != 0 ||
|
|
se.DifficultyOverride != 0
|
|
}
|
|
|
|
// GetActualRespawnTime calculates the actual respawn time including random offset
|
|
func (se *SpawnEntry) GetActualRespawnTime() int32 {
|
|
if se.RespawnOffsetLow == 0 && se.RespawnOffsetHigh == 0 {
|
|
return se.Respawn
|
|
}
|
|
|
|
// TODO: Implement random number generation
|
|
// For now, return base respawn time
|
|
return se.Respawn
|
|
}
|
|
|
|
// SpawnLocation represents a location in the world where spawns can appear
|
|
type SpawnLocation struct {
|
|
// Position data
|
|
X float32 // X coordinate in world space
|
|
Y float32 // Y coordinate in world space
|
|
Z float32 // Z coordinate in world space
|
|
Heading float32 // Direction the spawn faces
|
|
Pitch float32 // Pitch angle
|
|
Roll float32 // Roll angle
|
|
|
|
// Offset ranges for randomizing spawn positions
|
|
XOffset float32 // Random X offset range (+/-)
|
|
YOffset float32 // Random Y offset range (+/-)
|
|
ZOffset float32 // Random Z offset range (+/-)
|
|
|
|
// Location metadata
|
|
PlacementID int32 // Unique placement identifier
|
|
GridID int32 // Grid cell this location belongs to
|
|
Script string // Lua script to run for this location
|
|
Conditional int8 // Conditional flag for spawn logic
|
|
|
|
// Spawn management
|
|
Entities []*SpawnEntry // List of possible spawns at this location
|
|
TotalPercentage float32 // Sum of all spawn percentages
|
|
|
|
// Thread safety
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewSpawnLocation creates a new spawn location with default values
|
|
func NewSpawnLocation() *SpawnLocation {
|
|
return &SpawnLocation{
|
|
X: 0.0,
|
|
Y: 0.0,
|
|
Z: 0.0,
|
|
Heading: 0.0,
|
|
Pitch: 0.0,
|
|
Roll: 0.0,
|
|
XOffset: 0.0,
|
|
YOffset: 0.0,
|
|
ZOffset: 0.0,
|
|
PlacementID: 0,
|
|
GridID: 0,
|
|
Script: "",
|
|
Conditional: 0,
|
|
Entities: make([]*SpawnEntry, 0),
|
|
TotalPercentage: 0.0,
|
|
}
|
|
}
|
|
|
|
// AddSpawnEntry adds a spawn entry to this location
|
|
func (sl *SpawnLocation) AddSpawnEntry(entry *SpawnEntry) {
|
|
if entry == nil {
|
|
return
|
|
}
|
|
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
sl.Entities = append(sl.Entities, entry)
|
|
sl.TotalPercentage += entry.SpawnPercentage
|
|
}
|
|
|
|
// RemoveSpawnEntry removes a spawn entry by its ID
|
|
func (sl *SpawnLocation) RemoveSpawnEntry(entryID int32) bool {
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
for i, entry := range sl.Entities {
|
|
if entry.SpawnEntryID == entryID {
|
|
// Remove from slice
|
|
sl.Entities = append(sl.Entities[:i], sl.Entities[i+1:]...)
|
|
sl.TotalPercentage -= entry.SpawnPercentage
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetSpawnEntries returns a copy of all spawn entries
|
|
func (sl *SpawnLocation) GetSpawnEntries() []*SpawnEntry {
|
|
sl.mutex.RLock()
|
|
defer sl.mutex.RUnlock()
|
|
|
|
entries := make([]*SpawnEntry, len(sl.Entities))
|
|
copy(entries, sl.Entities)
|
|
return entries
|
|
}
|
|
|
|
// GetSpawnEntryCount returns the number of spawn entries at this location
|
|
func (sl *SpawnLocation) GetSpawnEntryCount() int {
|
|
sl.mutex.RLock()
|
|
defer sl.mutex.RUnlock()
|
|
|
|
return len(sl.Entities)
|
|
}
|
|
|
|
// GetSpawnEntryByID finds a spawn entry by its ID
|
|
func (sl *SpawnLocation) GetSpawnEntryByID(entryID int32) *SpawnEntry {
|
|
sl.mutex.RLock()
|
|
defer sl.mutex.RUnlock()
|
|
|
|
for _, entry := range sl.Entities {
|
|
if entry.SpawnEntryID == entryID {
|
|
return entry
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CalculateRandomPosition returns a randomized position within the offset ranges
|
|
// TODO: Implement proper random number generation
|
|
func (sl *SpawnLocation) CalculateRandomPosition() (float32, float32, float32) {
|
|
sl.mutex.RLock()
|
|
defer sl.mutex.RUnlock()
|
|
|
|
// For now, return base position
|
|
// In full implementation, would add random offsets within ranges
|
|
return sl.X, sl.Y, sl.Z
|
|
}
|
|
|
|
// SelectRandomSpawn selects a spawn entry based on spawn percentages
|
|
// Returns nil if no spawn is selected (based on random chance)
|
|
func (sl *SpawnLocation) SelectRandomSpawn() *SpawnEntry {
|
|
sl.mutex.RLock()
|
|
defer sl.mutex.RUnlock()
|
|
|
|
if len(sl.Entities) == 0 || sl.TotalPercentage <= 0 {
|
|
return nil
|
|
}
|
|
|
|
// TODO: Implement proper random selection based on percentages
|
|
// For now, return first entry if any exist
|
|
if len(sl.Entities) > 0 {
|
|
return sl.Entities[0]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasScript returns true if this location has a Lua script
|
|
func (sl *SpawnLocation) HasScript() bool {
|
|
return sl.Script != ""
|
|
}
|
|
|
|
// IsConditional returns true if this location has conditional spawning
|
|
func (sl *SpawnLocation) IsConditional() bool {
|
|
return sl.Conditional != 0
|
|
}
|
|
|
|
// GetDistance calculates distance from this location to specified coordinates
|
|
func (sl *SpawnLocation) GetDistance(x, y, z float32, ignoreY bool) float32 {
|
|
dx := sl.X - x
|
|
dy := sl.Y - y
|
|
dz := sl.Z - z
|
|
|
|
if ignoreY {
|
|
return float32(dx*dx + dz*dz)
|
|
}
|
|
return float32(dx*dx + dy*dy + dz*dz)
|
|
}
|
|
|
|
// SetPosition updates the location's position coordinates
|
|
func (sl *SpawnLocation) SetPosition(x, y, z float32) {
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
sl.X = x
|
|
sl.Y = y
|
|
sl.Z = z
|
|
}
|
|
|
|
// SetRotation updates the location's rotation angles
|
|
func (sl *SpawnLocation) SetRotation(heading, pitch, roll float32) {
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
sl.Heading = heading
|
|
sl.Pitch = pitch
|
|
sl.Roll = roll
|
|
}
|
|
|
|
// SetOffsets updates the location's position offset ranges
|
|
func (sl *SpawnLocation) SetOffsets(xOffset, yOffset, zOffset float32) {
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
sl.XOffset = xOffset
|
|
sl.YOffset = yOffset
|
|
sl.ZOffset = zOffset
|
|
}
|
|
|
|
// RecalculateTotalPercentage recalculates the total spawn percentage
|
|
// Should be called if spawn entry percentages are modified directly
|
|
func (sl *SpawnLocation) RecalculateTotalPercentage() {
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
sl.TotalPercentage = 0.0
|
|
for _, entry := range sl.Entities {
|
|
sl.TotalPercentage += entry.SpawnPercentage
|
|
}
|
|
}
|
|
|
|
// Cleanup releases resources used by this spawn location
|
|
func (sl *SpawnLocation) Cleanup() {
|
|
sl.mutex.Lock()
|
|
defer sl.mutex.Unlock()
|
|
|
|
// Clear spawn entries
|
|
sl.Entities = nil
|
|
sl.TotalPercentage = 0.0
|
|
}
|
|
|
|
// SpawnLocationManager manages collections of spawn locations
|
|
type SpawnLocationManager struct {
|
|
locations map[int32]*SpawnLocation // Key is placement ID
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewSpawnLocationManager creates a new spawn location manager
|
|
func NewSpawnLocationManager() *SpawnLocationManager {
|
|
return &SpawnLocationManager{
|
|
locations: make(map[int32]*SpawnLocation),
|
|
}
|
|
}
|
|
|
|
// AddLocation adds a spawn location to the manager
|
|
func (slm *SpawnLocationManager) AddLocation(placementID int32, location *SpawnLocation) {
|
|
if location == nil {
|
|
return
|
|
}
|
|
|
|
slm.mutex.Lock()
|
|
defer slm.mutex.Unlock()
|
|
|
|
location.PlacementID = placementID
|
|
slm.locations[placementID] = location
|
|
}
|
|
|
|
// RemoveLocation removes a spawn location by placement ID
|
|
func (slm *SpawnLocationManager) RemoveLocation(placementID int32) bool {
|
|
slm.mutex.Lock()
|
|
defer slm.mutex.Unlock()
|
|
|
|
if location, exists := slm.locations[placementID]; exists {
|
|
location.Cleanup()
|
|
delete(slm.locations, placementID)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetLocation retrieves a spawn location by placement ID
|
|
func (slm *SpawnLocationManager) GetLocation(placementID int32) *SpawnLocation {
|
|
slm.mutex.RLock()
|
|
defer slm.mutex.RUnlock()
|
|
|
|
return slm.locations[placementID]
|
|
}
|
|
|
|
// GetAllLocations returns a copy of all spawn locations
|
|
func (slm *SpawnLocationManager) GetAllLocations() map[int32]*SpawnLocation {
|
|
slm.mutex.RLock()
|
|
defer slm.mutex.RUnlock()
|
|
|
|
locations := make(map[int32]*SpawnLocation)
|
|
for id, location := range slm.locations {
|
|
locations[id] = location
|
|
}
|
|
return locations
|
|
}
|
|
|
|
// GetLocationCount returns the number of spawn locations
|
|
func (slm *SpawnLocationManager) GetLocationCount() int {
|
|
slm.mutex.RLock()
|
|
defer slm.mutex.RUnlock()
|
|
|
|
return len(slm.locations)
|
|
}
|
|
|
|
// GetLocationsInRange returns all locations within the specified distance of coordinates
|
|
func (slm *SpawnLocationManager) GetLocationsInRange(x, y, z, maxDistance float32, ignoreY bool) []*SpawnLocation {
|
|
slm.mutex.RLock()
|
|
defer slm.mutex.RUnlock()
|
|
|
|
var locations []*SpawnLocation
|
|
maxDistanceSquared := maxDistance * maxDistance
|
|
|
|
for _, location := range slm.locations {
|
|
distance := location.GetDistance(x, y, z, ignoreY)
|
|
if distance <= maxDistanceSquared {
|
|
locations = append(locations, location)
|
|
}
|
|
}
|
|
|
|
return locations
|
|
}
|
|
|
|
// GetLocationsByGridID returns all locations in a specific grid
|
|
func (slm *SpawnLocationManager) GetLocationsByGridID(gridID int32) []*SpawnLocation {
|
|
slm.mutex.RLock()
|
|
defer slm.mutex.RUnlock()
|
|
|
|
var locations []*SpawnLocation
|
|
for _, location := range slm.locations {
|
|
if location.GridID == gridID {
|
|
locations = append(locations, location)
|
|
}
|
|
}
|
|
|
|
return locations
|
|
}
|
|
|
|
// Clear removes all spawn locations
|
|
func (slm *SpawnLocationManager) Clear() {
|
|
slm.mutex.Lock()
|
|
defer slm.mutex.Unlock()
|
|
|
|
// Cleanup all locations
|
|
for _, location := range slm.locations {
|
|
location.Cleanup()
|
|
}
|
|
|
|
slm.locations = make(map[int32]*SpawnLocation)
|
|
}
|