627 lines
16 KiB
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)
|
|
} |