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

406 lines
11 KiB
Go

package combat
import (
"math/rand"
"time"
)
// MeleeAttack performs a melee attack
func (cm *CombatManager) MeleeAttack(attacker Entity, victim Entity, distance float32, primary bool, multiAttack bool) *AttackResult {
result := &AttackResult{
HitType: HitTypeMiss,
DamageDealt: 0,
DamageType: DamageTypeSlashing,
WeaponSkill: WeaponType1HSlash,
}
// Check if attack is allowed
if !cm.AttackAllowed(attacker, victim, false) {
return result
}
// Check weapon timing
attackType := int8(AttackTypePrimary)
if !primary {
attackType = int8(AttackTypeSecondary)
}
var weapon Item
if primary {
weapon = cm.itemManager.GetEquippedItem(attacker.GetID(), SlotPrimary)
if !cm.weaponTiming.IsPrimaryWeaponReady(attacker) {
return result // Weapon not ready
}
} else {
weapon = cm.itemManager.GetEquippedItem(attacker.GetID(), SlotSecondary)
if !cm.weaponTiming.IsSecondaryWeaponReady(attacker) {
return result // Weapon not ready
}
}
// Validate weapon attack
if valid, _ := cm.weaponTiming.ValidateWeaponAttack(attacker, weapon, attackType); !valid {
return result
}
// Set weapon properties
if weapon != nil {
result.Weapon = weapon
result.DamageType = weapon.GetDamageType()
result.WeaponSkill = weapon.GetSkillType()
}
// Determine hit
hitType := cm.DetermineHit(attacker, victim, AttackTypePrimary, result.DamageType, 0.0, false, nil)
result.HitType = hitType
// Calculate damage if hit
if hitType == HitTypeHit || hitType == HitTypeCritical || hitType == HitTypeGlancing {
damage := cm.CalculateWeaponDamage(attacker, victim, weapon, hitType)
result.DamageDealt = damage.FinalDamage
result.TotalDamage = damage.FinalDamage
result.CriticalHit = (hitType == HitTypeCritical)
// Apply damage
cm.DamageSpawn(attacker, victim, AttackTypePrimary, result.DamageType,
damage.FinalDamage, damage.FinalDamage, "", 0, false, false, false, false, nil)
} else {
// Handle defensive actions
switch hitType {
case HitTypeDodge:
result.Dodged = true
case HitTypeParry:
result.Parried = true
case HitTypeBlock:
result.Blocked = true
case HitTypeRiposte:
result.Riposte = true
// TODO: Implement riposte damage
}
}
// Set weapon timer
if primary {
cm.weaponTiming.SetPrimaryWeaponTimer(attacker, weapon)
} else {
cm.weaponTiming.SetSecondaryWeaponTimer(attacker, weapon)
}
// Update combat statistics
cm.updateAttackStats(attacker.GetID(), result)
// Add hate if victim is NPC
if victim.IsNPC() && result.DamageDealt > 0 {
cm.AddHate(victim.GetID(), attacker.GetID(), result.DamageDealt, false)
}
return result
}
// RangeAttack performs a ranged attack
func (cm *CombatManager) RangeAttack(attacker Entity, victim Entity, distance float32, weapon Item, ammo Item, multiAttack bool) *AttackResult {
result := &AttackResult{
HitType: HitTypeMiss,
DamageDealt: 0,
DamageType: DamageTypePiercing,
WeaponSkill: WeaponTypeBow,
}
// Check if attack is allowed
if !cm.AttackAllowed(attacker, victim, true) {
return result
}
// Validate weapon and ammo
if weapon == nil || !weapon.IsRanged() {
return result
}
if ammo == nil || !ammo.IsAmmo() {
return result
}
// Check range
rangeInfo := weapon.GetRangeInfo()
if rangeInfo != nil {
if distance < rangeInfo.RangeLow || distance > rangeInfo.RangeHigh {
return result
}
}
result.Weapon = weapon
result.DamageType = int8(weapon.GetDamageType())
result.WeaponSkill = weapon.GetSkillType()
// Determine hit
hitType := cm.DetermineHit(attacker, victim, AttackTypeRanged, result.DamageType, 0.0, false, nil)
result.HitType = hitType
// Calculate damage if hit
if hitType == HitTypeHit || hitType == HitTypeCritical || hitType == HitTypeGlancing {
damage := cm.CalculateRangedDamage(attacker, victim, weapon, ammo, hitType)
result.DamageDealt = damage.FinalDamage
result.TotalDamage = damage.FinalDamage
result.CriticalHit = (hitType == HitTypeCritical)
// Apply damage
cm.DamageSpawn(attacker, victim, AttackTypeRanged, result.DamageType,
damage.FinalDamage, damage.FinalDamage, "", 0, false, false, false, false, nil)
}
// Update combat statistics
cm.updateAttackStats(attacker.GetID(), result)
// Add hate if victim is NPC
if victim.IsNPC() && result.DamageDealt > 0 {
cm.AddHate(victim.GetID(), attacker.GetID(), result.DamageDealt, false)
}
return result
}
// SpellAttack performs a spell-based attack
func (cm *CombatManager) SpellAttack(caster Entity, victim Entity, distance float32, spell Spell, damageType int8, lowDamage, highDamage int32, critMod int8, noCalcs bool) *AttackResult {
result := &AttackResult{
HitType: HitTypeHit,
DamageDealt: 0,
DamageType: damageType,
WeaponSkill: 0, // Spell attacks don't use weapon skills
}
// Check if attack is allowed
if !cm.AttackAllowed(caster, victim, false) {
return result
}
// Check range
if distance > spell.GetRange() {
result.HitType = HitTypeMiss
return result
}
// Check for spell resistance
if cm.config.EnableSpellResist {
resistChance := cm.CalculateSpellResistance(caster, victim, spell)
if rand.Float32()*100.0 < resistChance {
result.Resisted = true
result.HitType = HitTypeMiss
return result
}
}
// Calculate spell damage
var finalDamage int32
if noCalcs {
// Use provided damage values directly
if highDamage > lowDamage {
finalDamage = lowDamage + rand.Int31n(highDamage-lowDamage+1)
} else {
finalDamage = lowDamage
}
} else {
damage := cm.CalculateSpellDamage(caster, victim, spell, lowDamage, highDamage)
finalDamage = damage.FinalDamage
}
// Check for critical hit
if critMod > 0 {
critChance := float32(critMod) + cm.GetSpellCritChance(caster)
if rand.Float32()*100.0 < critChance {
result.CriticalHit = true
result.HitType = HitTypeCritical
finalDamage = int32(float32(finalDamage) * (1.0 + float32(critMod)/100.0))
}
}
result.DamageDealt = finalDamage
result.TotalDamage = finalDamage
// Apply damage
cm.DamageSpawn(caster, victim, AttackTypeSpell, damageType,
finalDamage, finalDamage, spell.GetName(), critMod, false, noCalcs, false, false, spell)
// Update combat statistics
cm.updateAttackStats(caster.GetID(), result)
// Add hate if victim is NPC
if victim.IsNPC() && result.DamageDealt > 0 {
hateAmount := int32(float32(result.DamageDealt) * 1.5) // Spells generate more hate
cm.AddHate(victim.GetID(), caster.GetID(), hateAmount, false)
}
return result
}
// ProcAttack performs a proc-based attack
func (cm *CombatManager) ProcAttack(attacker Entity, victim Entity, damageType int8, lowDamage, highDamage int32, name, successMsg, effectMsg string) *AttackResult {
result := &AttackResult{
HitType: HitTypeHit,
DamageType: damageType,
WeaponSkill: 0,
}
// Calculate proc damage (usually no resistance for procs)
var damage int32
if highDamage > lowDamage {
damage = lowDamage + rand.Int31n(highDamage-lowDamage+1)
} else {
damage = lowDamage
}
result.DamageDealt = damage
result.TotalDamage = damage
// Apply damage
cm.DamageSpawn(attacker, victim, AttackTypeProc, damageType,
damage, damage, name, 0, false, false, false, false, nil)
// Update combat statistics
cm.updateAttackStats(attacker.GetID(), result)
// Add hate if victim is NPC (less hate for procs)
if victim.IsNPC() && result.DamageDealt > 0 {
hateAmount := result.DamageDealt / 2
cm.AddHate(victim.GetID(), attacker.GetID(), hateAmount, false)
}
return result
}
// CheckInterruptSpell checks if a spell should be interrupted
func (cm *CombatManager) CheckInterruptSpell(attacker Entity, victim Entity) bool {
if !cm.spellManager.IsSpellCasting(victim.GetID()) {
return false
}
// Calculate interrupt chance based on damage dealt vs caster's concentration
interruptChance := cm.ruleManager.GetFloat32(RuleCategorySpells, RuleSpellInterruptChance)
// Modify chance based on attacker/defender levels
levelDiff := float32(attacker.GetLevel() - victim.GetLevel())
modifiedChance := interruptChance + (levelDiff * 2.0)
// Cap at reasonable values
if modifiedChance > 95.0 {
modifiedChance = 95.0
} else if modifiedChance < 5.0 {
modifiedChance = 5.0
}
if rand.Float32()*100.0 < modifiedChance {
cm.spellManager.InterruptSpell(victim.GetID())
return true
}
return false
}
// CheckFizzleSpell checks if a spell should fizzle
func (cm *CombatManager) CheckFizzleSpell(caster Entity, spell Spell) bool {
baseFizzleChance := cm.ruleManager.GetFloat32(RuleCategorySpells, RuleFizzleChance)
// Modify based on caster's skill vs spell difficulty
// This would be more complex in a full implementation
skillBonus := float32(caster.GetLevel()) * 2.0
fizzleChance := baseFizzleChance - skillBonus
// Cap at reasonable values
if fizzleChance < 0.0 {
fizzleChance = 0.0
} else if fizzleChance > 50.0 {
fizzleChance = 50.0
}
return rand.Float32()*100.0 < fizzleChance
}
// GetSpellCritChance calculates spell critical hit chance
func (cm *CombatManager) GetSpellCritChance(caster Entity) float32 {
// Base chance plus INT bonus
baseChance := cm.config.BaseCriticalChance
intBonus := float32(caster.GetStat(StatINT)) * 0.1
return baseChance + intBonus
}
// updateAttackStats updates combat statistics after an attack
func (cm *CombatManager) updateAttackStats(attackerID int32, result *AttackResult) {
cm.statsMutex.Lock()
defer cm.statsMutex.Unlock()
cm.totalAttacks++
cm.totalDamage += int64(result.DamageDealt)
// Update session stats if exists
if stats, exists := cm.sessionStats[attackerID]; exists {
stats.TotalDamageDealt += int64(result.DamageDealt)
if result.HitType != HitTypeMiss {
stats.AttacksLanded++
} else {
stats.AttacksMissed++
}
if result.CriticalHit {
stats.CriticalHits++
}
}
// Update active combat session
cm.sessionMutex.Lock()
defer cm.sessionMutex.Unlock()
if session, exists := cm.activeSessions[attackerID]; exists {
session.mutex.Lock()
session.DamageDealt += int64(result.DamageDealt)
session.LastActivity = time.Now()
if result.HitType != HitTypeMiss {
session.AttacksLanded++
} else {
session.AttacksMissed++
}
session.mutex.Unlock()
}
}
// CalculateSpellResistance calculates spell resistance chance
func (cm *CombatManager) CalculateSpellResistance(caster Entity, victim Entity, spell Spell) float32 {
// Get victim's resistance to this damage type
var resistanceStat int32
switch spell.GetDamageType() {
case DamageTypeFire, DamageTypeHeat:
resistanceStat = StatVS_Heat
case DamageTypeCold:
resistanceStat = StatVS_Cold
case DamageTypeMagic:
resistanceStat = StatVS_Magic
case DamageTypeMental:
resistanceStat = StatVS_Mental
case DamageTypeDivine:
resistanceStat = StatVS_Divine
case DamageTypeDisease:
resistanceStat = StatVS_Disease
case DamageTypePoison:
resistanceStat = StatVS_Poison
case DamageTypeElemental:
resistanceStat = StatVS_Elemental
case DamageTypeArcane:
resistanceStat = StatVS_Arcane
case DamageTypeNoxious:
resistanceStat = StatVS_Noxious
default:
return 0.0 // No resistance
}
resistance := victim.GetStat(resistanceStat)
casterLevel := caster.GetLevel()
// Calculate resistance percentage
// This is a simplified formula - EQ2's actual formula is more complex
resistancePercent := float32(resistance) / (float32(casterLevel*10) + float32(resistance)) * 100.0
// Cap resistance
if resistancePercent > 75.0 {
resistancePercent = 75.0
}
return resistancePercent
}