277 lines
7.6 KiB
Go
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
|
|
}
|
|
} |