1004 lines
28 KiB
Go
1004 lines
28 KiB
Go
package ground_spawn
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"eq2emu/internal/database"
|
|
)
|
|
|
|
// GroundSpawn represents a harvestable resource node
|
|
type GroundSpawn struct {
|
|
// Database fields
|
|
ID int32 `json:"id" db:"id"`
|
|
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
|
Name string `json:"name" db:"name"`
|
|
CollectionSkill string `json:"collection_skill" db:"collection_skill"`
|
|
NumberHarvests int8 `json:"number_harvests" db:"number_harvests"`
|
|
AttemptsPerHarvest int8 `json:"attempts_per_harvest" db:"attempts_per_harvest"`
|
|
RandomizeHeading bool `json:"randomize_heading" db:"randomize_heading"`
|
|
RespawnTime int32 `json:"respawn_time" db:"respawn_time"`
|
|
|
|
// Position data
|
|
X float32 `json:"x" db:"x"`
|
|
Y float32 `json:"y" db:"y"`
|
|
Z float32 `json:"z" db:"z"`
|
|
Heading float32 `json:"heading" db:"heading"`
|
|
ZoneID int32 `json:"zone_id" db:"zone_id"`
|
|
GridID int32 `json:"grid_id" db:"grid_id"`
|
|
|
|
// State data
|
|
IsAlive bool `json:"is_alive"`
|
|
CurrentHarvests int8 `json:"current_harvests"`
|
|
LastHarvested time.Time `json:"last_harvested"`
|
|
NextRespawn time.Time `json:"next_respawn"`
|
|
|
|
// Associated data (loaded separately)
|
|
HarvestEntries []*HarvestEntry `json:"harvest_entries,omitempty"`
|
|
HarvestItems []*HarvestEntryItem `json:"harvest_items,omitempty"`
|
|
}
|
|
|
|
// HarvestEntry represents harvest table data from database
|
|
type HarvestEntry struct {
|
|
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
|
MinSkillLevel int16 `json:"min_skill_level" db:"min_skill_level"`
|
|
MinAdventureLevel int16 `json:"min_adventure_level" db:"min_adventure_level"`
|
|
BonusTable bool `json:"bonus_table" db:"bonus_table"`
|
|
Harvest1 float32 `json:"harvest1" db:"harvest1"`
|
|
Harvest3 float32 `json:"harvest3" db:"harvest3"`
|
|
Harvest5 float32 `json:"harvest5" db:"harvest5"`
|
|
HarvestImbue float32 `json:"harvest_imbue" db:"harvest_imbue"`
|
|
HarvestRare float32 `json:"harvest_rare" db:"harvest_rare"`
|
|
Harvest10 float32 `json:"harvest10" db:"harvest10"`
|
|
HarvestCoin float32 `json:"harvest_coin" db:"harvest_coin"`
|
|
}
|
|
|
|
// HarvestEntryItem represents items that can be harvested
|
|
type HarvestEntryItem struct {
|
|
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
|
ItemID int32 `json:"item_id" db:"item_id"`
|
|
IsRare int8 `json:"is_rare" db:"is_rare"`
|
|
GridID int32 `json:"grid_id" db:"grid_id"`
|
|
Quantity int16 `json:"quantity" db:"quantity"`
|
|
}
|
|
|
|
// HarvestResult represents the outcome of a harvest attempt
|
|
type HarvestResult struct {
|
|
Success bool `json:"success"`
|
|
HarvestType int8 `json:"harvest_type"`
|
|
ItemsAwarded []*HarvestedItem `json:"items_awarded"`
|
|
MessageText string `json:"message_text"`
|
|
SkillGained bool `json:"skill_gained"`
|
|
Error error `json:"error,omitempty"`
|
|
}
|
|
|
|
// HarvestedItem represents an item awarded from harvesting
|
|
type HarvestedItem struct {
|
|
ItemID int32 `json:"item_id"`
|
|
Quantity int16 `json:"quantity"`
|
|
IsRare bool `json:"is_rare"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Player interface for harvest operations
|
|
type Player interface {
|
|
GetLevel() int16
|
|
GetLocation() int32
|
|
GetName() string
|
|
}
|
|
|
|
// Skill interface for harvest operations
|
|
type Skill interface {
|
|
GetCurrentValue() int16
|
|
GetMaxValue() int16
|
|
}
|
|
|
|
// Logger interface for logging operations
|
|
type Logger interface {
|
|
LogInfo(system, format string, args ...interface{})
|
|
LogError(system, format string, args ...interface{})
|
|
LogDebug(system, format string, args ...interface{})
|
|
}
|
|
|
|
// Statistics holds ground spawn system statistics
|
|
type Statistics struct {
|
|
TotalHarvests int64 `json:"total_harvests"`
|
|
SuccessfulHarvests int64 `json:"successful_harvests"`
|
|
RareItemsHarvested int64 `json:"rare_items_harvested"`
|
|
SkillUpsGenerated int64 `json:"skill_ups_generated"`
|
|
HarvestsBySkill map[string]int64 `json:"harvests_by_skill"`
|
|
ActiveGroundSpawns int `json:"active_ground_spawns"`
|
|
GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"`
|
|
}
|
|
|
|
// GetID returns the ground spawn ID
|
|
func (gs *GroundSpawn) GetID() int32 {
|
|
return gs.GroundSpawnID
|
|
}
|
|
|
|
// IsGroundSpawn returns true (compatibility method)
|
|
func (gs *GroundSpawn) IsGroundSpawn() bool {
|
|
return true
|
|
}
|
|
|
|
// IsDepleted returns true if the ground spawn has no harvests remaining
|
|
func (gs *GroundSpawn) IsDepleted() bool {
|
|
return gs.CurrentHarvests <= 0
|
|
}
|
|
|
|
// IsAvailable returns true if the ground spawn can be harvested
|
|
func (gs *GroundSpawn) IsAvailable() bool {
|
|
return gs.CurrentHarvests > 0 && gs.IsAlive
|
|
}
|
|
|
|
// GetHarvestMessageName returns the appropriate harvest verb based on skill
|
|
func (gs *GroundSpawn) GetHarvestMessageName(presentTense bool, failure bool) string {
|
|
skill := strings.ToLower(gs.CollectionSkill)
|
|
|
|
switch skill {
|
|
case "gathering", "collecting":
|
|
if presentTense {
|
|
return "gather"
|
|
}
|
|
return "gathered"
|
|
|
|
case "mining":
|
|
if presentTense {
|
|
return "mine"
|
|
}
|
|
return "mined"
|
|
|
|
case "fishing":
|
|
if presentTense {
|
|
return "fish"
|
|
}
|
|
return "fished"
|
|
|
|
case "trapping":
|
|
if failure {
|
|
return "trap"
|
|
}
|
|
if presentTense {
|
|
return "acquire"
|
|
}
|
|
return "acquired"
|
|
|
|
case "foresting":
|
|
if presentTense {
|
|
return "forest"
|
|
}
|
|
return "forested"
|
|
|
|
default:
|
|
if presentTense {
|
|
return "collect"
|
|
}
|
|
return "collected"
|
|
}
|
|
}
|
|
|
|
// GetHarvestSpellType returns the spell type for harvesting
|
|
func (gs *GroundSpawn) GetHarvestSpellType() string {
|
|
skill := strings.ToLower(gs.CollectionSkill)
|
|
|
|
switch skill {
|
|
case "gathering", "collecting":
|
|
return SpellTypeGather
|
|
case "mining":
|
|
return SpellTypeMine
|
|
case "trapping":
|
|
return SpellTypeTrap
|
|
case "foresting":
|
|
return SpellTypeChop
|
|
case "fishing":
|
|
return SpellTypeFish
|
|
default:
|
|
return SpellTypeGather
|
|
}
|
|
}
|
|
|
|
// GetHarvestSpellName returns the spell name for harvesting
|
|
func (gs *GroundSpawn) GetHarvestSpellName() string {
|
|
skill := gs.CollectionSkill
|
|
|
|
if skill == SkillCollecting {
|
|
return SkillGathering
|
|
}
|
|
|
|
return skill
|
|
}
|
|
|
|
// ProcessHarvest handles the complex harvesting logic (preserves C++ algorithm)
|
|
func (gs *GroundSpawn) ProcessHarvest(player Player, skill Skill, totalSkill int16) (*HarvestResult, error) {
|
|
// Check if ground spawn is depleted
|
|
if gs.CurrentHarvests <= 0 {
|
|
return &HarvestResult{
|
|
Success: false,
|
|
MessageText: "This spawn has nothing more to harvest!",
|
|
}, nil
|
|
}
|
|
|
|
// Validate harvest data
|
|
if len(gs.HarvestEntries) == 0 {
|
|
return &HarvestResult{
|
|
Success: false,
|
|
MessageText: fmt.Sprintf("Error: No groundspawn entries assigned to groundspawn id: %d", gs.GroundSpawnID),
|
|
}, nil
|
|
}
|
|
|
|
if len(gs.HarvestItems) == 0 {
|
|
return &HarvestResult{
|
|
Success: false,
|
|
MessageText: fmt.Sprintf("Error: No groundspawn items assigned to groundspawn id: %d", gs.GroundSpawnID),
|
|
}, nil
|
|
}
|
|
|
|
// Check for collection skill
|
|
isCollection := gs.CollectionSkill == "Collecting"
|
|
|
|
result := &HarvestResult{
|
|
Success: true,
|
|
ItemsAwarded: make([]*HarvestedItem, 0),
|
|
}
|
|
|
|
// Process each harvest attempt (preserving C++ logic)
|
|
for attempt := int8(0); attempt < gs.AttemptsPerHarvest; attempt++ {
|
|
attemptResult := gs.processHarvestAttempt(player, skill, totalSkill, isCollection)
|
|
if attemptResult != nil {
|
|
result.ItemsAwarded = append(result.ItemsAwarded, attemptResult.ItemsAwarded...)
|
|
if attemptResult.SkillGained {
|
|
result.SkillGained = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decrement harvest count and update state
|
|
gs.CurrentHarvests--
|
|
gs.LastHarvested = time.Now()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// processHarvestAttempt handles a single harvest attempt (preserves C++ algorithm)
|
|
func (gs *GroundSpawn) processHarvestAttempt(player Player, skill Skill, totalSkill int16, isCollection bool) *HarvestResult {
|
|
// Filter available harvest tables based on player skill and level
|
|
availableTables := gs.filterHarvestTables(player, totalSkill)
|
|
if len(availableTables) == 0 {
|
|
return &HarvestResult{
|
|
Success: false,
|
|
MessageText: "You lack the skills to harvest this node!",
|
|
}
|
|
}
|
|
|
|
// Select harvest table based on skill roll (matching C++ algorithm)
|
|
selectedTable := gs.selectHarvestTable(availableTables, totalSkill)
|
|
if selectedTable == nil {
|
|
return &HarvestResult{
|
|
Success: false,
|
|
MessageText: "Failed to determine harvest table",
|
|
}
|
|
}
|
|
|
|
// Determine harvest type based on table percentages
|
|
harvestType := gs.determineHarvestType(selectedTable, isCollection)
|
|
if harvestType == HarvestTypeNone {
|
|
return &HarvestResult{
|
|
Success: false,
|
|
MessageText: fmt.Sprintf("You failed to %s anything from %s.",
|
|
gs.GetHarvestMessageName(true, true), gs.Name),
|
|
}
|
|
}
|
|
|
|
// Award items based on harvest type
|
|
items := gs.awardHarvestItems(harvestType, player)
|
|
|
|
// Handle skill progression (simplified for now)
|
|
skillGained := false // TODO: Implement skill progression
|
|
|
|
return &HarvestResult{
|
|
Success: len(items) > 0,
|
|
HarvestType: harvestType,
|
|
ItemsAwarded: items,
|
|
SkillGained: skillGained,
|
|
}
|
|
}
|
|
|
|
// filterHarvestTables filters tables based on player capabilities (preserves C++ logic)
|
|
func (gs *GroundSpawn) filterHarvestTables(player Player, totalSkill int16) []*HarvestEntry {
|
|
var filtered []*HarvestEntry
|
|
|
|
for _, entry := range gs.HarvestEntries {
|
|
// Check skill requirement
|
|
if entry.MinSkillLevel > totalSkill {
|
|
continue
|
|
}
|
|
|
|
// Check level requirement for bonus tables
|
|
if entry.BonusTable && player.GetLevel() < entry.MinAdventureLevel {
|
|
continue
|
|
}
|
|
|
|
filtered = append(filtered, entry)
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// selectHarvestTable selects a harvest table based on skill level (preserves C++ algorithm)
|
|
func (gs *GroundSpawn) selectHarvestTable(tables []*HarvestEntry, totalSkill int16) *HarvestEntry {
|
|
if len(tables) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Find lowest skill requirement (matching C++ logic)
|
|
lowestSkill := int16(32767)
|
|
for _, table := range tables {
|
|
if table.MinSkillLevel < lowestSkill {
|
|
lowestSkill = table.MinSkillLevel
|
|
}
|
|
}
|
|
|
|
// Roll for table selection (matching C++ MakeRandomInt)
|
|
tableChoice := int16(rand.Intn(int(totalSkill-lowestSkill+1))) + lowestSkill
|
|
|
|
// Find best matching table (matching C++ logic)
|
|
var bestTable *HarvestEntry
|
|
bestScore := int16(0)
|
|
|
|
for _, table := range tables {
|
|
if tableChoice >= table.MinSkillLevel && table.MinSkillLevel > bestScore {
|
|
bestTable = table
|
|
bestScore = table.MinSkillLevel
|
|
}
|
|
}
|
|
|
|
// If multiple tables match, pick randomly (matching C++ logic)
|
|
var matches []*HarvestEntry
|
|
for _, table := range tables {
|
|
if table.MinSkillLevel == bestScore {
|
|
matches = append(matches, table)
|
|
}
|
|
}
|
|
|
|
if len(matches) > 1 {
|
|
return matches[rand.Intn(len(matches))]
|
|
}
|
|
|
|
return bestTable
|
|
}
|
|
|
|
// determineHarvestType determines what type of harvest occurs (preserves C++ algorithm)
|
|
func (gs *GroundSpawn) determineHarvestType(table *HarvestEntry, isCollection bool) int8 {
|
|
chance := rand.Float32() * 100.0
|
|
|
|
// Collection items always get 1 item (matching C++ logic)
|
|
if isCollection {
|
|
return HarvestType1Item
|
|
}
|
|
|
|
// Check harvest types in order of rarity (matching C++ order)
|
|
if chance <= table.Harvest10 {
|
|
return HarvestType10AndRare
|
|
}
|
|
if chance <= table.HarvestRare {
|
|
return HarvestTypeRare
|
|
}
|
|
if chance <= table.HarvestImbue {
|
|
return HarvestTypeImbue
|
|
}
|
|
if chance <= table.Harvest5 {
|
|
return HarvestType5Items
|
|
}
|
|
if chance <= table.Harvest3 {
|
|
return HarvestType3Items
|
|
}
|
|
if chance <= table.Harvest1 {
|
|
return HarvestType1Item
|
|
}
|
|
|
|
return HarvestTypeNone
|
|
}
|
|
|
|
// awardHarvestItems awards items based on harvest type (preserves C++ algorithm)
|
|
func (gs *GroundSpawn) awardHarvestItems(harvestType int8, player Player) []*HarvestedItem {
|
|
var items []*HarvestedItem
|
|
|
|
// Filter items based on harvest type and player location (matching C++ logic)
|
|
normalItems := gs.filterItems(gs.HarvestItems, ItemRarityNormal, player.GetLocation())
|
|
rareItems := gs.filterItems(gs.HarvestItems, ItemRarityRare, player.GetLocation())
|
|
imbueItems := gs.filterItems(gs.HarvestItems, ItemRarityImbue, player.GetLocation())
|
|
|
|
switch harvestType {
|
|
case HarvestType1Item:
|
|
items = gs.selectRandomItems(normalItems, 1)
|
|
case HarvestType3Items:
|
|
items = gs.selectRandomItems(normalItems, 3)
|
|
case HarvestType5Items:
|
|
items = gs.selectRandomItems(normalItems, 5)
|
|
case HarvestTypeImbue:
|
|
items = gs.selectRandomItems(imbueItems, 1)
|
|
case HarvestTypeRare:
|
|
items = gs.selectRandomItems(rareItems, 1)
|
|
case HarvestType10AndRare:
|
|
normal := gs.selectRandomItems(normalItems, 10)
|
|
rare := gs.selectRandomItems(rareItems, 1)
|
|
items = append(normal, rare...)
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
// filterItems filters items by rarity and grid restriction (preserves C++ logic)
|
|
func (gs *GroundSpawn) filterItems(items []*HarvestEntryItem, rarity int8, playerGrid int32) []*HarvestEntryItem {
|
|
var filtered []*HarvestEntryItem
|
|
|
|
for _, item := range items {
|
|
if item.IsRare != rarity {
|
|
continue
|
|
}
|
|
|
|
// Check grid restriction (matching C++ logic)
|
|
if item.GridID != 0 && item.GridID != playerGrid {
|
|
continue
|
|
}
|
|
|
|
filtered = append(filtered, item)
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// selectRandomItems randomly selects items from available list (preserves C++ logic)
|
|
func (gs *GroundSpawn) selectRandomItems(items []*HarvestEntryItem, quantity int16) []*HarvestedItem {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var result []*HarvestedItem
|
|
|
|
for i := int16(0); i < quantity; i++ {
|
|
selectedItem := items[rand.Intn(len(items))]
|
|
|
|
harvestedItem := &HarvestedItem{
|
|
ItemID: selectedItem.ItemID,
|
|
Quantity: selectedItem.Quantity,
|
|
IsRare: selectedItem.IsRare == ItemRarityRare,
|
|
Name: fmt.Sprintf("Item_%d", selectedItem.ItemID), // Placeholder for item name lookup
|
|
}
|
|
|
|
result = append(result, harvestedItem)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Respawn resets the ground spawn to harvestable state
|
|
func (gs *GroundSpawn) Respawn() {
|
|
// Reset harvest count to default
|
|
gs.CurrentHarvests = gs.NumberHarvests
|
|
|
|
// Randomize heading if configured
|
|
if gs.RandomizeHeading {
|
|
gs.Heading = rand.Float32() * 360.0
|
|
}
|
|
|
|
// Mark as alive and update times
|
|
gs.IsAlive = true
|
|
gs.NextRespawn = time.Time{} // Clear next respawn time
|
|
}
|
|
|
|
// GroundSpawnManager provides unified management of the ground spawn system
|
|
type GroundSpawnManager struct {
|
|
// Core storage with specialized indices for O(1) lookups
|
|
spawns map[int32]*GroundSpawn // ID -> GroundSpawn
|
|
byZone map[int32][]*GroundSpawn // Zone ID -> spawns
|
|
bySkill map[string][]*GroundSpawn // Skill -> spawns
|
|
|
|
// External dependencies
|
|
database *database.Database
|
|
logger Logger
|
|
mutex sync.RWMutex
|
|
|
|
// Statistics
|
|
totalHarvests int64
|
|
successfulHarvests int64
|
|
rareItemsHarvested int64
|
|
skillUpsGenerated int64
|
|
harvestsBySkill map[string]int64
|
|
}
|
|
|
|
// NewGroundSpawnManager creates a new unified ground spawn manager
|
|
func NewGroundSpawnManager(db *database.Database, logger Logger) *GroundSpawnManager {
|
|
return &GroundSpawnManager{
|
|
spawns: make(map[int32]*GroundSpawn),
|
|
byZone: make(map[int32][]*GroundSpawn),
|
|
bySkill: make(map[string][]*GroundSpawn),
|
|
database: db,
|
|
logger: logger,
|
|
harvestsBySkill: make(map[string]int64),
|
|
}
|
|
}
|
|
|
|
// Initialize loads ground spawns from database
|
|
func (gsm *GroundSpawnManager) Initialize(ctx context.Context) error {
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogInfo("ground_spawn", "Initializing ground spawn manager...")
|
|
}
|
|
|
|
if gsm.database == nil {
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogInfo("ground_spawn", "No database provided, starting with empty spawn list")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load all ground spawns
|
|
if err := gsm.loadGroundSpawnsFromDB(); err != nil {
|
|
return fmt.Errorf("failed to load ground spawns from database: %w", err)
|
|
}
|
|
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogInfo("ground_spawn", "Loaded %d ground spawns from database", len(gsm.spawns))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadGroundSpawnsFromDB loads all ground spawns from database (internal method)
|
|
func (gsm *GroundSpawnManager) loadGroundSpawnsFromDB() error {
|
|
// Create ground_spawns table if it doesn't exist
|
|
_, err := gsm.database.Exec(`
|
|
CREATE TABLE IF NOT EXISTS ground_spawns (
|
|
id INTEGER PRIMARY KEY,
|
|
groundspawn_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
collection_skill TEXT,
|
|
number_harvests INTEGER DEFAULT 1,
|
|
attempts_per_harvest INTEGER DEFAULT 1,
|
|
randomize_heading BOOLEAN DEFAULT 1,
|
|
respawn_time INTEGER DEFAULT 300,
|
|
x REAL NOT NULL,
|
|
y REAL NOT NULL,
|
|
z REAL NOT NULL,
|
|
heading REAL DEFAULT 0,
|
|
zone_id INTEGER NOT NULL,
|
|
grid_id INTEGER DEFAULT 0
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create ground_spawns table: %w", err)
|
|
}
|
|
|
|
rows, err := gsm.database.Query(`
|
|
SELECT id, groundspawn_id, name, collection_skill, number_harvests,
|
|
attempts_per_harvest, randomize_heading, respawn_time,
|
|
x, y, z, heading, zone_id, grid_id
|
|
FROM ground_spawns ORDER BY id
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query ground spawns: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
count := 0
|
|
for rows.Next() {
|
|
gs := &GroundSpawn{
|
|
HarvestEntries: make([]*HarvestEntry, 0),
|
|
HarvestItems: make([]*HarvestEntryItem, 0),
|
|
IsAlive: true,
|
|
}
|
|
|
|
err := rows.Scan(&gs.ID, &gs.GroundSpawnID, &gs.Name, &gs.CollectionSkill,
|
|
&gs.NumberHarvests, &gs.AttemptsPerHarvest, &gs.RandomizeHeading,
|
|
&gs.RespawnTime, &gs.X, &gs.Y, &gs.Z, &gs.Heading, &gs.ZoneID, &gs.GridID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to scan ground spawn: %w", err)
|
|
}
|
|
|
|
// Initialize state
|
|
gs.CurrentHarvests = gs.NumberHarvests
|
|
|
|
// Load harvest entries and items
|
|
if err := gsm.loadHarvestData(gs); err != nil {
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogError("ground_spawn", "Failed to load harvest data for spawn %d: %v", gs.GroundSpawnID, err)
|
|
}
|
|
}
|
|
|
|
if err := gsm.addGroundSpawnToIndices(gs); err != nil {
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogError("ground_spawn", "Failed to add ground spawn %d: %v", gs.GroundSpawnID, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
count++
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadHarvestData loads harvest entries and items for a ground spawn (internal method)
|
|
func (gsm *GroundSpawnManager) loadHarvestData(gs *GroundSpawn) error {
|
|
// Load harvest entries
|
|
if err := gsm.loadHarvestEntries(gs); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load harvest items
|
|
if err := gsm.loadHarvestItems(gs); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadHarvestEntries loads harvest entries for a ground spawn (internal method)
|
|
func (gsm *GroundSpawnManager) loadHarvestEntries(gs *GroundSpawn) error {
|
|
rows, err := gsm.database.Query(`
|
|
SELECT groundspawn_id, min_skill_level, min_adventure_level, bonus_table,
|
|
harvest1, harvest3, harvest5, harvest_imbue, harvest_rare, harvest10, harvest_coin
|
|
FROM groundspawn_entries WHERE groundspawn_id = ?
|
|
`, gs.GroundSpawnID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
entry := &HarvestEntry{}
|
|
err := rows.Scan(&entry.GroundSpawnID, &entry.MinSkillLevel, &entry.MinAdventureLevel,
|
|
&entry.BonusTable, &entry.Harvest1, &entry.Harvest3, &entry.Harvest5,
|
|
&entry.HarvestImbue, &entry.HarvestRare, &entry.Harvest10, &entry.HarvestCoin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gs.HarvestEntries = append(gs.HarvestEntries, entry)
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadHarvestItems loads harvest items for a ground spawn (internal method)
|
|
func (gsm *GroundSpawnManager) loadHarvestItems(gs *GroundSpawn) error {
|
|
rows, err := gsm.database.Query(`
|
|
SELECT groundspawn_id, item_id, is_rare, grid_id, quantity
|
|
FROM groundspawn_items WHERE groundspawn_id = ?
|
|
`, gs.GroundSpawnID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
item := &HarvestEntryItem{}
|
|
err := rows.Scan(&item.GroundSpawnID, &item.ItemID, &item.IsRare, &item.GridID, &item.Quantity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gs.HarvestItems = append(gs.HarvestItems, item)
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
// addGroundSpawnToIndices adds a ground spawn to all internal indices (internal method)
|
|
func (gsm *GroundSpawnManager) addGroundSpawnToIndices(gs *GroundSpawn) error {
|
|
if gs == nil {
|
|
return fmt.Errorf("ground spawn cannot be nil")
|
|
}
|
|
|
|
// Check if exists
|
|
if _, exists := gsm.spawns[gs.GroundSpawnID]; exists {
|
|
return fmt.Errorf("ground spawn with ID %d already exists", gs.GroundSpawnID)
|
|
}
|
|
|
|
// Add to core storage
|
|
gsm.spawns[gs.GroundSpawnID] = gs
|
|
|
|
// Add to zone index
|
|
gsm.byZone[gs.ZoneID] = append(gsm.byZone[gs.ZoneID], gs)
|
|
|
|
// Add to skill index
|
|
gsm.bySkill[gs.CollectionSkill] = append(gsm.bySkill[gs.CollectionSkill], gs)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetGroundSpawn returns a ground spawn by ID
|
|
func (gsm *GroundSpawnManager) GetGroundSpawn(groundSpawnID int32) *GroundSpawn {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
return gsm.spawns[groundSpawnID]
|
|
}
|
|
|
|
// GetGroundSpawnsByZone returns all ground spawns in a zone (O(1))
|
|
func (gsm *GroundSpawnManager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
|
|
spawns, exists := gsm.byZone[zoneID]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]*GroundSpawn, len(spawns))
|
|
copy(result, spawns)
|
|
return result
|
|
}
|
|
|
|
// GetGroundSpawnsBySkill returns all ground spawns for a skill (O(1))
|
|
func (gsm *GroundSpawnManager) GetGroundSpawnsBySkill(skill string) []*GroundSpawn {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
|
|
spawns, exists := gsm.bySkill[skill]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]*GroundSpawn, len(spawns))
|
|
copy(result, spawns)
|
|
return result
|
|
}
|
|
|
|
// GetGroundSpawnsByZoneAndSkill returns spawns matching both zone and skill
|
|
func (gsm *GroundSpawnManager) GetGroundSpawnsByZoneAndSkill(zoneID int32, skill string) []*GroundSpawn {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
|
|
zoneSpawns := gsm.byZone[zoneID]
|
|
skillSpawns := gsm.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
|
|
}
|
|
|
|
// GetAvailableGroundSpawns returns all harvestable ground spawns
|
|
func (gsm *GroundSpawnManager) GetAvailableGroundSpawns() []*GroundSpawn {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
|
|
var available []*GroundSpawn
|
|
for _, gs := range gsm.spawns {
|
|
if gs.IsAvailable() {
|
|
available = append(available, gs)
|
|
}
|
|
}
|
|
|
|
return available
|
|
}
|
|
|
|
// GetAvailableGroundSpawnsByZone returns harvestable ground spawns in a zone
|
|
func (gsm *GroundSpawnManager) GetAvailableGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
|
|
zoneSpawns := gsm.byZone[zoneID]
|
|
var available []*GroundSpawn
|
|
|
|
for _, gs := range zoneSpawns {
|
|
if gs.IsAvailable() {
|
|
available = append(available, gs)
|
|
}
|
|
}
|
|
|
|
return available
|
|
}
|
|
|
|
// ProcessHarvest processes a harvest attempt and updates statistics
|
|
func (gsm *GroundSpawnManager) ProcessHarvest(groundSpawnID int32, player Player, skill Skill, totalSkill int16) (*HarvestResult, error) {
|
|
gsm.mutex.Lock()
|
|
defer gsm.mutex.Unlock()
|
|
|
|
gs := gsm.spawns[groundSpawnID]
|
|
if gs == nil {
|
|
return nil, fmt.Errorf("ground spawn %d not found", groundSpawnID)
|
|
}
|
|
|
|
// Process harvest
|
|
result, err := gs.ProcessHarvest(player, skill, totalSkill)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update statistics
|
|
gsm.totalHarvests++
|
|
if result.Success {
|
|
gsm.successfulHarvests++
|
|
gsm.harvestsBySkill[gs.CollectionSkill]++
|
|
|
|
// Count rare items
|
|
for _, item := range result.ItemsAwarded {
|
|
if item.IsRare {
|
|
gsm.rareItemsHarvested++
|
|
}
|
|
}
|
|
|
|
if result.SkillGained {
|
|
gsm.skillUpsGenerated++
|
|
}
|
|
}
|
|
|
|
// Log harvest if logger available
|
|
if gsm.logger != nil {
|
|
if result.Success {
|
|
gsm.logger.LogInfo("ground_spawn", "Player %s harvested %d items from spawn %d",
|
|
player.GetName(), len(result.ItemsAwarded), groundSpawnID)
|
|
} else {
|
|
gsm.logger.LogDebug("ground_spawn", "Player %s failed to harvest spawn %d: %s",
|
|
player.GetName(), groundSpawnID, result.MessageText)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// RespawnGroundSpawn respawns a specific ground spawn
|
|
func (gsm *GroundSpawnManager) RespawnGroundSpawn(groundSpawnID int32) bool {
|
|
gsm.mutex.Lock()
|
|
defer gsm.mutex.Unlock()
|
|
|
|
gs := gsm.spawns[groundSpawnID]
|
|
if gs == nil {
|
|
return false
|
|
}
|
|
|
|
gs.Respawn()
|
|
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogDebug("ground_spawn", "Respawned ground spawn %d", groundSpawnID)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// GetStatistics returns ground spawn system statistics
|
|
func (gsm *GroundSpawnManager) GetStatistics() *Statistics {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
|
|
// Count active spawns and spawns by zone
|
|
activeSpawns := 0
|
|
spawnsByZone := make(map[int32]int)
|
|
|
|
for _, gs := range gsm.spawns {
|
|
if gs.IsAvailable() {
|
|
activeSpawns++
|
|
}
|
|
spawnsByZone[gs.ZoneID]++
|
|
}
|
|
|
|
// Copy harvests by skill to prevent external modification
|
|
harvestsBySkill := make(map[string]int64)
|
|
for skill, count := range gsm.harvestsBySkill {
|
|
harvestsBySkill[skill] = count
|
|
}
|
|
|
|
return &Statistics{
|
|
TotalHarvests: gsm.totalHarvests,
|
|
SuccessfulHarvests: gsm.successfulHarvests,
|
|
RareItemsHarvested: gsm.rareItemsHarvested,
|
|
SkillUpsGenerated: gsm.skillUpsGenerated,
|
|
HarvestsBySkill: harvestsBySkill,
|
|
ActiveGroundSpawns: activeSpawns,
|
|
GroundSpawnsByZone: spawnsByZone,
|
|
}
|
|
}
|
|
|
|
// GetGroundSpawnCount returns the total number of ground spawns
|
|
func (gsm *GroundSpawnManager) GetGroundSpawnCount() int32 {
|
|
gsm.mutex.RLock()
|
|
defer gsm.mutex.RUnlock()
|
|
return int32(len(gsm.spawns))
|
|
}
|
|
|
|
// AddGroundSpawn adds a new ground spawn with database persistence
|
|
func (gsm *GroundSpawnManager) AddGroundSpawn(gs *GroundSpawn) error {
|
|
if gs == nil {
|
|
return fmt.Errorf("ground spawn cannot be nil")
|
|
}
|
|
|
|
gsm.mutex.Lock()
|
|
defer gsm.mutex.Unlock()
|
|
|
|
// Add to indices
|
|
if err := gsm.addGroundSpawnToIndices(gs); err != nil {
|
|
return fmt.Errorf("failed to add ground spawn to indices: %w", err)
|
|
}
|
|
|
|
// Save to database if available
|
|
if gsm.database != nil {
|
|
if err := gsm.saveGroundSpawnToDBInternal(gs); err != nil {
|
|
// Remove from indices if save failed
|
|
gsm.removeGroundSpawnFromIndicesInternal(gs.GroundSpawnID)
|
|
return fmt.Errorf("failed to save ground spawn to database: %w", err)
|
|
}
|
|
}
|
|
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogInfo("ground_spawn", "Added ground spawn %d: %s in zone %d",
|
|
gs.GroundSpawnID, gs.Name, gs.ZoneID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// saveGroundSpawnToDBInternal saves a ground spawn to database (internal method, assumes lock held)
|
|
func (gsm *GroundSpawnManager) saveGroundSpawnToDBInternal(gs *GroundSpawn) error {
|
|
query := `INSERT OR REPLACE INTO ground_spawns
|
|
(groundspawn_id, name, collection_skill, number_harvests, attempts_per_harvest,
|
|
randomize_heading, respawn_time, x, y, z, heading, zone_id, grid_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
_, err := gsm.database.Exec(query, gs.GroundSpawnID, gs.Name, gs.CollectionSkill,
|
|
gs.NumberHarvests, gs.AttemptsPerHarvest, gs.RandomizeHeading, gs.RespawnTime,
|
|
gs.X, gs.Y, gs.Z, gs.Heading, gs.ZoneID, gs.GridID)
|
|
return err
|
|
}
|
|
|
|
// removeGroundSpawnFromIndicesInternal removes ground spawn from all indices (internal method, assumes lock held)
|
|
func (gsm *GroundSpawnManager) removeGroundSpawnFromIndicesInternal(groundSpawnID int32) {
|
|
gs, exists := gsm.spawns[groundSpawnID]
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
// Remove from core storage
|
|
delete(gsm.spawns, groundSpawnID)
|
|
|
|
// Remove from zone index
|
|
zoneSpawns := gsm.byZone[gs.ZoneID]
|
|
for i, spawn := range zoneSpawns {
|
|
if spawn.GroundSpawnID == groundSpawnID {
|
|
gsm.byZone[gs.ZoneID] = append(zoneSpawns[:i], zoneSpawns[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove from skill index
|
|
skillSpawns := gsm.bySkill[gs.CollectionSkill]
|
|
for i, spawn := range skillSpawns {
|
|
if spawn.GroundSpawnID == groundSpawnID {
|
|
gsm.bySkill[gs.CollectionSkill] = append(skillSpawns[:i], skillSpawns[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the manager
|
|
func (gsm *GroundSpawnManager) Shutdown() {
|
|
if gsm.logger != nil {
|
|
gsm.logger.LogInfo("ground_spawn", "Shutting down ground spawn manager...")
|
|
}
|
|
|
|
gsm.mutex.Lock()
|
|
defer gsm.mutex.Unlock()
|
|
|
|
// Clear all data
|
|
gsm.spawns = make(map[int32]*GroundSpawn)
|
|
gsm.byZone = make(map[int32][]*GroundSpawn)
|
|
gsm.bySkill = make(map[string][]*GroundSpawn)
|
|
gsm.harvestsBySkill = make(map[string]int64)
|
|
} |