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

357 lines
10 KiB
Go

package combat
import (
"fmt"
"math/rand"
"time"
)
// DamageSpawn applies damage to a spawn/entity
func (cm *CombatManager) DamageSpawn(attacker Entity, victim Entity, attackType int8, damageType int8, lowDamage, highDamage int32, spellName string, critMod int8, isTick bool, noCalcs bool, ignoreAttacker bool, takePower bool, spell Spell) bool {
if victim == nil {
return false
}
// Check if victim is invulnerable or dead
if victim.GetHP() <= 0 {
return false
}
// Calculate final damage
var finalDamage int32
if noCalcs {
// Use damage as-is
if highDamage > lowDamage {
finalDamage = lowDamage + rand.Int31n(highDamage-lowDamage+1)
} else {
finalDamage = lowDamage
}
} else {
damageInfo := cm.CalculateDamage(attacker, victim, attackType, damageType, lowDamage, highDamage)
finalDamage = damageInfo.FinalDamage
}
// Apply damage
currentHP := victim.GetHP()
newHP := currentHP - finalDamage
if newHP < 0 {
newHP = 0
}
victim.SetHP(newHP)
// Log combat event
if cm.database != nil {
event := &CombatEvent{
Timestamp: time.Now(),
AttackerID: attacker.GetID(),
DefenderID: victim.GetID(),
DamageType: damageType,
HitType: HitTypeHit,
DamageAmount: finalDamage,
WeaponType: 0,
SpellID: 0,
ZoneID: victim.GetZoneID(),
X: victim.GetX(),
Y: victim.GetY(),
Z: victim.GetZ(),
}
if spell != nil {
event.SpellID = spell.GetID()
}
cm.database.LogCombatEvent(event)
}
// Check if victim died
if newHP <= 0 {
cm.KillSpawn(attacker, victim, attackType, damageType, 0)
}
// Check for spell interruption
if finalDamage > 0 && !isTick {
cm.CheckInterruptSpell(attacker, victim)
}
return finalDamage > 0
}
// CalculateDamage calculates damage with all modifiers applied
func (cm *CombatManager) CalculateDamage(attacker Entity, victim Entity, attackType int8, damageType int8, lowDamage, highDamage int32) *DamageInfo {
damage := &DamageInfo{
BaseDamage: lowDamage,
DamageType: damageType,
EffectiveLevelAttacker: int16(attacker.GetLevel()),
EffectiveLevelDefender: int16(victim.GetLevel()),
}
// Calculate base damage with variance
if highDamage > lowDamage {
damage.ScaledDamage = lowDamage + rand.Int31n(highDamage-lowDamage+1)
} else {
damage.ScaledDamage = lowDamage
}
// Apply stat bonuses based on attack type
if attackType == AttackTypePrimary || attackType == AttackTypeSecondary {
// Melee attacks use STR for damage bonus
strBonus := float32(attacker.GetStat(StatSTR)) * 0.1
damage.ScaledDamage = int32(float32(damage.ScaledDamage) * (1.0 + strBonus/100.0))
} else if attackType == AttackTypeSpell {
// Spell attacks use INT for damage bonus
intBonus := float32(attacker.GetStat(StatINT)) * 0.1
damage.ScaledDamage = int32(float32(damage.ScaledDamage) * (1.0 + intBonus/100.0))
}
// Calculate mitigation
mitigation := cm.CalculateMitigation(attackType, damageType, damage.EffectiveLevelAttacker, false)
damage.Mitigation = mitigation
// Apply mitigation
damage.FinalDamage = int32(float32(damage.ScaledDamage) * (1.0 - mitigation/100.0))
// Ensure minimum damage
if damage.FinalDamage < 1 {
damage.FinalDamage = 1
}
return damage
}
// CalculateWeaponDamage calculates damage for weapon-based attacks
func (cm *CombatManager) CalculateWeaponDamage(attacker Entity, victim Entity, weapon Item, hitType int8) *DamageInfo {
var lowDamage, highDamage int32
if weapon != nil {
lowDamage = weapon.GetMinDamage()
highDamage = weapon.GetMaxDamage()
} else {
// Unarmed combat - use base fist damage
lowDamage = int32(attacker.GetLevel()) / 5
highDamage = int32(attacker.GetLevel()) / 3
}
damageType := int8(DamageTypeCrushing) // Default for unarmed
if weapon != nil {
damageType = weapon.GetDamageType()
}
damage := cm.CalculateDamage(attacker, victim, AttackTypePrimary, damageType, lowDamage, highDamage)
// Apply critical hit modifier
if hitType == HitTypeCritical {
critMultiplier := 1.5 + (float32(attacker.GetStat(StatSTR)) * 0.001)
damage.FinalDamage = int32(float32(damage.FinalDamage) * critMultiplier)
damage.CriticalMod = critMultiplier
} else if hitType == HitTypeGlancing {
// Glancing blows do reduced damage
damage.FinalDamage = int32(float32(damage.FinalDamage) * 0.75)
}
return damage
}
// CalculateRangedDamage calculates damage for ranged attacks
func (cm *CombatManager) CalculateRangedDamage(attacker Entity, victim Entity, weapon Item, ammo Item, hitType int8) *DamageInfo {
if weapon == nil {
return &DamageInfo{FinalDamage: 0}
}
lowDamage := weapon.GetMinDamage()
highDamage := weapon.GetMaxDamage()
// Add ammo damage if present
if ammo != nil {
lowDamage += ammo.GetMinDamage()
highDamage += ammo.GetMaxDamage()
}
damageType := int8(weapon.GetDamageType())
damage := cm.CalculateDamage(attacker, victim, AttackTypeRanged, damageType, lowDamage, highDamage)
// Apply AGI bonus for ranged attacks
agiBonus := float32(attacker.GetStat(StatAGI)) * 0.05
damage.FinalDamage = int32(float32(damage.FinalDamage) * (1.0 + agiBonus/100.0))
// Apply critical hit modifier
if hitType == HitTypeCritical {
critMultiplier := 1.75 + (float32(attacker.GetStat(StatAGI)) * 0.001)
damage.FinalDamage = int32(float32(damage.FinalDamage) * critMultiplier)
damage.CriticalMod = critMultiplier
}
return damage
}
// CalculateSpellDamage calculates damage for spell attacks
func (cm *CombatManager) CalculateSpellDamage(caster Entity, victim Entity, spell Spell, lowDamage, highDamage int32) *DamageInfo {
damageType := int8(spell.GetDamageType())
damage := cm.CalculateDamage(caster, victim, AttackTypeSpell, damageType, lowDamage, highDamage)
// Apply spell-specific bonuses
intBonus := float32(caster.GetStat(StatINT)) * 0.15
wisBonus := float32(caster.GetStat(StatWIS)) * 0.1
totalBonus := intBonus + wisBonus
damage.FinalDamage = int32(float32(damage.FinalDamage) * (1.0 + totalBonus/100.0))
return damage
}
// CalculateMitigation calculates damage mitigation
func (cm *CombatManager) CalculateMitigation(attackType int8, damageType int8, effectiveLevelAttacker int16, forPVP bool) float32 {
// This is a simplified mitigation calculation
// EQ2's actual system is much more complex with multiple mitigation types
var baseMitigation float32 = 0.0
// Different mitigation for different damage types
switch damageType {
case DamageTypeSlashing, DamageTypePiercing, DamageTypeCrushing:
baseMitigation = 10.0 // Physical mitigation
case DamageTypeFire, DamageTypeCold, DamageTypeHeat:
baseMitigation = 5.0 // Elemental mitigation
case DamageTypePoison, DamageTypeDisease:
baseMitigation = 8.0 // Noxious mitigation
case DamageTypeMagic, DamageTypeMental, DamageTypeDivine, DamageTypeArcane:
baseMitigation = 3.0 // Arcane mitigation
}
// Cap mitigation
if baseMitigation > cm.config.MaxMitigationPercent {
baseMitigation = cm.config.MaxMitigationPercent
}
return baseMitigation
}
// SpellHeal performs spell healing
func (cm *CombatManager) SpellHeal(caster Entity, target Entity, distance float32, spell Spell, healType string, lowHeal, highHeal int32, critMod int8, noCalcs bool, customSpellName string) *HealInfo {
healInfo := &HealInfo{
BaseHealing: lowHeal,
}
// Check range
if distance > spell.GetRange() {
return healInfo // No healing if out of range
}
// Calculate healing amount
var finalHealing int32
if noCalcs {
if highHeal > lowHeal {
finalHealing = lowHeal + rand.Int31n(highHeal-lowHeal+1)
} else {
finalHealing = lowHeal
}
} else {
// Apply healing bonuses
wisBonus := float32(caster.GetStat(StatWIS)) * 0.2
healingMultiplier := 1.0 + wisBonus/100.0
baseHeal := lowHeal
if highHeal > lowHeal {
baseHeal = lowHeal + rand.Int31n(highHeal-lowHeal+1)
}
finalHealing = int32(float32(baseHeal) * healingMultiplier)
}
// Check for critical heal
if critMod > 0 {
critChance := float32(critMod) + (float32(caster.GetStat(StatWIS)) * 0.1)
if rand.Float32()*100.0 < critChance {
finalHealing = int32(float32(finalHealing) * (1.0 + float32(critMod)/100.0))
}
}
healInfo.FinalHealing = finalHealing
// Apply healing
currentHP := target.GetHP()
maxHP := target.GetTotalHP()
newHP := currentHP + finalHealing
if newHP > maxHP {
healInfo.Overheal = newHP - maxHP
newHP = maxHP
}
target.SetHP(newHP)
// Update statistics
cm.statsMutex.Lock()
cm.totalHealing += int64(finalHealing)
cm.statsMutex.Unlock()
return healInfo
}
// KillSpawn handles entity death
func (cm *CombatManager) KillSpawn(killer Entity, victim Entity, attackType int8, damageType int8, killBlowType int16) {
if victim == nil {
return
}
// Set victim as dead
victim.SetHP(0)
victim.SetAlive(false)
// Clear any active combat sessions involving this entity
cm.StopCombat(victim.GetID())
// Clear hate lists targeting this entity
cm.ClearHateTarget(victim.GetID())
// TODO: Handle experience, loot, etc.
// This would involve integration with other systems
fmt.Printf("Entity %s (ID: %d) killed by %s (ID: %d)\n",
victim.GetName(), victim.GetID(), killer.GetName(), killer.GetID())
}
// HandleDeathExperienceDebt handles experience debt on death
func (cm *CombatManager) HandleDeathExperienceDebt(victim Entity, killer Entity) {
if !victim.IsPlayer() {
return
}
// TODO: Implement experience debt system
// This would integrate with the experience system
fmt.Printf("Player %s died - applying experience debt\n", victim.GetName())
}
// GetDamageTypeResistancePercentage gets resistance percentage for a damage type
func (cm *CombatManager) GetDamageTypeResistancePercentage(victim Entity, damageType int8) float32 {
var resistanceStat int32
switch damageType {
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
}
resistance := victim.GetStat(resistanceStat)
// Convert resistance value to percentage (simplified formula)
return float32(resistance) * 0.1
}