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 }