406 lines
11 KiB
Go
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
|
|
} |