eq2go/internal/combat/weapon_timing.go
2025-08-30 11:51:05 -05:00

386 lines
10 KiB
Go

package combat
import (
"sync"
"time"
)
// WeaponTimingManager manages weapon attack timing and delays
type WeaponTimingManager struct {
timings map[int32]*WeaponTiming // entityID -> timing
timingMutex sync.RWMutex
}
// NewWeaponTimingManager creates a new weapon timing manager
func NewWeaponTimingManager() *WeaponTimingManager {
return &WeaponTimingManager{
timings: make(map[int32]*WeaponTiming),
}
}
// GetWeaponTiming gets or creates weapon timing for an entity
func (wtm *WeaponTimingManager) GetWeaponTiming(entityID int32) *WeaponTiming {
wtm.timingMutex.RLock()
timing, exists := wtm.timings[entityID]
wtm.timingMutex.RUnlock()
if exists {
return timing
}
// Create new timing entry
wtm.timingMutex.Lock()
defer wtm.timingMutex.Unlock()
// Check again in case another goroutine created it
if timing, exists = wtm.timings[entityID]; exists {
return timing
}
timing = &WeaponTiming{
PrimaryReady: true,
SecondaryReady: true,
RangedReady: true,
}
wtm.timings[entityID] = timing
return timing
}
// IsPrimaryWeaponReady checks if primary weapon is ready to attack
func (wtm *WeaponTimingManager) IsPrimaryWeaponReady(entity Entity) bool {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.RLock()
defer timing.mutex.RUnlock()
currentTime := time.Now().UnixMilli()
// Check if enough time has passed since last attack
if timing.LastPrimaryTime > 0 {
timeSinceLastAttack := currentTime - timing.LastPrimaryTime
return timeSinceLastAttack >= timing.PrimaryDelay
}
return timing.PrimaryReady
}
// IsSecondaryWeaponReady checks if secondary weapon is ready to attack
func (wtm *WeaponTimingManager) IsSecondaryWeaponReady(entity Entity) bool {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.RLock()
defer timing.mutex.RUnlock()
currentTime := time.Now().UnixMilli()
// Check if enough time has passed since last attack
if timing.LastSecondaryTime > 0 {
timeSinceLastAttack := currentTime - timing.LastSecondaryTime
return timeSinceLastAttack >= timing.SecondaryDelay
}
return timing.SecondaryReady
}
// IsRangedWeaponReady checks if ranged weapon is ready to attack
func (wtm *WeaponTimingManager) IsRangedWeaponReady(entity Entity) bool {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.RLock()
defer timing.mutex.RUnlock()
currentTime := time.Now().UnixMilli()
// Check if enough time has passed since last attack
if timing.LastRangedTime > 0 {
timeSinceLastAttack := currentTime - timing.LastRangedTime
return timeSinceLastAttack >= timing.RangedDelay
}
return timing.RangedReady
}
// CalculateWeaponDelay calculates attack delay based on weapon and entity stats
func (wtm *WeaponTimingManager) CalculateWeaponDelay(entity Entity, weapon Item, attackType int8) int64 {
baseDelay := int64(4000) // 4 seconds default
if weapon != nil {
// Use weapon speed as base delay
weaponSpeed := weapon.GetSpeed()
baseDelay = int64(weaponSpeed * 1000) // Convert to milliseconds
}
// Apply haste modifiers based on stats
hasteModifier := wtm.calculateHasteModifier(entity, attackType)
finalDelay := int64(float32(baseDelay) / hasteModifier)
// Apply minimum delay cap
minDelay := int64(1000) // 1 second minimum
if finalDelay < minDelay {
finalDelay = minDelay
}
return finalDelay
}
// calculateHasteModifier calculates haste modifier based on entity stats
func (wtm *WeaponTimingManager) calculateHasteModifier(entity Entity, attackType int8) float32 {
hasteModifier := float32(1.0)
// Different stats affect different attack types
switch attackType {
case AttackTypePrimary, AttackTypeSecondary:
// Melee attacks use DEX for attack speed
dexBonus := float32(entity.GetStat(StatDEX)) * 0.002
hasteModifier += dexBonus
case AttackTypeRanged:
// Ranged attacks use AGI for attack speed
agiBonus := float32(entity.GetStat(StatAGI)) * 0.002
hasteModifier += agiBonus
case AttackTypeSpell:
// Spell casting uses INT for casting speed
intBonus := float32(entity.GetStat(StatINT)) * 0.001
hasteModifier += intBonus
}
// Cap haste modifier
if hasteModifier > 3.0 {
hasteModifier = 3.0
} else if hasteModifier < 0.5 {
hasteModifier = 0.5
}
return hasteModifier
}
// SetPrimaryWeaponTimer sets the primary weapon timer after an attack
func (wtm *WeaponTimingManager) SetPrimaryWeaponTimer(entity Entity, weapon Item) {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.Lock()
defer timing.mutex.Unlock()
currentTime := time.Now().UnixMilli()
delay := wtm.CalculateWeaponDelay(entity, weapon, AttackTypePrimary)
timing.LastPrimaryTime = currentTime
timing.PrimaryDelay = delay
timing.PrimaryReady = false
}
// SetSecondaryWeaponTimer sets the secondary weapon timer after an attack
func (wtm *WeaponTimingManager) SetSecondaryWeaponTimer(entity Entity, weapon Item) {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.Lock()
defer timing.mutex.Unlock()
currentTime := time.Now().UnixMilli()
delay := wtm.CalculateWeaponDelay(entity, weapon, AttackTypeSecondary)
timing.LastSecondaryTime = currentTime
timing.SecondaryDelay = delay
timing.SecondaryReady = false
}
// SetRangedWeaponTimer sets the ranged weapon timer after an attack
func (wtm *WeaponTimingManager) SetRangedWeaponTimer(entity Entity, weapon Item) {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.Lock()
defer timing.mutex.Unlock()
currentTime := time.Now().UnixMilli()
delay := wtm.CalculateWeaponDelay(entity, weapon, AttackTypeRanged)
timing.LastRangedTime = currentTime
timing.RangedDelay = delay
timing.RangedReady = false
}
// ProcessWeaponTimers updates weapon readiness based on elapsed time
func (wtm *WeaponTimingManager) ProcessWeaponTimers() {
wtm.timingMutex.RLock()
defer wtm.timingMutex.RUnlock()
currentTime := time.Now().UnixMilli()
for _, timing := range wtm.timings {
timing.mutex.Lock()
// Check primary weapon
if !timing.PrimaryReady && timing.LastPrimaryTime > 0 {
if currentTime-timing.LastPrimaryTime >= timing.PrimaryDelay {
timing.PrimaryReady = true
}
}
// Check secondary weapon
if !timing.SecondaryReady && timing.LastSecondaryTime > 0 {
if currentTime-timing.LastSecondaryTime >= timing.SecondaryDelay {
timing.SecondaryReady = true
}
}
// Check ranged weapon
if !timing.RangedReady && timing.LastRangedTime > 0 {
if currentTime-timing.LastRangedTime >= timing.RangedDelay {
timing.RangedReady = true
}
}
timing.mutex.Unlock()
}
}
// ValidateWeaponAttack validates if a weapon attack is allowed
func (wtm *WeaponTimingManager) ValidateWeaponAttack(entity Entity, weapon Item, attackType int8) (bool, string) {
// Check weapon readiness
switch attackType {
case AttackTypePrimary:
if !wtm.IsPrimaryWeaponReady(entity) {
return false, "Primary weapon not ready"
}
case AttackTypeSecondary:
if !wtm.IsSecondaryWeaponReady(entity) {
return false, "Secondary weapon not ready"
}
// Check if entity can dual wield
if !entity.IsDualWield() {
return false, "Cannot dual wield"
}
case AttackTypeRanged:
if !wtm.IsRangedWeaponReady(entity) {
return false, "Ranged weapon not ready"
}
}
// Validate weapon requirements
if weapon != nil {
if !wtm.validateWeaponRequirements(entity, weapon) {
return false, "Weapon requirements not met"
}
}
return true, ""
}
// validateWeaponRequirements checks if entity meets weapon requirements
func (wtm *WeaponTimingManager) validateWeaponRequirements(entity Entity, weapon Item) bool {
// This would integrate with item requirements system
// For now, simplified validation
// Check if weapon is appropriate for entity level
entityLevel := entity.GetLevel()
if entityLevel < 1 {
return false
}
// All basic validation passed
return true
}
// GetWeaponCooldownRemaining gets remaining cooldown time for a weapon type
func (wtm *WeaponTimingManager) GetWeaponCooldownRemaining(entity Entity, attackType int8) int64 {
timing := wtm.GetWeaponTiming(entity.GetID())
timing.mutex.RLock()
defer timing.mutex.RUnlock()
currentTime := time.Now().UnixMilli()
switch attackType {
case AttackTypePrimary:
if timing.LastPrimaryTime > 0 {
elapsed := currentTime - timing.LastPrimaryTime
if elapsed < timing.PrimaryDelay {
return timing.PrimaryDelay - elapsed
}
}
case AttackTypeSecondary:
if timing.LastSecondaryTime > 0 {
elapsed := currentTime - timing.LastSecondaryTime
if elapsed < timing.SecondaryDelay {
return timing.SecondaryDelay - elapsed
}
}
case AttackTypeRanged:
if timing.LastRangedTime > 0 {
elapsed := currentTime - timing.LastRangedTime
if elapsed < timing.RangedDelay {
return timing.RangedDelay - elapsed
}
}
}
return 0
}
// ResetWeaponTimers resets all weapon timers for an entity
func (wtm *WeaponTimingManager) ResetWeaponTimers(entityID int32) {
wtm.timingMutex.Lock()
defer wtm.timingMutex.Unlock()
if timing, exists := wtm.timings[entityID]; exists {
timing.mutex.Lock()
timing.PrimaryReady = true
timing.SecondaryReady = true
timing.RangedReady = true
timing.LastPrimaryTime = 0
timing.LastSecondaryTime = 0
timing.LastRangedTime = 0
timing.mutex.Unlock()
}
}
// RemoveEntity removes timing data for an entity (cleanup)
func (wtm *WeaponTimingManager) RemoveEntity(entityID int32) {
wtm.timingMutex.Lock()
defer wtm.timingMutex.Unlock()
delete(wtm.timings, entityID)
}
// GetWeaponTimingStats returns statistics about weapon timings
func (wtm *WeaponTimingManager) GetWeaponTimingStats() map[string]interface{} {
wtm.timingMutex.RLock()
defer wtm.timingMutex.RUnlock()
stats := map[string]interface{}{
"active_timings": len(wtm.timings),
"timestamp": time.Now(),
}
// Count ready weapons
primaryReady := 0
secondaryReady := 0
rangedReady := 0
for _, timing := range wtm.timings {
timing.mutex.RLock()
if timing.PrimaryReady {
primaryReady++
}
if timing.SecondaryReady {
secondaryReady++
}
if timing.RangedReady {
rangedReady++
}
timing.mutex.RUnlock()
}
stats["primary_ready"] = primaryReady
stats["secondary_ready"] = secondaryReady
stats["ranged_ready"] = rangedReady
return stats
}
// Shutdown cleans up the weapon timing manager
func (wtm *WeaponTimingManager) Shutdown() {
wtm.timingMutex.Lock()
defer wtm.timingMutex.Unlock()
wtm.timings = make(map[int32]*WeaponTiming)
}