340 lines
8.9 KiB
Go
340 lines
8.9 KiB
Go
package ground_spawn
|
|
|
|
import (
|
|
"sync"
|
|
)
|
|
|
|
// MasterList is a specialized ground spawn master list optimized for:
|
|
// - Fast zone-based lookups (O(1))
|
|
// - Fast skill-based lookups (O(1))
|
|
// - Spatial grid queries for proximity searches
|
|
// - Set intersection operations for complex queries
|
|
// - Fast state-based queries (available/depleted)
|
|
type MasterList struct {
|
|
// Core storage
|
|
spawns map[int32]*GroundSpawn // ID -> GroundSpawn
|
|
mutex sync.RWMutex
|
|
|
|
// Category indices for O(1) lookups
|
|
byZone map[int32][]*GroundSpawn // Zone ID -> spawns
|
|
bySkill map[string][]*GroundSpawn // Skill -> spawns
|
|
|
|
// State indices for O(1) filtering
|
|
availableSpawns []*GroundSpawn // Available spawns (cached)
|
|
depletedSpawns []*GroundSpawn // Depleted spawns (cached)
|
|
stateStale bool // Whether state caches need refresh
|
|
|
|
// Statistics cache
|
|
stats *Statistics
|
|
statsStale bool
|
|
|
|
// Spatial grid for proximity queries (grid size = 100 units)
|
|
spatialGrid map[gridKey][]*GroundSpawn
|
|
gridSize float32
|
|
}
|
|
|
|
// gridKey represents a spatial grid cell
|
|
type gridKey struct {
|
|
x, y int32
|
|
}
|
|
|
|
// NewMasterList creates a new specialized ground spawn master list
|
|
func NewMasterList() *MasterList {
|
|
return &MasterList{
|
|
spawns: make(map[int32]*GroundSpawn),
|
|
byZone: make(map[int32][]*GroundSpawn),
|
|
bySkill: make(map[string][]*GroundSpawn),
|
|
spatialGrid: make(map[gridKey][]*GroundSpawn),
|
|
gridSize: 100.0, // 100 unit grid cells
|
|
stateStale: true, // Initial state needs refresh
|
|
statsStale: true, // Initial stats need refresh
|
|
}
|
|
}
|
|
|
|
// getGridKey returns the grid cell for given coordinates
|
|
func (ml *MasterList) getGridKey(x, y float32) gridKey {
|
|
return gridKey{
|
|
x: int32(x / ml.gridSize),
|
|
y: int32(y / ml.gridSize),
|
|
}
|
|
}
|
|
|
|
// refreshStateCache updates the available/depleted spawn caches
|
|
func (ml *MasterList) refreshStateCache() {
|
|
if !ml.stateStale {
|
|
return
|
|
}
|
|
|
|
// Clear existing caches
|
|
ml.availableSpawns = ml.availableSpawns[:0]
|
|
ml.depletedSpawns = ml.depletedSpawns[:0]
|
|
|
|
// Rebuild caches
|
|
for _, gs := range ml.spawns {
|
|
if gs.IsAvailable() {
|
|
ml.availableSpawns = append(ml.availableSpawns, gs)
|
|
} else if gs.IsDepleted() {
|
|
ml.depletedSpawns = append(ml.depletedSpawns, gs)
|
|
}
|
|
}
|
|
|
|
ml.stateStale = false
|
|
}
|
|
|
|
// refreshStatsCache updates the statistics cache
|
|
func (ml *MasterList) refreshStatsCache() {
|
|
if !ml.statsStale {
|
|
return
|
|
}
|
|
|
|
var availableSpawns int
|
|
zoneMap := make(map[int32]int)
|
|
skillMap := make(map[string]int64)
|
|
|
|
// Single pass through all spawns
|
|
for _, gs := range ml.spawns {
|
|
if gs.IsAvailable() {
|
|
availableSpawns++
|
|
}
|
|
zoneMap[gs.ZoneID]++
|
|
skillMap[gs.CollectionSkill]++
|
|
}
|
|
|
|
ml.stats = &Statistics{
|
|
TotalHarvests: 0, // Would need to be tracked separately
|
|
SuccessfulHarvests: 0, // Would need to be tracked separately
|
|
RareItemsHarvested: 0, // Would need to be tracked separately
|
|
SkillUpsGenerated: 0, // Would need to be tracked separately
|
|
HarvestsBySkill: skillMap,
|
|
ActiveGroundSpawns: availableSpawns,
|
|
GroundSpawnsByZone: zoneMap,
|
|
}
|
|
|
|
ml.statsStale = false
|
|
}
|
|
|
|
// AddGroundSpawn adds a ground spawn with full indexing
|
|
func (ml *MasterList) AddGroundSpawn(gs *GroundSpawn) bool {
|
|
ml.mutex.Lock()
|
|
defer ml.mutex.Unlock()
|
|
|
|
// Check if exists
|
|
if _, exists := ml.spawns[gs.GroundSpawnID]; exists {
|
|
return false
|
|
}
|
|
|
|
// Add to core storage
|
|
ml.spawns[gs.GroundSpawnID] = gs
|
|
|
|
// Update zone index
|
|
ml.byZone[gs.ZoneID] = append(ml.byZone[gs.ZoneID], gs)
|
|
|
|
// Update skill index
|
|
ml.bySkill[gs.CollectionSkill] = append(ml.bySkill[gs.CollectionSkill], gs)
|
|
|
|
// Update spatial grid
|
|
gridKey := ml.getGridKey(gs.X, gs.Y)
|
|
ml.spatialGrid[gridKey] = append(ml.spatialGrid[gridKey], gs)
|
|
|
|
// Invalidate state and stats caches
|
|
ml.stateStale = true
|
|
ml.statsStale = true
|
|
|
|
return true
|
|
}
|
|
|
|
// GetGroundSpawn retrieves by ID (O(1))
|
|
func (ml *MasterList) GetGroundSpawn(id int32) *GroundSpawn {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
return ml.spawns[id]
|
|
}
|
|
|
|
// GetByZone returns all spawns in a zone (O(1))
|
|
func (ml *MasterList) GetByZone(zoneID int32) []*GroundSpawn {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
return ml.byZone[zoneID] // Return slice directly for performance
|
|
}
|
|
|
|
// GetBySkill returns all spawns for a skill (O(1))
|
|
func (ml *MasterList) GetBySkill(skill string) []*GroundSpawn {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
return ml.bySkill[skill] // Return slice directly for performance
|
|
}
|
|
|
|
// GetByZoneAndSkill returns spawns matching both zone and skill (set intersection)
|
|
func (ml *MasterList) GetByZoneAndSkill(zoneID int32, skill string) []*GroundSpawn {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
|
|
zoneSpawns := ml.byZone[zoneID]
|
|
skillSpawns := ml.bySkill[skill]
|
|
|
|
// Use smaller set for iteration efficiency
|
|
if len(zoneSpawns) > len(skillSpawns) {
|
|
zoneSpawns, skillSpawns = skillSpawns, zoneSpawns
|
|
}
|
|
|
|
// Set intersection using map lookup
|
|
skillSet := make(map[*GroundSpawn]struct{}, len(skillSpawns))
|
|
for _, gs := range skillSpawns {
|
|
skillSet[gs] = struct{}{}
|
|
}
|
|
|
|
var result []*GroundSpawn
|
|
for _, gs := range zoneSpawns {
|
|
if _, exists := skillSet[gs]; exists {
|
|
result = append(result, gs)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetNearby returns spawns within radius of given coordinates using spatial grid
|
|
func (ml *MasterList) GetNearby(x, y float32, radius float32) []*GroundSpawn {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
|
|
// Calculate grid search bounds
|
|
minX := int32((x - radius) / ml.gridSize)
|
|
maxX := int32((x + radius) / ml.gridSize)
|
|
minY := int32((y - radius) / ml.gridSize)
|
|
maxY := int32((y + radius) / ml.gridSize)
|
|
|
|
var candidates []*GroundSpawn
|
|
|
|
// Check all grid cells in range
|
|
for gx := minX; gx <= maxX; gx++ {
|
|
for gy := minY; gy <= maxY; gy++ {
|
|
key := gridKey{x: gx, y: gy}
|
|
if spawns, exists := ml.spatialGrid[key]; exists {
|
|
candidates = append(candidates, spawns...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter by exact distance
|
|
radiusSquared := radius * radius
|
|
var result []*GroundSpawn
|
|
for _, gs := range candidates {
|
|
dx := gs.X - x
|
|
dy := gs.Y - y
|
|
if dx*dx+dy*dy <= radiusSquared {
|
|
result = append(result, gs)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetAvailableSpawns returns harvestable spawns using cached results
|
|
func (ml *MasterList) GetAvailableSpawns() []*GroundSpawn {
|
|
ml.mutex.Lock() // Need write lock to potentially update cache
|
|
defer ml.mutex.Unlock()
|
|
|
|
ml.refreshStateCache()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]*GroundSpawn, len(ml.availableSpawns))
|
|
copy(result, ml.availableSpawns)
|
|
return result
|
|
}
|
|
|
|
// GetDepletedSpawns returns depleted spawns using cached results
|
|
func (ml *MasterList) GetDepletedSpawns() []*GroundSpawn {
|
|
ml.mutex.Lock() // Need write lock to potentially update cache
|
|
defer ml.mutex.Unlock()
|
|
|
|
ml.refreshStateCache()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]*GroundSpawn, len(ml.depletedSpawns))
|
|
copy(result, ml.depletedSpawns)
|
|
return result
|
|
}
|
|
|
|
// GetStatistics returns system statistics using cached results
|
|
func (ml *MasterList) GetStatistics() *Statistics {
|
|
ml.mutex.Lock() // Need write lock to potentially update cache
|
|
defer ml.mutex.Unlock()
|
|
|
|
ml.refreshStatsCache()
|
|
|
|
// Return a copy to prevent external modification
|
|
return &Statistics{
|
|
TotalHarvests: ml.stats.TotalHarvests,
|
|
SuccessfulHarvests: ml.stats.SuccessfulHarvests,
|
|
RareItemsHarvested: ml.stats.RareItemsHarvested,
|
|
SkillUpsGenerated: ml.stats.SkillUpsGenerated,
|
|
HarvestsBySkill: ml.stats.HarvestsBySkill,
|
|
ActiveGroundSpawns: ml.stats.ActiveGroundSpawns,
|
|
GroundSpawnsByZone: ml.stats.GroundSpawnsByZone,
|
|
}
|
|
}
|
|
|
|
// RemoveGroundSpawn removes a spawn and updates all indices
|
|
func (ml *MasterList) RemoveGroundSpawn(id int32) bool {
|
|
ml.mutex.Lock()
|
|
defer ml.mutex.Unlock()
|
|
|
|
gs, exists := ml.spawns[id]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
// Remove from core storage
|
|
delete(ml.spawns, id)
|
|
|
|
// Remove from zone index
|
|
zoneSpawns := ml.byZone[gs.ZoneID]
|
|
for i, spawn := range zoneSpawns {
|
|
if spawn.GroundSpawnID == id {
|
|
ml.byZone[gs.ZoneID] = append(zoneSpawns[:i], zoneSpawns[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove from skill index
|
|
skillSpawns := ml.bySkill[gs.CollectionSkill]
|
|
for i, spawn := range skillSpawns {
|
|
if spawn.GroundSpawnID == id {
|
|
ml.bySkill[gs.CollectionSkill] = append(skillSpawns[:i], skillSpawns[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove from spatial grid
|
|
gridKey := ml.getGridKey(gs.X, gs.Y)
|
|
gridSpawns := ml.spatialGrid[gridKey]
|
|
for i, spawn := range gridSpawns {
|
|
if spawn.GroundSpawnID == id {
|
|
ml.spatialGrid[gridKey] = append(gridSpawns[:i], gridSpawns[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Invalidate state and stats caches
|
|
ml.stateStale = true
|
|
ml.statsStale = true
|
|
|
|
return true
|
|
}
|
|
|
|
// Size returns the total number of spawns
|
|
func (ml *MasterList) Size() int {
|
|
ml.mutex.RLock()
|
|
defer ml.mutex.RUnlock()
|
|
return len(ml.spawns)
|
|
}
|
|
|
|
// InvalidateStateCache marks the state and stats caches as stale
|
|
// Call this when spawn states change (e.g., after harvesting, respawning)
|
|
func (ml *MasterList) InvalidateStateCache() {
|
|
ml.mutex.Lock()
|
|
defer ml.mutex.Unlock()
|
|
ml.stateStale = true
|
|
ml.statsStale = true
|
|
} |