eq2go/internal/ground_spawn/ground_spawn.go

627 lines
16 KiB
Go

package ground_spawn
import (
"fmt"
"math/rand"
"strings"
"sync"
"time"
"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)
}