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

277 lines
7.6 KiB
Go

package combat
import (
"math"
"math/rand"
)
// DetermineHit calculates hit result (hit/miss/dodge/parry/block/riposte)
func (cm *CombatManager) DetermineHit(attacker Entity, victim Entity, attackType int8, damageType int8, toHitBonus float32, isCasterSpell bool, spell Spell) int8 {
// Spell attacks have different hit mechanics
if isCasterSpell && spell != nil {
return cm.determineSpellHit(attacker, victim, spell)
}
// Calculate base hit chance
baseHitChance := cm.config.BaseHitChance + toHitBonus
// Modify hit chance based on level difference
levelDiff := float32(attacker.GetLevel() - victim.GetLevel())
hitChance := baseHitChance + (levelDiff * 2.0)
// Add attacker's accuracy bonuses
if attackType == AttackTypePrimary || attackType == AttackTypeSecondary {
// Melee attacks use STR for accuracy
accuracyBonus := float32(attacker.GetStat(StatSTR)) * 0.05
hitChance += accuracyBonus
} else if attackType == AttackTypeRanged {
// Ranged attacks use AGI for accuracy
accuracyBonus := float32(attacker.GetStat(StatAGI)) * 0.05
hitChance += accuracyBonus
}
// Cap hit chance
if hitChance > 95.0 {
hitChance = 95.0
} else if hitChance < 5.0 {
hitChance = 5.0
}
// Roll for hit
hitRoll := rand.Float32() * 100.0
if hitRoll > hitChance {
return HitTypeMiss
}
// Hit successful - check for defensive actions
defenseRoll := rand.Float32() * 100.0
// Check for dodge (AGI based)
dodgeChance := cm.calculateDodgeChance(victim)
if defenseRoll < dodgeChance {
return HitTypeDodge
}
defenseRoll -= dodgeChance
// Check for parry (only for frontal attacks with weapon equipped)
if cm.isFrontalAttack(attacker, victim) && cm.hasWeaponEquipped(victim) {
parryChance := cm.calculateParryChance(victim)
if defenseRoll < parryChance {
// Check for riposte
riposteChance := parryChance * 0.3 // 30% of parry chance
if rand.Float32()*100.0 < riposteChance {
return HitTypeRiposte
}
return HitTypeParry
}
defenseRoll -= parryChance
}
// Check for block (only with shield equipped)
if cm.hasShieldEquipped(victim) {
blockChance := cm.calculateBlockChance(victim)
if defenseRoll < blockChance {
return HitTypeBlock
}
defenseRoll -= blockChance
}
// Check for critical hit
critChance := cm.calculateCritChance(attacker, attackType)
if rand.Float32()*100.0 < critChance {
return HitTypeCritical
}
// Regular hit
return HitTypeHit
}
// determineSpellHit calculates hit for spell attacks
func (cm *CombatManager) determineSpellHit(caster Entity, victim Entity, spell Spell) int8 {
// Spells generally always hit unless resisted
// Check for spell resistance
if cm.config.EnableSpellResist {
resistChance := cm.CalculateSpellResistance(caster, victim, spell)
if rand.Float32()*100.0 < resistChance {
return HitTypeMiss
}
}
// Check for spell critical
critChance := cm.GetSpellCritChance(caster)
if rand.Float32()*100.0 < critChance {
return HitTypeCritical
}
return HitTypeHit
}
// calculateDodgeChance calculates dodge chance based on AGI and level
func (cm *CombatManager) calculateDodgeChance(entity Entity) float32 {
baseDodge := cm.config.BaseDodgeChance
agiBonus := float32(entity.GetStat(StatAGI)) * 0.1
// Level scaling
levelBonus := float32(entity.GetLevel()) * 0.2
totalDodge := baseDodge + agiBonus + levelBonus
// Cap dodge chance
if totalDodge > 40.0 {
totalDodge = 40.0
} else if totalDodge < 0.0 {
totalDodge = 0.0
}
return totalDodge
}
// calculateParryChance calculates parry chance based on weapon skill and STR
func (cm *CombatManager) calculateParryChance(entity Entity) float32 {
baseParry := cm.config.BaseParryChance
strBonus := float32(entity.GetStat(StatSTR)) * 0.08
// Level scaling
levelBonus := float32(entity.GetLevel()) * 0.15
totalParry := baseParry + strBonus + levelBonus
// Cap parry chance
if totalParry > 35.0 {
totalParry = 35.0
} else if totalParry < 0.0 {
totalParry = 0.0
}
return totalParry
}
// calculateBlockChance calculates block chance based on shield and STA
func (cm *CombatManager) calculateBlockChance(entity Entity) float32 {
baseBlock := cm.config.BaseBlockChance
staBonus := float32(entity.GetStat(StatSTA)) * 0.1
// TODO: Add shield-specific bonuses when item system is integrated
totalBlock := baseBlock + staBonus
// Cap block chance
if totalBlock > 30.0 {
totalBlock = 30.0
} else if totalBlock < 0.0 {
totalBlock = 0.0
}
return totalBlock
}
// calculateCritChance calculates critical hit chance
func (cm *CombatManager) calculateCritChance(attacker Entity, attackType int8) float32 {
baseCrit := cm.config.BaseCriticalChance
var statBonus float32
switch attackType {
case AttackTypePrimary, AttackTypeSecondary:
// Melee crits use STR
statBonus = float32(attacker.GetStat(StatSTR)) * 0.1
case AttackTypeRanged:
// Ranged crits use AGI
statBonus = float32(attacker.GetStat(StatAGI)) * 0.1
case AttackTypeSpell:
// Spell crits use INT
statBonus = float32(attacker.GetStat(StatINT)) * 0.1
default:
statBonus = 0.0
}
totalCrit := baseCrit + statBonus
// Cap critical chance
if totalCrit > 50.0 {
totalCrit = 50.0
} else if totalCrit < 0.0 {
totalCrit = 0.0
}
return totalCrit
}
// isFrontalAttack checks if attack is from the front (for parry eligibility)
func (cm *CombatManager) isFrontalAttack(attacker Entity, victim Entity) bool {
// Calculate angle between attacker and victim's facing direction
dx := attacker.GetX() - victim.GetX()
dz := attacker.GetZ() - victim.GetZ()
if dx == 0 && dz == 0 {
return true // Same position
}
// Calculate angle to attacker
angleToAttacker := math.Atan2(float64(dx), float64(dz)) * 180.0 / math.Pi
// Normalize to 0-360
if angleToAttacker < 0 {
angleToAttacker += 360.0
}
// Get victim's heading (already in degrees 0-360)
victimHeading := float64(victim.GetHeading())
// Calculate relative angle
relativeAngle := math.Abs(angleToAttacker - victimHeading)
if relativeAngle > 180.0 {
relativeAngle = 360.0 - relativeAngle
}
// Front arc is 90 degrees (45 degrees either side of facing direction)
return relativeAngle <= 45.0
}
// hasWeaponEquipped checks if entity has a weapon equipped for parrying
func (cm *CombatManager) hasWeaponEquipped(entity Entity) bool {
primaryWeapon := cm.itemManager.GetEquippedItem(entity.GetID(), SlotPrimary)
return primaryWeapon != nil
}
// hasShieldEquipped checks if entity has a shield equipped for blocking
func (cm *CombatManager) hasShieldEquipped(entity Entity) bool {
secondaryItem := cm.itemManager.GetEquippedItem(entity.GetID(), SlotSecondary)
if secondaryItem == nil {
return false
}
// Check if it's a shield (weapon type 11 = shield)
return secondaryItem.GetSkillType() == WeaponTypeShield
}
// GetSkillByWeaponType gets the appropriate skill for a weapon type
func (cm *CombatManager) GetSkillByWeaponType(weaponType int8, damageType int8, update bool) int8 {
// Map weapon types to skill types
// This would integrate with the skills system
switch weaponType {
case WeaponType1HSlash:
return 0 // 1H Slashing skill
case WeaponType2HSlash:
return 1 // 2H Slashing skill
case WeaponType1HPierce:
return 2 // 1H Piercing skill
case WeaponType1HCrush:
return 3 // 1H Crushing skill
case WeaponType2HCrush:
return 4 // 2H Crushing skill
case WeaponType2HPierce:
return 5 // 2H Piercing skill
case WeaponTypeBow:
return 6 // Bow skill
case WeaponTypeThrown:
return 7 // Thrown skill
case WeaponTypeCrossbow:
return 8 // Crossbow skill
case WeaponTypeWand:
return 9 // Wand skill
case WeaponTypeFist:
return 10 // Fist/Unarmed skill
default:
return 0 // Default to basic weapon skill
}
}