package ground_spawn import ( "fmt" "math/rand" "strings" "eq2emu/internal/spawn" ) // NewGroundSpawn creates a new ground spawn instance func NewGroundSpawn(config GroundSpawnConfig) *GroundSpawn { baseSpawn := spawn.NewSpawn() gs := &GroundSpawn{ Spawn: baseSpawn, numberHarvests: config.NumberHarvests, numAttemptsPerHarvest: config.AttemptsPerHarvest, groundspawnID: config.GroundSpawnID, collectionSkill: config.CollectionSkill, randomizeHeading: config.RandomizeHeading, } // Configure base spawn properties gs.SetName(config.Name) gs.SetSpawnType(DefaultSpawnType) gs.SetDifficulty(DefaultDifficulty) gs.SetState(DefaultState) // Set position gs.SetX(config.Location.X) gs.SetY(config.Location.Y) gs.SetZ(config.Location.Z) if config.RandomizeHeading { gs.SetHeading(rand.Float32() * 360.0) } else { gs.SetHeading(config.Location.Heading) } return gs } // Copy creates a deep copy of the ground spawn func (gs *GroundSpawn) Copy() *GroundSpawn { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() newSpawn := &GroundSpawn{ Spawn: gs.Spawn.Copy().(*spawn.Spawn), numberHarvests: gs.numberHarvests, numAttemptsPerHarvest: gs.numAttemptsPerHarvest, groundspawnID: gs.groundspawnID, collectionSkill: gs.collectionSkill, randomizeHeading: gs.randomizeHeading, } return newSpawn } // IsGroundSpawn returns true (implements spawn interface) func (gs *GroundSpawn) IsGroundSpawn() bool { return true } // GetNumberHarvests returns the number of harvests remaining func (gs *GroundSpawn) GetNumberHarvests() int8 { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() return gs.numberHarvests } // SetNumberHarvests sets the number of harvests remaining func (gs *GroundSpawn) SetNumberHarvests(val int8) { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() gs.numberHarvests = val } // GetAttemptsPerHarvest returns attempts per harvest session func (gs *GroundSpawn) GetAttemptsPerHarvest() int8 { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() return gs.numAttemptsPerHarvest } // SetAttemptsPerHarvest sets attempts per harvest session func (gs *GroundSpawn) SetAttemptsPerHarvest(val int8) { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() gs.numAttemptsPerHarvest = val } // GetGroundSpawnEntryID returns the database entry ID func (gs *GroundSpawn) GetGroundSpawnEntryID() int32 { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() return gs.groundspawnID } // SetGroundSpawnEntryID sets the database entry ID func (gs *GroundSpawn) SetGroundSpawnEntryID(val int32) { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() gs.groundspawnID = val } // GetCollectionSkill returns the required harvesting skill func (gs *GroundSpawn) GetCollectionSkill() string { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() return gs.collectionSkill } // SetCollectionSkill sets the required harvesting skill func (gs *GroundSpawn) SetCollectionSkill(skill string) { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() gs.collectionSkill = skill } // GetRandomizeHeading returns whether heading should be randomized func (gs *GroundSpawn) GetRandomizeHeading() bool { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() return gs.randomizeHeading } // SetRandomizeHeading sets whether heading should be randomized func (gs *GroundSpawn) SetRandomizeHeading(val bool) { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() gs.randomizeHeading = val } // IsDepleted returns true if the ground spawn has no harvests remaining func (gs *GroundSpawn) IsDepleted() bool { return gs.GetNumberHarvests() <= 0 } // IsAvailable returns true if the ground spawn can be harvested func (gs *GroundSpawn) IsAvailable() bool { return gs.GetNumberHarvests() > 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.GetCollectionSkill()) 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.GetCollectionSkill()) 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.GetCollectionSkill() if skill == SkillCollecting { return SkillGathering } return skill } // ProcessHarvest handles the complex harvesting logic func (gs *GroundSpawn) ProcessHarvest(context *HarvestContext) (*HarvestResult, error) { if context == nil { return nil, fmt.Errorf("harvest context cannot be nil") } if context.Player == nil { return nil, fmt.Errorf("player cannot be nil") } gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() // Check if ground spawn is depleted if gs.numberHarvests <= 0 { return &HarvestResult{ Success: false, MessageText: "This spawn has nothing more to harvest!", }, nil } // Validate harvest data if context.GroundSpawnEntries == nil || len(context.GroundSpawnEntries) == 0 { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: No groundspawn entries assigned to groundspawn id: %d", gs.groundspawnID), }, nil } if context.GroundSpawnItems == nil || len(context.GroundSpawnItems) == 0 { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: No groundspawn items assigned to groundspawn id: %d", gs.groundspawnID), }, nil } // Validate player skill if context.PlayerSkill == nil { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: You do not have the '%s' skill!", gs.collectionSkill), }, nil } result := &HarvestResult{ Success: true, ItemsAwarded: make([]*HarvestedItem, 0), } // Process each harvest attempt for attempt := int8(0); attempt < gs.numAttemptsPerHarvest; attempt++ { attemptResult := gs.processHarvestAttempt(context) if attemptResult != nil { result.ItemsAwarded = append(result.ItemsAwarded, attemptResult.ItemsAwarded...) if attemptResult.SkillGained { result.SkillGained = true } } } // Decrement harvest count gs.numberHarvests-- return result, nil } // processHarvestAttempt handles a single harvest attempt func (gs *GroundSpawn) processHarvestAttempt(context *HarvestContext) *HarvestResult { // Filter available harvest tables based on player skill and level availableTables := gs.filterHarvestTables(context) if len(availableTables) == 0 { return &HarvestResult{ Success: false, MessageText: "You lack the skills to harvest this node!", } } // Select harvest table based on skill roll selectedTable := gs.selectHarvestTable(availableTables, context.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, context.IsCollection) if harvestType == HarvestTypeNone { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("You failed to %s anything from %s.", gs.GetHarvestMessageName(true, true), gs.GetName()), } } // Award items based on harvest type items := gs.awardHarvestItems(harvestType, context.GroundSpawnItems, context.Player) // Handle skill progression skillGained := gs.handleSkillProgression(context, selectedTable) return &HarvestResult{ Success: len(items) > 0, HarvestType: harvestType, ItemsAwarded: items, SkillGained: skillGained, } } // filterHarvestTables filters tables based on player capabilities func (gs *GroundSpawn) filterHarvestTables(context *HarvestContext) []*GroundSpawnEntry { var filtered []*GroundSpawnEntry for _, entry := range context.GroundSpawnEntries { // Check skill requirement if entry.MinSkillLevel > context.TotalSkill { continue } // Check level requirement for bonus tables if entry.BonusTable && context.Player.GetLevel() < entry.MinAdventureLevel { continue } filtered = append(filtered, entry) } return filtered } // selectHarvestTable selects a harvest table based on skill level func (gs *GroundSpawn) selectHarvestTable(tables []*GroundSpawnEntry, totalSkill int16) *GroundSpawnEntry { if len(tables) == 0 { return nil } // Find lowest skill requirement lowestSkill := int16(32767) for _, table := range tables { if table.MinSkillLevel < lowestSkill { lowestSkill = table.MinSkillLevel } } // Roll for table selection tableChoice := int16(rand.Intn(int(totalSkill-lowestSkill+1))) + lowestSkill // Find best matching table var bestTable *GroundSpawnEntry 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 var matches []*GroundSpawnEntry 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 func (gs *GroundSpawn) determineHarvestType(table *GroundSpawnEntry, isCollection bool) int8 { chance := rand.Float32() * 100.0 // Collection items always get 1 item if isCollection { return HarvestType1Item } // Check harvest types in order of rarity (most rare first) 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 func (gs *GroundSpawn) awardHarvestItems(harvestType int8, availableItems []*GroundSpawnEntryItem, player *Player) []*HarvestedItem { var items []*HarvestedItem // Filter items based on harvest type and player location normalItems := gs.filterItems(availableItems, ItemRarityNormal, player.GetLocation()) rareItems := gs.filterItems(availableItems, ItemRarityRare, player.GetLocation()) imbueItems := gs.filterItems(availableItems, 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 func (gs *GroundSpawn) filterItems(items []*GroundSpawnEntryItem, rarity int8, playerGrid int32) []*GroundSpawnEntryItem { var filtered []*GroundSpawnEntryItem for _, item := range items { if item.IsRare != rarity { continue } // Check grid restriction if item.GridID != 0 && item.GridID != playerGrid { continue } filtered = append(filtered, item) } return filtered } // selectRandomItems randomly selects items from available list func (gs *GroundSpawn) selectRandomItems(items []*GroundSpawnEntryItem, 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 } result = append(result, harvestedItem) } return result } // handleSkillProgression manages skill increases from harvesting func (gs *GroundSpawn) handleSkillProgression(context *HarvestContext, table *GroundSpawnEntry) bool { if context.IsCollection { return false // Collections don't give skill } if context.PlayerSkill == nil { return false } // Check if player skill is already at max for this node maxSkillAllowed := int16(float32(context.MaxSkillRequired) * 1.0) // TODO: Use skill multiplier rule if context.PlayerSkill.GetCurrentValue() >= maxSkillAllowed { return false } // Award skill increase (placeholder implementation) // TODO: Integrate with actual skill system when available return true } // HandleUse processes player interaction with the ground spawn func (gs *GroundSpawn) HandleUse(client Client, useType string) error { if client == nil { return fmt.Errorf("client cannot be nil") } gs.harvestUseMutex.Lock() defer gs.harvestUseMutex.Unlock() // Check spawn access requirements if !gs.MeetsSpawnAccessRequirements(client.GetPlayer()) { return nil // Silently ignore if requirements not met } // Normalize use type useType = strings.ToLower(strings.TrimSpace(useType)) // Handle older clients that don't send use type if client.GetVersion() <= 561 && useType == "" { useType = gs.GetHarvestSpellType() } // Check if this is a harvest action expectedSpellType := gs.GetHarvestSpellType() if useType == expectedSpellType { return gs.handleHarvestUse(client) } // Handle other command interactions if gs.HasCommandIcon() { return gs.handleCommandUse(client, useType) } return nil } // handleHarvestUse processes harvest-specific use func (gs *GroundSpawn) handleHarvestUse(client Client) error { spellName := gs.GetHarvestSpellName() // TODO: Integrate with spell system when available // spell := masterSpellList.GetSpellByName(spellName) // if spell != nil { // zone.ProcessSpell(spell, player, target, true, true) // } if client.GetLogger() != nil { client.GetLogger().LogDebug("Player %s attempting to harvest %s using spell %s", client.GetPlayer().GetName(), gs.GetName(), spellName) } return nil } // handleCommandUse processes command-specific use func (gs *GroundSpawn) handleCommandUse(client Client, command string) error { // TODO: Integrate with entity command system when available // entityCommand := gs.FindEntityCommand(command) // if entityCommand != nil { // zone.ProcessEntityCommand(entityCommand, player, target) // } if client.GetLogger() != nil { client.GetLogger().LogDebug("Player %s using command %s on %s", client.GetPlayer().GetName(), command, gs.GetName()) } return nil } // Serialize creates a packet representation of the ground spawn func (gs *GroundSpawn) Serialize(player *Player, version int16) ([]byte, error) { // Use base spawn serialization return gs.Spawn.Serialize(player, version) } // Respawn resets the ground spawn to harvestable state func (gs *GroundSpawn) Respawn() { gs.harvestMutex.Lock() defer gs.harvestMutex.Unlock() // Reset harvest count to default gs.numberHarvests = DefaultNumberHarvests // Randomize heading if configured if gs.randomizeHeading { gs.SetHeading(rand.Float32() * 360.0) } // Mark as alive gs.SetAlive(true) }