add combat
This commit is contained in:
parent
fc32aa4b74
commit
ab2b2600d0
406
internal/combat/attacks.go
Normal file
406
internal/combat/attacks.go
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
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
|
||||||
|
}
|
203
internal/combat/constants.go
Normal file
203
internal/combat/constants.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
package combat
|
||||||
|
|
||||||
|
// Combat type constants
|
||||||
|
const (
|
||||||
|
CombatTypeMelee = iota
|
||||||
|
CombatTypeRanged
|
||||||
|
CombatTypeSpell
|
||||||
|
)
|
||||||
|
|
||||||
|
// Damage type constants matching EQ2 database
|
||||||
|
const (
|
||||||
|
DamageTypeSlashing = 0
|
||||||
|
DamageTypeCrushing = 1
|
||||||
|
DamageTypePiercing = 2
|
||||||
|
DamageTypeFire = 3
|
||||||
|
DamageTypeCold = 4
|
||||||
|
DamageTypePoison = 5
|
||||||
|
DamageTypeDisease = 6
|
||||||
|
DamageTypeMagic = 7
|
||||||
|
DamageTypeMental = 8
|
||||||
|
DamageTypeDivine = 9
|
||||||
|
DamageTypeHeat = 10
|
||||||
|
DamageTypeArcane = 11
|
||||||
|
DamageTypeNoxious = 12
|
||||||
|
DamageTypeElemental = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hit type constants
|
||||||
|
const (
|
||||||
|
HitTypeMiss = 0
|
||||||
|
HitTypeHit = 1
|
||||||
|
HitTypeCritical = 2
|
||||||
|
HitTypeGlancing = 3
|
||||||
|
HitTypeDodge = 4
|
||||||
|
HitTypeParry = 5
|
||||||
|
HitTypeRiposte = 6
|
||||||
|
HitTypeBlock = 7
|
||||||
|
HitTypeDeflect = 8
|
||||||
|
HitTypeImmune = 9
|
||||||
|
HitTypeInterrupt = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attack type constants
|
||||||
|
const (
|
||||||
|
AttackTypePrimary = 0
|
||||||
|
AttackTypeSecondary = 1
|
||||||
|
AttackTypeRanged = 2
|
||||||
|
AttackTypeSpell = 3
|
||||||
|
AttackTypeProc = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combat state constants
|
||||||
|
const (
|
||||||
|
CombatStateNone = 0
|
||||||
|
CombatStateMelee = 1
|
||||||
|
CombatStateRanged = 2
|
||||||
|
CombatStateCasting = 3
|
||||||
|
CombatStateFizzled = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Equipment slot constants for weapons
|
||||||
|
const (
|
||||||
|
SlotPrimary = 0
|
||||||
|
SlotSecondary = 1
|
||||||
|
SlotRanged = 2
|
||||||
|
SlotAmmo = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// PVP type constants
|
||||||
|
const (
|
||||||
|
PVPTypeNone = 0
|
||||||
|
PVPTypeAlignment = 1
|
||||||
|
PVPTypeFaction = 2
|
||||||
|
PVPTypeFFA = 3
|
||||||
|
PVPTypeOpen = 4
|
||||||
|
PVPTypeRace = 5
|
||||||
|
PVPTypeClass = 6
|
||||||
|
PVPTypeGuild = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encounter state constants
|
||||||
|
const (
|
||||||
|
EncounterStateNormal = 0
|
||||||
|
EncounterStateLocked = 1
|
||||||
|
EncounterStateOvermatched = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combat flags
|
||||||
|
const (
|
||||||
|
CombatFlagMultiAttack = 1 << 0
|
||||||
|
CombatFlagFlurry = 1 << 1
|
||||||
|
CombatFlagBerserk = 1 << 2
|
||||||
|
CombatFlagDualWield = 1 << 3
|
||||||
|
CombatFlagRangedAttack = 1 << 4
|
||||||
|
CombatFlagSpellAttack = 1 << 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// Weapon type constants
|
||||||
|
const (
|
||||||
|
WeaponType1HSlash = 0
|
||||||
|
WeaponType2HSlash = 1
|
||||||
|
WeaponType1HPierce = 2
|
||||||
|
WeaponType1HCrush = 3
|
||||||
|
WeaponType2HCrush = 4
|
||||||
|
WeaponType2HPierce = 5
|
||||||
|
WeaponTypeBow = 6
|
||||||
|
WeaponTypeThrown = 7
|
||||||
|
WeaponTypeCrossbow = 8
|
||||||
|
WeaponTypeWand = 9
|
||||||
|
WeaponTypeFist = 10
|
||||||
|
WeaponTypeShield = 11
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default combat values
|
||||||
|
const (
|
||||||
|
DefaultAttackSpeed = 4.0 // Default attack speed in seconds
|
||||||
|
DefaultRangeDistance = 15.0 // Default range for ranged attacks
|
||||||
|
DefaultMeleeDistance = 5.0 // Default melee range
|
||||||
|
DefaultHateDecayRate = 0.1 // Hate decay per second
|
||||||
|
DefaultCriticalChance = 5.0 // Base critical hit chance
|
||||||
|
DefaultDodgeChance = 5.0 // Base dodge chance
|
||||||
|
DefaultParryChance = 5.0 // Base parry chance
|
||||||
|
DefaultBlockChance = 5.0 // Base block chance
|
||||||
|
DefaultMitigationCap = 75.0 // Maximum mitigation percentage
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combat rule categories
|
||||||
|
const (
|
||||||
|
RuleCategoryPVP = "PVP"
|
||||||
|
RuleCategoryCombat = "Combat"
|
||||||
|
RuleCategorySpells = "Spells"
|
||||||
|
RuleCategoryExperience = "Experience"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combat rule names
|
||||||
|
const (
|
||||||
|
RuleAllowPVP = "AllowPVP"
|
||||||
|
RulePVPType = "PVPType"
|
||||||
|
RulePVPLevelRange = "LevelRange"
|
||||||
|
RulePVPNewbieLevel = "PVPNewbieLevel"
|
||||||
|
RulePVPDamageMultiplier = "PVPDamageMultiplier"
|
||||||
|
RulePVPHealingMultiplier = "PVPHealingMultiplier"
|
||||||
|
RulePVPRangedAllowed = "PVPRangedAllowed"
|
||||||
|
RulePVPSpellsAllowed = "PVPSpellsAllowed"
|
||||||
|
RulePVPCombatCooldown = "PVPCombatCooldown"
|
||||||
|
RuleLockedEncounterNoAttack = "LockedEncounterNoAttack"
|
||||||
|
RuleArmorMitigationLimit = "ArmorMitigationLimit"
|
||||||
|
RuleMaxDamageVariance = "MaxDamageVariance"
|
||||||
|
RuleMinDamageVariance = "MinDamageVariance"
|
||||||
|
RuleDefaultHitChance = "DefaultHitChance"
|
||||||
|
RuleSpellInterruptChance = "SpellInterruptChance"
|
||||||
|
RuleFizzleChance = "FizzleChance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Packet opcodes (will be added to main opcodes file)
|
||||||
|
var CombatOpcodes = map[string]int32{
|
||||||
|
"OP_CombatLogDataMsg": 0x0000,
|
||||||
|
"OP_AttackAllowedMsg": 0x0001,
|
||||||
|
"OP_AttackNotAllowedMsg": 0x0002,
|
||||||
|
"OP_StartAttackMsg": 0x0003,
|
||||||
|
"OP_InterruptCastMsg": 0x0004,
|
||||||
|
"OP_FinishAttackMsg": 0x0005,
|
||||||
|
"OP_IncomingAttackMsg": 0x0006,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat type constants for combat calculations
|
||||||
|
const (
|
||||||
|
StatSTR = 0
|
||||||
|
StatSTA = 1
|
||||||
|
StatAGI = 2
|
||||||
|
StatWIS = 3
|
||||||
|
StatINT = 4
|
||||||
|
StatDEX = 5
|
||||||
|
StatVS_Heat = 200
|
||||||
|
StatVS_Cold = 201
|
||||||
|
StatVS_Magic = 202
|
||||||
|
StatVS_Mental = 203
|
||||||
|
StatVS_Divine = 204
|
||||||
|
StatVS_Disease = 205
|
||||||
|
StatVS_Poison = 206
|
||||||
|
StatVS_Elemental = 207
|
||||||
|
StatVS_Arcane = 208
|
||||||
|
StatVS_Noxious = 209
|
||||||
|
StatPhysicalMitigation = 1000
|
||||||
|
StatElementalMitigation = 1001
|
||||||
|
StatNoxiousMitigation = 1002
|
||||||
|
StatArcaneMitigation = 1003
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combat message channels
|
||||||
|
const (
|
||||||
|
CombatChannelGeneral = 4
|
||||||
|
CombatChannelDamage = 11
|
||||||
|
CombatChannelMiss = 12
|
||||||
|
CombatChannelCombat = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interrupt types
|
||||||
|
const (
|
||||||
|
InterruptTypeSpell = 0
|
||||||
|
InterruptTypeCrafting = 1
|
||||||
|
InterruptTypeAll = 2
|
||||||
|
)
|
357
internal/combat/damage.go
Normal file
357
internal/combat/damage.go
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
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
|
||||||
|
}
|
401
internal/combat/hate.go
Normal file
401
internal/combat/hate.go
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
package combat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HateManager manages hate/threat for all entities in combat
|
||||||
|
type HateManager struct {
|
||||||
|
hateLists map[int32]*HateList
|
||||||
|
hatesMutex sync.RWMutex
|
||||||
|
config *CombatConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHateManager creates a new hate manager
|
||||||
|
func NewHateManager(config *CombatConfig) *HateManager {
|
||||||
|
return &HateManager{
|
||||||
|
hateLists: make(map[int32]*HateList),
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHate adds hate/threat to an entity's hate list
|
||||||
|
func (hm *HateManager) AddHate(ownerID int32, targetID int32, hateAmount int32, lockHate bool) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
// Get or create hate list
|
||||||
|
hateList, exists := hm.hateLists[ownerID]
|
||||||
|
if !exists {
|
||||||
|
hateList = NewHateList(ownerID)
|
||||||
|
hm.hateLists[ownerID] = hateList
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.mutex.Lock()
|
||||||
|
defer hateList.mutex.Unlock()
|
||||||
|
|
||||||
|
// Get or create hate entry
|
||||||
|
hateEntry, exists := hateList.HateEntries[targetID]
|
||||||
|
if !exists {
|
||||||
|
hateEntry = NewHateEntry(targetID, 0)
|
||||||
|
hateList.HateEntries[targetID] = hateEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hate
|
||||||
|
hateEntry.HateAmount += hateAmount
|
||||||
|
hateEntry.LastHit = time.Now()
|
||||||
|
if lockHate {
|
||||||
|
hateEntry.IsLocked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update most hated
|
||||||
|
hm.updateMostHated(hateList)
|
||||||
|
hateList.LastUpdate = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveHate removes hate from a target
|
||||||
|
func (hm *HateManager) RemoveHate(ownerID int32, targetID int32, hateAmount int32) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
hateList, exists := hm.hateLists[ownerID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.mutex.Lock()
|
||||||
|
defer hateList.mutex.Unlock()
|
||||||
|
|
||||||
|
hateEntry, exists := hateList.HateEntries[targetID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove hate
|
||||||
|
hateEntry.HateAmount -= hateAmount
|
||||||
|
if hateEntry.HateAmount <= 0 && !hateEntry.IsLocked {
|
||||||
|
delete(hateList.HateEntries, targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update most hated
|
||||||
|
hm.updateMostHated(hateList)
|
||||||
|
hateList.LastUpdate = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHate sets the exact hate amount for a target
|
||||||
|
func (hm *HateManager) SetHate(ownerID int32, targetID int32, hateAmount int32) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
hateList, exists := hm.hateLists[ownerID]
|
||||||
|
if !exists {
|
||||||
|
hateList = NewHateList(ownerID)
|
||||||
|
hm.hateLists[ownerID] = hateList
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.mutex.Lock()
|
||||||
|
defer hateList.mutex.Unlock()
|
||||||
|
|
||||||
|
hateEntry, exists := hateList.HateEntries[targetID]
|
||||||
|
if !exists {
|
||||||
|
hateEntry = NewHateEntry(targetID, hateAmount)
|
||||||
|
hateList.HateEntries[targetID] = hateEntry
|
||||||
|
} else {
|
||||||
|
hateEntry.HateAmount = hateAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
hateEntry.LastHit = time.Now()
|
||||||
|
|
||||||
|
// Update most hated
|
||||||
|
hm.updateMostHated(hateList)
|
||||||
|
hateList.LastUpdate = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearHate clears all hate for a specific owner
|
||||||
|
func (hm *HateManager) ClearHate(ownerID int32) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
delete(hm.hateLists, ownerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearHateTarget removes a target from all hate lists
|
||||||
|
func (hm *HateManager) ClearHateTarget(targetID int32) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
for _, hateList := range hm.hateLists {
|
||||||
|
hateList.mutex.Lock()
|
||||||
|
delete(hateList.HateEntries, targetID)
|
||||||
|
hm.updateMostHated(hateList)
|
||||||
|
hateList.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHateList returns a copy of the hate list for an entity
|
||||||
|
func (hm *HateManager) GetHateList(ownerID int32) *HateList {
|
||||||
|
hm.hatesMutex.RLock()
|
||||||
|
defer hm.hatesMutex.RUnlock()
|
||||||
|
|
||||||
|
originalList, exists := hm.hateLists[ownerID]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy to prevent external modification
|
||||||
|
copyList := &HateList{
|
||||||
|
OwnerID: originalList.OwnerID,
|
||||||
|
HateEntries: make(map[int32]*HateEntry),
|
||||||
|
MostHated: originalList.MostHated,
|
||||||
|
LastUpdate: originalList.LastUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
originalList.mutex.RLock()
|
||||||
|
defer originalList.mutex.RUnlock()
|
||||||
|
|
||||||
|
for id, entry := range originalList.HateEntries {
|
||||||
|
copyList.HateEntries[id] = &HateEntry{
|
||||||
|
TargetID: entry.TargetID,
|
||||||
|
HateAmount: entry.HateAmount,
|
||||||
|
DamageAmount: entry.DamageAmount,
|
||||||
|
LastHit: entry.LastHit,
|
||||||
|
IsLocked: entry.IsLocked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyList
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostHated returns the most hated target for an entity
|
||||||
|
func (hm *HateManager) GetMostHated(ownerID int32) int32 {
|
||||||
|
hm.hatesMutex.RLock()
|
||||||
|
defer hm.hatesMutex.RUnlock()
|
||||||
|
|
||||||
|
if hateList, exists := hm.hateLists[ownerID]; exists {
|
||||||
|
return hateList.MostHated
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHateAmount returns the hate amount for a specific target
|
||||||
|
func (hm *HateManager) GetHateAmount(ownerID int32, targetID int32) int32 {
|
||||||
|
hm.hatesMutex.RLock()
|
||||||
|
defer hm.hatesMutex.RUnlock()
|
||||||
|
|
||||||
|
hateList, exists := hm.hateLists[ownerID]
|
||||||
|
if !exists {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.mutex.RLock()
|
||||||
|
defer hateList.mutex.RUnlock()
|
||||||
|
|
||||||
|
if hateEntry, exists := hateList.HateEntries[targetID]; exists {
|
||||||
|
return hateEntry.HateAmount
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasHateTarget checks if an owner has hate toward a specific target
|
||||||
|
func (hm *HateManager) HasHateTarget(ownerID int32, targetID int32) bool {
|
||||||
|
return hm.GetHateAmount(ownerID, targetID) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTopHateTargets returns the top N hate targets for an entity
|
||||||
|
func (hm *HateManager) GetTopHateTargets(ownerID int32, count int) []int32 {
|
||||||
|
hateList := hm.GetHateList(ownerID)
|
||||||
|
if hateList == nil || len(hateList.HateEntries) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create slice of entries for sorting
|
||||||
|
type hateTarget struct {
|
||||||
|
targetID int32
|
||||||
|
hate int32
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := make([]hateTarget, 0, len(hateList.HateEntries))
|
||||||
|
for targetID, entry := range hateList.HateEntries {
|
||||||
|
targets = append(targets, hateTarget{
|
||||||
|
targetID: targetID,
|
||||||
|
hate: entry.HateAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple bubble sort for top N (sufficient for small hate lists)
|
||||||
|
for i := 0; i < len(targets)-1; i++ {
|
||||||
|
for j := 0; j < len(targets)-i-1; j++ {
|
||||||
|
if targets[j].hate < targets[j+1].hate {
|
||||||
|
targets[j], targets[j+1] = targets[j+1], targets[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return top N target IDs
|
||||||
|
result := make([]int32, 0, count)
|
||||||
|
for i := 0; i < count && i < len(targets); i++ {
|
||||||
|
result = append(result, targets[i].targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessHateDecay applies hate decay over time
|
||||||
|
func (hm *HateManager) ProcessHateDecay(currentTime time.Time) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
for ownerID, hateList := range hm.hateLists {
|
||||||
|
hateList.mutex.Lock()
|
||||||
|
|
||||||
|
timeDiff := float32(currentTime.Sub(hateList.LastUpdate).Seconds())
|
||||||
|
decayAmount := int32(timeDiff * hm.config.HateDecayRate)
|
||||||
|
|
||||||
|
if decayAmount > 0 {
|
||||||
|
hasHate := false
|
||||||
|
for targetID, entry := range hateList.HateEntries {
|
||||||
|
if !entry.IsLocked {
|
||||||
|
entry.HateAmount -= decayAmount
|
||||||
|
if entry.HateAmount <= 0 {
|
||||||
|
delete(hateList.HateEntries, targetID)
|
||||||
|
} else {
|
||||||
|
hasHate = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasHate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.LastUpdate = currentTime
|
||||||
|
|
||||||
|
// Remove empty hate lists
|
||||||
|
if !hasHate {
|
||||||
|
hateList.mutex.Unlock()
|
||||||
|
delete(hm.hateLists, ownerID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update most hated
|
||||||
|
hm.updateMostHated(hateList)
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockHate locks hate for a target (prevents decay)
|
||||||
|
func (hm *HateManager) LockHate(ownerID int32, targetID int32, lock bool) {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
|
||||||
|
hateList, exists := hm.hateLists[ownerID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.mutex.Lock()
|
||||||
|
defer hateList.mutex.Unlock()
|
||||||
|
|
||||||
|
if hateEntry, exists := hateList.HateEntries[targetID]; exists {
|
||||||
|
hateEntry.IsLocked = lock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateHateModifier calculates hate modifier based on various factors
|
||||||
|
func (hm *HateManager) CalculateHateModifier(attacker Entity, victim Entity, damageAmount int32, isHealing bool) float32 {
|
||||||
|
modifier := float32(1.0)
|
||||||
|
|
||||||
|
// Level difference affects hate generation
|
||||||
|
levelDiff := float32(attacker.GetLevel() - victim.GetLevel())
|
||||||
|
if levelDiff > 0 {
|
||||||
|
// Higher level attacker generates more hate
|
||||||
|
modifier += levelDiff * 0.02
|
||||||
|
} else if levelDiff < 0 {
|
||||||
|
// Lower level attacker generates less hate
|
||||||
|
modifier += levelDiff * 0.01
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance affects hate (closer = more threatening)
|
||||||
|
distance := hm.calculateDistance(attacker, victim)
|
||||||
|
if distance > 100.0 {
|
||||||
|
modifier *= 0.8 // Reduced hate for distant attackers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Healing generates less hate than damage
|
||||||
|
if isHealing {
|
||||||
|
modifier *= 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical hits generate more hate
|
||||||
|
if damageAmount > 0 {
|
||||||
|
// Check if this was a particularly high damage hit
|
||||||
|
avgLevel := float32(attacker.GetLevel() + victim.GetLevel()) / 2.0
|
||||||
|
expectedDamage := avgLevel * 10.0 // Rough estimate
|
||||||
|
if float32(damageAmount) > expectedDamage*1.5 {
|
||||||
|
modifier *= 1.25 // Bonus hate for high damage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap modifier
|
||||||
|
if modifier > 2.0 {
|
||||||
|
modifier = 2.0
|
||||||
|
} else if modifier < 0.1 {
|
||||||
|
modifier = 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHateListCount returns the number of active hate lists
|
||||||
|
func (hm *HateManager) GetHateListCount() int {
|
||||||
|
hm.hatesMutex.RLock()
|
||||||
|
defer hm.hatesMutex.RUnlock()
|
||||||
|
return len(hm.hateLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalHateEntries returns the total number of hate entries across all lists
|
||||||
|
func (hm *HateManager) GetTotalHateEntries() int {
|
||||||
|
hm.hatesMutex.RLock()
|
||||||
|
defer hm.hatesMutex.RUnlock()
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
for _, hateList := range hm.hateLists {
|
||||||
|
hateList.mutex.RLock()
|
||||||
|
total += len(hateList.HateEntries)
|
||||||
|
hateList.mutex.RUnlock()
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateMostHated updates the most hated target in a hate list (internal method)
|
||||||
|
func (hm *HateManager) updateMostHated(hateList *HateList) {
|
||||||
|
var maxHate int32
|
||||||
|
var mostHated int32
|
||||||
|
|
||||||
|
for targetID, entry := range hateList.HateEntries {
|
||||||
|
if entry.HateAmount > maxHate {
|
||||||
|
maxHate = entry.HateAmount
|
||||||
|
mostHated = targetID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hateList.MostHated = mostHated
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateDistance calculates distance between two entities
|
||||||
|
func (hm *HateManager) calculateDistance(entity1 Entity, entity2 Entity) float32 {
|
||||||
|
dx := entity1.GetX() - entity2.GetX()
|
||||||
|
dy := entity1.GetY() - entity2.GetY()
|
||||||
|
dz := entity1.GetZ() - entity2.GetZ()
|
||||||
|
|
||||||
|
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown cleans up the hate manager
|
||||||
|
func (hm *HateManager) Shutdown() {
|
||||||
|
hm.hatesMutex.Lock()
|
||||||
|
defer hm.hatesMutex.Unlock()
|
||||||
|
hm.hateLists = make(map[int32]*HateList)
|
||||||
|
}
|
277
internal/combat/hit_calculation.go
Normal file
277
internal/combat/hit_calculation.go
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
294
internal/combat/manager.go
Normal file
294
internal/combat/manager.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package combat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCombatManager creates a new combat manager
|
||||||
|
func NewCombatManager(
|
||||||
|
database Database,
|
||||||
|
ruleManager RuleManager,
|
||||||
|
spellManager SpellManager,
|
||||||
|
itemManager ItemManager,
|
||||||
|
zoneManager ZoneManager,
|
||||||
|
) *CombatManager {
|
||||||
|
config := &CombatConfig{
|
||||||
|
EnablePVP: ruleManager.GetBool(RuleCategoryPVP, RuleAllowPVP),
|
||||||
|
PVPType: ruleManager.GetInt32(RuleCategoryPVP, RulePVPType),
|
||||||
|
PVPLevelRange: ruleManager.GetInt32(RuleCategoryPVP, RulePVPLevelRange),
|
||||||
|
MaxMitigationPercent: ruleManager.GetFloat32(RuleCategoryCombat, RuleArmorMitigationLimit),
|
||||||
|
BaseHitChance: ruleManager.GetFloat32(RuleCategoryCombat, RuleDefaultHitChance),
|
||||||
|
BaseCriticalChance: DefaultCriticalChance,
|
||||||
|
BaseDodgeChance: DefaultDodgeChance,
|
||||||
|
BaseParryChance: DefaultParryChance,
|
||||||
|
BaseBlockChance: DefaultBlockChance,
|
||||||
|
HateDecayRate: DefaultHateDecayRate,
|
||||||
|
CombatTimeout: 5 * time.Minute,
|
||||||
|
EnableArmorMitigation: true,
|
||||||
|
EnableSpellResist: true,
|
||||||
|
EnableMultiAttack: true,
|
||||||
|
EnableFlurry: true,
|
||||||
|
EnableBerserk: true,
|
||||||
|
EnableRiposte: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := &CombatManager{
|
||||||
|
activeSessions: make(map[int32]*CombatSession),
|
||||||
|
hateLists: make(map[int32]*HateList),
|
||||||
|
sessionStats: make(map[int32]*SessionStats),
|
||||||
|
config: config,
|
||||||
|
database: database,
|
||||||
|
ruleManager: ruleManager,
|
||||||
|
spellManager: spellManager,
|
||||||
|
itemManager: itemManager,
|
||||||
|
zoneManager: zoneManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize hate manager
|
||||||
|
cm.hateManager = NewHateManager(config)
|
||||||
|
|
||||||
|
// Initialize PVP manager
|
||||||
|
cm.pvpManager = NewPVPManager(config, ruleManager)
|
||||||
|
|
||||||
|
// Initialize weapon timing manager
|
||||||
|
cm.weaponTiming = NewWeaponTimingManager()
|
||||||
|
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartCombat initiates combat between two entities
|
||||||
|
func (cm *CombatManager) StartCombat(attacker, defender Entity) error {
|
||||||
|
if attacker == nil || defender == nil {
|
||||||
|
return fmt.Errorf("attacker and defender cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if combat is allowed
|
||||||
|
if !cm.AttackAllowed(attacker, defender, false) {
|
||||||
|
return fmt.Errorf("attack not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create combat session
|
||||||
|
session := NewCombatSession(attacker.GetID(), defender.GetID(), CombatTypeMelee)
|
||||||
|
|
||||||
|
cm.sessionMutex.Lock()
|
||||||
|
cm.activeSessions[attacker.GetID()] = session
|
||||||
|
cm.sessionMutex.Unlock()
|
||||||
|
|
||||||
|
// Initialize hate if defender is NPC
|
||||||
|
if defender.IsNPC() {
|
||||||
|
cm.AddHate(defender.GetID(), attacker.GetID(), 1, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopCombat ends combat for an entity
|
||||||
|
func (cm *CombatManager) StopCombat(entityID int32) {
|
||||||
|
cm.sessionMutex.Lock()
|
||||||
|
defer cm.sessionMutex.Unlock()
|
||||||
|
|
||||||
|
if session, exists := cm.activeSessions[entityID]; exists {
|
||||||
|
session.IsActive = false
|
||||||
|
delete(cm.activeSessions, entityID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInCombat checks if an entity is in combat
|
||||||
|
func (cm *CombatManager) IsInCombat(entityID int32) bool {
|
||||||
|
cm.sessionMutex.RLock()
|
||||||
|
defer cm.sessionMutex.RUnlock()
|
||||||
|
|
||||||
|
session, exists := cm.activeSessions[entityID]
|
||||||
|
return exists && session.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCombatSession returns the combat session for an entity
|
||||||
|
func (cm *CombatManager) GetCombatSession(entityID int32) *CombatSession {
|
||||||
|
cm.sessionMutex.RLock()
|
||||||
|
defer cm.sessionMutex.RUnlock()
|
||||||
|
|
||||||
|
if session, exists := cm.activeSessions[entityID]; exists {
|
||||||
|
// Return session directly (caller should not modify)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfig refreshes combat configuration from rules
|
||||||
|
func (cm *CombatManager) UpdateConfig() {
|
||||||
|
cm.config.EnablePVP = cm.ruleManager.GetBool(RuleCategoryPVP, RuleAllowPVP)
|
||||||
|
cm.config.PVPType = cm.ruleManager.GetInt32(RuleCategoryPVP, RulePVPType)
|
||||||
|
cm.config.PVPLevelRange = cm.ruleManager.GetInt32(RuleCategoryPVP, RulePVPLevelRange)
|
||||||
|
cm.config.MaxMitigationPercent = cm.ruleManager.GetFloat32(RuleCategoryCombat, RuleArmorMitigationLimit)
|
||||||
|
cm.config.BaseHitChance = cm.ruleManager.GetFloat32(RuleCategoryCombat, RuleDefaultHitChance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatistics returns combat system statistics
|
||||||
|
func (cm *CombatManager) GetStatistics() map[string]any {
|
||||||
|
cm.statsMutex.RLock()
|
||||||
|
defer cm.statsMutex.RUnlock()
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"total_attacks": cm.totalAttacks,
|
||||||
|
"total_damage": cm.totalDamage,
|
||||||
|
"total_healing": cm.totalHealing,
|
||||||
|
"active_sessions": len(cm.activeSessions),
|
||||||
|
"active_hate_lists": len(cm.hateLists),
|
||||||
|
"last_update": time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process handles periodic combat updates (called by main server loop)
|
||||||
|
func (cm *CombatManager) Process() {
|
||||||
|
currentTime := time.Now()
|
||||||
|
|
||||||
|
// Process active combat sessions
|
||||||
|
cm.processActiveSessions(currentTime)
|
||||||
|
|
||||||
|
// Decay hate lists
|
||||||
|
cm.hateManager.ProcessHateDecay(currentTime)
|
||||||
|
|
||||||
|
// Process weapon timers
|
||||||
|
cm.weaponTiming.ProcessWeaponTimers()
|
||||||
|
|
||||||
|
// Clean up expired sessions
|
||||||
|
cm.cleanupExpiredSessions(currentTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processActiveSessions updates active combat sessions
|
||||||
|
func (cm *CombatManager) processActiveSessions(currentTime time.Time) {
|
||||||
|
cm.sessionMutex.Lock()
|
||||||
|
defer cm.sessionMutex.Unlock()
|
||||||
|
|
||||||
|
for sessionID, session := range cm.activeSessions {
|
||||||
|
session.mutex.Lock()
|
||||||
|
|
||||||
|
// Check for timeout
|
||||||
|
if currentTime.Sub(session.LastActivity) > cm.config.CombatTimeout {
|
||||||
|
session.IsActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
session.mutex.Unlock()
|
||||||
|
|
||||||
|
// Remove inactive sessions
|
||||||
|
if !session.IsActive {
|
||||||
|
delete(cm.activeSessions, sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// cleanupExpiredSessions removes old combat sessions
|
||||||
|
func (cm *CombatManager) cleanupExpiredSessions(currentTime time.Time) {
|
||||||
|
cm.sessionMutex.Lock()
|
||||||
|
defer cm.sessionMutex.Unlock()
|
||||||
|
|
||||||
|
for sessionID, session := range cm.activeSessions {
|
||||||
|
if currentTime.Sub(session.StartTime) > 24*time.Hour {
|
||||||
|
delete(cm.activeSessions, sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GetConfig returns the current combat configuration
|
||||||
|
func (cm *CombatManager) GetConfig() *CombatConfig {
|
||||||
|
return cm.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown gracefully shuts down the combat manager
|
||||||
|
func (cm *CombatManager) Shutdown() {
|
||||||
|
// Save any persistent data
|
||||||
|
cm.sessionMutex.Lock()
|
||||||
|
defer cm.sessionMutex.Unlock()
|
||||||
|
|
||||||
|
for _, session := range cm.activeSessions {
|
||||||
|
session.IsActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all data
|
||||||
|
cm.activeSessions = make(map[int32]*CombatSession)
|
||||||
|
cm.hateLists = make(map[int32]*HateList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttackAllowed checks if an attack is allowed between two entities
|
||||||
|
func (cm *CombatManager) AttackAllowed(attacker Entity, victim Entity, isRanged bool) bool {
|
||||||
|
if attacker == nil || victim == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot attack self
|
||||||
|
if attacker.GetID() == victim.GetID() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot attack dead entities
|
||||||
|
if !victim.IsAlive() || victim.GetHP() <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if attacker is stunned/mezzed
|
||||||
|
if attacker.IsMezzedOrStunned() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check PVP rules
|
||||||
|
if attacker.IsPlayer() && victim.IsPlayer() {
|
||||||
|
return cm.isPVPAllowed(attacker, victim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check faction rules
|
||||||
|
if !cm.isFactionAttackAllowed(attacker, victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if victim is attackable
|
||||||
|
if victim.GetAttackable() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPVPAllowed checks if PVP is allowed between two players
|
||||||
|
func (cm *CombatManager) isPVPAllowed(attacker Entity, victim Entity) bool {
|
||||||
|
return cm.pvpManager.IsPVPAllowed(attacker, victim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFactionAttackAllowed checks faction rules for attacks
|
||||||
|
func (cm *CombatManager) isFactionAttackAllowed(attacker Entity, victim Entity) bool {
|
||||||
|
// Simplified faction check - in full implementation would check faction standings
|
||||||
|
attackerFaction := attacker.GetFactionID()
|
||||||
|
victimFaction := victim.GetFactionID()
|
||||||
|
|
||||||
|
// Same faction cannot attack each other (except in PVP)
|
||||||
|
if attackerFaction == victimFaction && attackerFaction > 0 {
|
||||||
|
if attacker.IsPlayer() && victim.IsPlayer() {
|
||||||
|
return cm.isPVPAllowed(attacker, victim)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHate adds hate/threat to an entity's hate list
|
||||||
|
func (cm *CombatManager) AddHate(ownerID int32, targetID int32, hateAmount int32, lockHate bool) {
|
||||||
|
cm.hateManager.AddHate(ownerID, targetID, hateAmount, lockHate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearHateTarget removes a target from all hate lists
|
||||||
|
func (cm *CombatManager) ClearHateTarget(targetID int32) {
|
||||||
|
cm.hateManager.ClearHateTarget(targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHateList returns the hate list for an entity
|
||||||
|
func (cm *CombatManager) GetHateList(ownerID int32) *HateList {
|
||||||
|
return cm.hateManager.GetHateList(ownerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostHated returns the most hated target for an entity
|
||||||
|
func (cm *CombatManager) GetMostHated(ownerID int32) int32 {
|
||||||
|
return cm.hateManager.GetMostHated(ownerID)
|
||||||
|
}
|
386
internal/combat/pvp.go
Normal file
386
internal/combat/pvp.go
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
package combat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PVPManager handles all PVP-related logic and validation
|
||||||
|
type PVPManager struct {
|
||||||
|
config *CombatConfig
|
||||||
|
ruleManager RuleManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVPManager creates a new PVP manager
|
||||||
|
func NewPVPManager(config *CombatConfig, ruleManager RuleManager) *PVPManager {
|
||||||
|
return &PVPManager{
|
||||||
|
config: config,
|
||||||
|
ruleManager: ruleManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPVPAllowed checks if PVP is allowed between two players
|
||||||
|
func (pm *PVPManager) IsPVPAllowed(attacker Entity, victim Entity) bool {
|
||||||
|
if !pm.config.EnablePVP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both must be players
|
||||||
|
if !attacker.IsPlayer() || !victim.IsPlayer() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot attack self
|
||||||
|
if attacker.GetID() == victim.GetID() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if victim is attackable
|
||||||
|
if victim.GetAttackable() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check PVP type restrictions
|
||||||
|
if !pm.checkPVPType(attacker, victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check level range restrictions
|
||||||
|
if !pm.checkLevelRange(attacker, victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check zone PVP rules
|
||||||
|
if !pm.checkZonePVPRules(attacker, victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check faction rules
|
||||||
|
if !pm.checkFactionPVPRules(attacker, victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check guild rules
|
||||||
|
if !pm.checkGuildPVPRules(attacker, victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check recent login protection
|
||||||
|
if !pm.checkRecentLoginProtection(victim) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPVPType validates PVP type rules
|
||||||
|
func (pm *PVPManager) checkPVPType(attacker Entity, victim Entity) bool {
|
||||||
|
switch pm.config.PVPType {
|
||||||
|
case PVPTypeNone:
|
||||||
|
return false
|
||||||
|
case PVPTypeOpen:
|
||||||
|
return true
|
||||||
|
case PVPTypeRace:
|
||||||
|
return pm.checkRacePVP(attacker, victim)
|
||||||
|
case PVPTypeClass:
|
||||||
|
return pm.checkClassPVP(attacker, victim)
|
||||||
|
case PVPTypeGuild:
|
||||||
|
return pm.checkGuildWarPVP(attacker, victim)
|
||||||
|
case PVPTypeAlignment:
|
||||||
|
return pm.checkAlignmentPVP(attacker, victim)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkLevelRange validates level-based PVP restrictions
|
||||||
|
func (pm *PVPManager) checkLevelRange(attacker Entity, victim Entity) bool {
|
||||||
|
if pm.config.PVPLevelRange <= 0 {
|
||||||
|
return true // No level restrictions
|
||||||
|
}
|
||||||
|
|
||||||
|
levelDiff := int32(math.Abs(float64(attacker.GetLevel() - victim.GetLevel())))
|
||||||
|
return levelDiff <= pm.config.PVPLevelRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkZonePVPRules validates zone-specific PVP rules
|
||||||
|
func (pm *PVPManager) checkZonePVPRules(attacker Entity, victim Entity) bool {
|
||||||
|
zoneID := attacker.GetZoneID()
|
||||||
|
|
||||||
|
// Check if PVP is enabled in this zone
|
||||||
|
zonePVPEnabled := pm.ruleManager.GetZoneBool(zoneID, RuleCategoryPVP, RuleAllowPVP)
|
||||||
|
if !zonePVPEnabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for safe zones or newbie areas
|
||||||
|
if pm.isNewbieZone(zoneID) {
|
||||||
|
maxNewbieLevel := pm.ruleManager.GetZoneInt32(zoneID, RuleCategoryPVP, RulePVPNewbieLevel)
|
||||||
|
if victim.GetLevel() <= maxNewbieLevel {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFactionPVPRules validates faction-based PVP rules
|
||||||
|
func (pm *PVPManager) checkFactionPVPRules(attacker Entity, victim Entity) bool {
|
||||||
|
attackerFaction := attacker.GetFactionID()
|
||||||
|
victimFaction := victim.GetFactionID()
|
||||||
|
|
||||||
|
// Same faction can only attack in guild wars or specific PVP types
|
||||||
|
if attackerFaction == victimFaction && attackerFaction > 0 {
|
||||||
|
return pm.config.PVPType == PVPTypeGuild || pm.config.PVPType == PVPTypeOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check faction standings
|
||||||
|
return pm.areFactionHostile(attackerFaction, victimFaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkGuildPVPRules validates guild-based PVP rules
|
||||||
|
func (pm *PVPManager) checkGuildPVPRules(attacker Entity, victim Entity) bool {
|
||||||
|
// This would integrate with guild system when available
|
||||||
|
// For now, simplified logic
|
||||||
|
|
||||||
|
// Same guild members cannot attack each other unless in guild war
|
||||||
|
attackerGuildID := pm.getPlayerGuildID(attacker.GetID())
|
||||||
|
victimGuildID := pm.getPlayerGuildID(victim.GetID())
|
||||||
|
|
||||||
|
if attackerGuildID > 0 && attackerGuildID == victimGuildID {
|
||||||
|
// Check if guild war is active
|
||||||
|
return pm.isGuildWarActive(attackerGuildID, victimGuildID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRecentLoginProtection checks for new player login protection
|
||||||
|
func (pm *PVPManager) checkRecentLoginProtection(victim Entity) bool {
|
||||||
|
// This would check player login time when player system is integrated
|
||||||
|
// For now, assume no protection needed
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRacePVP validates race-based PVP rules
|
||||||
|
func (pm *PVPManager) checkRacePVP(attacker Entity, victim Entity) bool {
|
||||||
|
// Different races can attack each other
|
||||||
|
return attacker.GetRace() != victim.GetRace()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkClassPVP validates class-based PVP rules
|
||||||
|
func (pm *PVPManager) checkClassPVP(attacker Entity, victim Entity) bool {
|
||||||
|
// Different classes can attack each other
|
||||||
|
return attacker.GetClass() != victim.GetClass()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkGuildWarPVP validates guild war PVP rules
|
||||||
|
func (pm *PVPManager) checkGuildWarPVP(attacker Entity, victim Entity) bool {
|
||||||
|
attackerGuildID := pm.getPlayerGuildID(attacker.GetID())
|
||||||
|
victimGuildID := pm.getPlayerGuildID(victim.GetID())
|
||||||
|
|
||||||
|
if attackerGuildID == 0 || victimGuildID == 0 {
|
||||||
|
return false // Both must be in guilds
|
||||||
|
}
|
||||||
|
|
||||||
|
return pm.isGuildWarActive(attackerGuildID, victimGuildID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAlignmentPVP validates alignment-based PVP rules
|
||||||
|
func (pm *PVPManager) checkAlignmentPVP(attacker Entity, victim Entity) bool {
|
||||||
|
attackerAlignment := attacker.GetInfoStruct().GetAlignment()
|
||||||
|
victimAlignment := victim.GetInfoStruct().GetAlignment()
|
||||||
|
|
||||||
|
// Evil vs Good, Neutral can attack anyone
|
||||||
|
return pm.areAlignmentsHostile(attackerAlignment, victimAlignment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculatePVPDamageModifier calculates damage modifiers for PVP combat
|
||||||
|
func (pm *PVPManager) CalculatePVPDamageModifier(attacker Entity, victim Entity) float32 {
|
||||||
|
modifier := float32(1.0)
|
||||||
|
|
||||||
|
// Level-based modifier
|
||||||
|
levelDiff := float32(attacker.GetLevel() - victim.GetLevel())
|
||||||
|
if levelDiff > 0 {
|
||||||
|
// Higher level attacker does less damage to lower level victim
|
||||||
|
modifier -= (levelDiff * 0.05)
|
||||||
|
} else if levelDiff < 0 {
|
||||||
|
// Lower level attacker does more damage to higher level victim
|
||||||
|
modifier += (float32(math.Abs(float64(levelDiff))) * 0.02)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PVP damage reduction
|
||||||
|
pvpDamageMultiplier := pm.ruleManager.GetFloat32(RuleCategoryPVP, RulePVPDamageMultiplier)
|
||||||
|
modifier *= pvpDamageMultiplier
|
||||||
|
|
||||||
|
// Cap modifiers
|
||||||
|
if modifier > 2.0 {
|
||||||
|
modifier = 2.0
|
||||||
|
} else if modifier < 0.1 {
|
||||||
|
modifier = 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculatePVPHealingModifier calculates healing modifiers for PVP combat
|
||||||
|
func (pm *PVPManager) CalculatePVPHealingModifier(caster Entity, target Entity) float32 {
|
||||||
|
// Healing is generally less effective in PVP
|
||||||
|
pvpHealingMultiplier := pm.ruleManager.GetFloat32(RuleCategoryPVP, RulePVPHealingMultiplier)
|
||||||
|
|
||||||
|
modifier := pvpHealingMultiplier
|
||||||
|
if modifier < 0.1 {
|
||||||
|
modifier = 0.1
|
||||||
|
} else if modifier > 1.0 {
|
||||||
|
modifier = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPVPExperienceModifier calculates experience modifiers for PVP kills
|
||||||
|
func (pm *PVPManager) GetPVPExperienceModifier(killer Entity, victim Entity) float32 {
|
||||||
|
levelDiff := float32(victim.GetLevel() - killer.GetLevel())
|
||||||
|
|
||||||
|
modifier := float32(1.0)
|
||||||
|
|
||||||
|
// Higher level victims give more experience
|
||||||
|
if levelDiff > 0 {
|
||||||
|
modifier += (levelDiff * 0.1)
|
||||||
|
} else if levelDiff < 0 {
|
||||||
|
// Lower level victims give less experience
|
||||||
|
modifier += (levelDiff * 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap modifiers
|
||||||
|
if modifier > 3.0 {
|
||||||
|
modifier = 3.0
|
||||||
|
} else if modifier < 0.1 {
|
||||||
|
modifier = 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePVPKill processes a PVP kill
|
||||||
|
func (pm *PVPManager) HandlePVPKill(killer Entity, victim Entity) {
|
||||||
|
// This would integrate with experience, faction, and reputation systems
|
||||||
|
// For now, just basic handling
|
||||||
|
|
||||||
|
// Award PVP experience
|
||||||
|
expModifier := pm.GetPVPExperienceModifier(killer, victim)
|
||||||
|
baseExp := float32(victim.GetLevel() * 100)
|
||||||
|
_ = int32(baseExp * expModifier) // pvpExp - would be used when experience system is integrated
|
||||||
|
|
||||||
|
// This would call experience system when available
|
||||||
|
// killer.AddExperience(pvpExp)
|
||||||
|
|
||||||
|
// Handle faction changes if applicable
|
||||||
|
pm.handlePVPFactionChanges(killer, victim)
|
||||||
|
|
||||||
|
// Handle guild war points if applicable
|
||||||
|
pm.handleGuildWarPoints(killer, victim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions (would integrate with other systems when available)
|
||||||
|
|
||||||
|
// isNewbieZone checks if a zone is designated as a newbie/starting zone
|
||||||
|
func (pm *PVPManager) isNewbieZone(zoneID int32) bool {
|
||||||
|
// This would check against a list of newbie zones
|
||||||
|
newbieZones := []int32{1, 2, 3, 4} // Example zone IDs
|
||||||
|
for _, newbieZone := range newbieZones {
|
||||||
|
if zoneID == newbieZone {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// areFactionHostile checks if two factions are hostile to each other
|
||||||
|
func (pm *PVPManager) areFactionHostile(faction1, faction2 int32) bool {
|
||||||
|
// This would integrate with faction system
|
||||||
|
// For now, simplified logic: different factions are hostile
|
||||||
|
return faction1 != faction2
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPlayerGuildID gets the guild ID for a player
|
||||||
|
func (pm *PVPManager) getPlayerGuildID(playerID int32) int32 {
|
||||||
|
// This would integrate with guild system
|
||||||
|
// For now, return 0 (no guild)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGuildWarActive checks if two guilds are at war
|
||||||
|
func (pm *PVPManager) isGuildWarActive(guild1ID, guild2ID int32) bool {
|
||||||
|
// This would integrate with guild war system
|
||||||
|
// For now, return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// areAlignmentsHostile checks if two alignments are hostile
|
||||||
|
func (pm *PVPManager) areAlignmentsHostile(alignment1, alignment2 int32) bool {
|
||||||
|
// Evil vs Good are always hostile
|
||||||
|
if (alignment1 < 0 && alignment2 > 0) || (alignment1 > 0 && alignment2 < 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neutral (0) can attack anyone
|
||||||
|
if alignment1 == 0 || alignment2 == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePVPFactionChanges processes faction changes from PVP kills
|
||||||
|
func (pm *PVPManager) handlePVPFactionChanges(killer Entity, victim Entity) {
|
||||||
|
// This would integrate with faction system
|
||||||
|
// Could modify faction standings based on PVP kills
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGuildWarPoints processes guild war points from PVP kills
|
||||||
|
func (pm *PVPManager) handleGuildWarPoints(killer Entity, victim Entity) {
|
||||||
|
// This would integrate with guild war system
|
||||||
|
// Could award points for guild vs guild kills
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPVPStatistics returns PVP-related statistics
|
||||||
|
func (pm *PVPManager) GetPVPStatistics() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"pvp_enabled": pm.config.EnablePVP,
|
||||||
|
"pvp_type": pm.config.PVPType,
|
||||||
|
"level_range": pm.config.PVPLevelRange,
|
||||||
|
"damage_modifier": pm.ruleManager.GetFloat32(RuleCategoryPVP, RulePVPDamageMultiplier),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAttack performs comprehensive PVP attack validation
|
||||||
|
func (pm *PVPManager) ValidateAttack(attacker Entity, victim Entity, attackType int8) (bool, string) {
|
||||||
|
if !pm.IsPVPAllowed(attacker, victim) {
|
||||||
|
return false, "PVP not allowed between these players"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional attack-specific validation
|
||||||
|
if attackType == AttackTypeRanged {
|
||||||
|
// Check if ranged PVP is allowed in this zone
|
||||||
|
zoneID := attacker.GetZoneID()
|
||||||
|
if !pm.ruleManager.GetZoneBool(zoneID, RuleCategoryPVP, RulePVPRangedAllowed) {
|
||||||
|
return false, "Ranged attacks not allowed in this zone"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attackType == AttackTypeSpell {
|
||||||
|
// Check if spell PVP is allowed
|
||||||
|
if !pm.ruleManager.GetBool(RuleCategoryPVP, RulePVPSpellsAllowed) {
|
||||||
|
return false, "Spell attacks not allowed in PVP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPVPCooldown calculates PVP cooldown time after combat
|
||||||
|
func (pm *PVPManager) GetPVPCooldown(player Entity) time.Duration {
|
||||||
|
baseCooldown := pm.ruleManager.GetInt32(RuleCategoryPVP, RulePVPCombatCooldown)
|
||||||
|
return time.Duration(baseCooldown) * time.Second
|
||||||
|
}
|
382
internal/combat/types.go
Normal file
382
internal/combat/types.go
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
package combat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entity represents any combat-capable entity (Player, NPC, etc.)
|
||||||
|
type Entity interface {
|
||||||
|
GetID() int32
|
||||||
|
GetLevel() int32
|
||||||
|
GetHP() int32
|
||||||
|
GetTotalHP() int32
|
||||||
|
GetPower() int32
|
||||||
|
GetTotalPower() int32
|
||||||
|
GetX() float32
|
||||||
|
GetY() float32
|
||||||
|
GetZ() float32
|
||||||
|
GetHeading() float32
|
||||||
|
GetStat(statType int32) int32
|
||||||
|
SetHP(hp int32)
|
||||||
|
SetPower(power int32)
|
||||||
|
IsAlive() bool
|
||||||
|
SetAlive(alive bool)
|
||||||
|
IsPlayer() bool
|
||||||
|
IsNPC() bool
|
||||||
|
IsPet() bool
|
||||||
|
IsBot() bool
|
||||||
|
GetClass() int8
|
||||||
|
GetRace() int8
|
||||||
|
GetName() string
|
||||||
|
GetFactionID() int32
|
||||||
|
GetZoneID() int32
|
||||||
|
GetAttackable() int8
|
||||||
|
SetAttackable(attackable int8)
|
||||||
|
IsMezzedOrStunned() bool
|
||||||
|
IsDazed() bool
|
||||||
|
IsDualWield() bool
|
||||||
|
GetPrimaryLastAttackTime() int64
|
||||||
|
GetSecondaryLastAttackTime() int64
|
||||||
|
GetRangeLastAttackTime() int64
|
||||||
|
GetPrimaryAttackDelay() int64
|
||||||
|
GetSecondaryAttackDelay() int64
|
||||||
|
GetRangeAttackDelay() int64
|
||||||
|
SetPrimaryLastAttackTime(time int64)
|
||||||
|
SetSecondaryLastAttackTime(time int64)
|
||||||
|
SetRangeLastAttackTime(time int64)
|
||||||
|
GetLockedNoLoot() int32
|
||||||
|
IsEngagedBySpawnID(spawnID int32) bool
|
||||||
|
GetInfoStruct() InfoStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoStruct interface for entity info
|
||||||
|
type InfoStruct interface {
|
||||||
|
GetLockableEncounter() bool
|
||||||
|
GetAlignment() int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item represents a weapon or equipment item
|
||||||
|
type Item interface {
|
||||||
|
GetID() int32
|
||||||
|
GetName() string
|
||||||
|
GetItemType() int8
|
||||||
|
GetSkillType() int8
|
||||||
|
GetDamageType() int8
|
||||||
|
GetMinDamage() int32
|
||||||
|
GetMaxDamage() int32
|
||||||
|
GetSpeed() float32
|
||||||
|
IsRanged() bool
|
||||||
|
IsAmmo() bool
|
||||||
|
IsThrown() bool
|
||||||
|
GetRangeInfo() *RangeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeInfo contains ranged weapon information
|
||||||
|
type RangeInfo struct {
|
||||||
|
RangeLow float32
|
||||||
|
RangeHigh float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client represents a player client
|
||||||
|
type Client interface {
|
||||||
|
GetVersion() int32
|
||||||
|
SimpleMessage(channel int32, message string)
|
||||||
|
Message(channel int32, format string, args ...any)
|
||||||
|
QueuePacket(packet []byte)
|
||||||
|
GetPlayer() Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombatManager manages all combat operations
|
||||||
|
type CombatManager struct {
|
||||||
|
// Combat sessions
|
||||||
|
activeSessions map[int32]*CombatSession
|
||||||
|
sessionMutex sync.RWMutex
|
||||||
|
|
||||||
|
// Hate management
|
||||||
|
hateLists map[int32]*HateList
|
||||||
|
hatesMutex sync.RWMutex
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
totalAttacks int64
|
||||||
|
totalDamage int64
|
||||||
|
totalHealing int64
|
||||||
|
sessionStats map[int32]*SessionStats
|
||||||
|
statsMutex sync.RWMutex
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
config *CombatConfig
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
database Database
|
||||||
|
ruleManager RuleManager
|
||||||
|
spellManager SpellManager
|
||||||
|
itemManager ItemManager
|
||||||
|
zoneManager ZoneManager
|
||||||
|
hateManager *HateManager
|
||||||
|
pvpManager *PVPManager
|
||||||
|
weaponTiming *WeaponTimingManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombatSession represents an active combat session
|
||||||
|
type CombatSession struct {
|
||||||
|
SessionID int32
|
||||||
|
AttackerID int32
|
||||||
|
DefenderID int32
|
||||||
|
StartTime time.Time
|
||||||
|
LastActivity time.Time
|
||||||
|
DamageDealt int64
|
||||||
|
DamageTaken int64
|
||||||
|
HealingDealt int64
|
||||||
|
HealingTaken int64
|
||||||
|
AttacksLanded int32
|
||||||
|
AttacksMissed int32
|
||||||
|
SpellsCast int32
|
||||||
|
SpellsResisted int32
|
||||||
|
IsActive bool
|
||||||
|
CombatType int8
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// HateList manages hate/threat for an entity
|
||||||
|
type HateList struct {
|
||||||
|
OwnerID int32
|
||||||
|
HateEntries map[int32]*HateEntry
|
||||||
|
MostHated int32
|
||||||
|
LastUpdate time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// HateEntry represents hate toward a specific target
|
||||||
|
type HateEntry struct {
|
||||||
|
TargetID int32
|
||||||
|
HateAmount int32
|
||||||
|
DamageAmount int64
|
||||||
|
LastHit time.Time
|
||||||
|
IsLocked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttackResult contains the outcome of an attack
|
||||||
|
type AttackResult struct {
|
||||||
|
HitType int8
|
||||||
|
DamageDealt int32
|
||||||
|
DamageType int8
|
||||||
|
WeaponSkill int8
|
||||||
|
CriticalHit bool
|
||||||
|
Blocked bool
|
||||||
|
Dodged bool
|
||||||
|
Parried bool
|
||||||
|
Riposte bool
|
||||||
|
Interrupted bool
|
||||||
|
Resisted bool
|
||||||
|
Immune bool
|
||||||
|
TotalDamage int32
|
||||||
|
Mitigation float32
|
||||||
|
Weapon Item
|
||||||
|
SkillIncrease bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeaponTiming tracks weapon attack timers
|
||||||
|
type WeaponTiming struct {
|
||||||
|
PrimaryReady bool
|
||||||
|
SecondaryReady bool
|
||||||
|
RangedReady bool
|
||||||
|
LastPrimaryTime int64
|
||||||
|
LastSecondaryTime int64
|
||||||
|
LastRangedTime int64
|
||||||
|
PrimaryDelay int64
|
||||||
|
SecondaryDelay int64
|
||||||
|
RangedDelay int64
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// DamageInfo contains damage calculation details
|
||||||
|
type DamageInfo struct {
|
||||||
|
BaseDamage int32
|
||||||
|
ScaledDamage int32
|
||||||
|
FinalDamage int32
|
||||||
|
DamageType int8
|
||||||
|
Mitigation float32
|
||||||
|
Resistance float32
|
||||||
|
CriticalMod float32
|
||||||
|
SpellDamage int32
|
||||||
|
WeaponDamage int32
|
||||||
|
EffectiveLevelAttacker int16
|
||||||
|
EffectiveLevelDefender int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealInfo contains healing calculation details
|
||||||
|
type HealInfo struct {
|
||||||
|
BaseHealing int32
|
||||||
|
ScaledHealing int32
|
||||||
|
FinalHealing int32
|
||||||
|
CriticalMod float32
|
||||||
|
SpellHealing int32
|
||||||
|
WisdomBonus int32
|
||||||
|
HealingFocus float32
|
||||||
|
Overheal int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombatConfig holds combat system configuration
|
||||||
|
type CombatConfig struct {
|
||||||
|
EnablePVP bool
|
||||||
|
PVPType int32
|
||||||
|
PVPLevelRange int32
|
||||||
|
MaxMitigationPercent float32
|
||||||
|
BaseHitChance float32
|
||||||
|
BaseCriticalChance float32
|
||||||
|
BaseDodgeChance float32
|
||||||
|
BaseParryChance float32
|
||||||
|
BaseBlockChance float32
|
||||||
|
HateDecayRate float32
|
||||||
|
CombatTimeout time.Duration
|
||||||
|
EnableArmorMitigation bool
|
||||||
|
EnableSpellResist bool
|
||||||
|
EnableMultiAttack bool
|
||||||
|
EnableFlurry bool
|
||||||
|
EnableBerserk bool
|
||||||
|
EnableRiposte bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStats tracks statistics for a combat session
|
||||||
|
type SessionStats struct {
|
||||||
|
TotalDamageDealt int64
|
||||||
|
TotalDamageTaken int64
|
||||||
|
TotalHealingDealt int64
|
||||||
|
TotalHealingTaken int64
|
||||||
|
AttacksLanded int32
|
||||||
|
AttacksMissed int32
|
||||||
|
CriticalHits int32
|
||||||
|
SpellsCast int32
|
||||||
|
SpellsResisted int32
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database interface for combat persistence
|
||||||
|
type Database interface {
|
||||||
|
// Combat logs
|
||||||
|
LogCombatEvent(event *CombatEvent) error
|
||||||
|
GetCombatHistory(entityID int32, limit int32) ([]*CombatEvent, error)
|
||||||
|
|
||||||
|
// Hate persistence (for NPCs that need to persist hate across server restarts)
|
||||||
|
SaveHateList(ownerID int32, hateList *HateList) error
|
||||||
|
LoadHateList(ownerID int32) (*HateList, error)
|
||||||
|
ClearHateList(ownerID int32) error
|
||||||
|
|
||||||
|
// Combat statistics
|
||||||
|
SaveCombatStats(stats *SessionStats) error
|
||||||
|
GetPlayerCombatStats(playerID int32) (*SessionStats, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombatEvent represents a combat event for logging
|
||||||
|
type CombatEvent struct {
|
||||||
|
ID int64
|
||||||
|
Timestamp time.Time
|
||||||
|
AttackerID int32
|
||||||
|
DefenderID int32
|
||||||
|
DamageType int8
|
||||||
|
HitType int8
|
||||||
|
DamageAmount int32
|
||||||
|
WeaponType int8
|
||||||
|
SpellID int32
|
||||||
|
ZoneID int32
|
||||||
|
X float32
|
||||||
|
Y float32
|
||||||
|
Z float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuleManager interface for rule access
|
||||||
|
type RuleManager interface {
|
||||||
|
GetBool(category, rule string) bool
|
||||||
|
GetInt32(category, rule string) int32
|
||||||
|
GetFloat32(category, rule string) float32
|
||||||
|
GetZoneBool(zoneID int32, category, rule string) bool
|
||||||
|
GetZoneInt32(zoneID int32, category, rule string) int32
|
||||||
|
GetZoneFloat32(zoneID int32, category, rule string) float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpellManager interface for spell integration
|
||||||
|
type SpellManager interface {
|
||||||
|
GetSpell(spellID int32) Spell
|
||||||
|
IsSpellCasting(entityID int32) bool
|
||||||
|
InterruptSpell(entityID int32) bool
|
||||||
|
CheckFizzle(entityID int32, spell Spell) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell interface for combat spell integration
|
||||||
|
type Spell interface {
|
||||||
|
GetID() int32
|
||||||
|
GetName() string
|
||||||
|
GetDamageType() int8
|
||||||
|
GetBaseDamage() int32
|
||||||
|
GetCastTime() int32
|
||||||
|
GetRecoveryTime() int32
|
||||||
|
GetRange() float32
|
||||||
|
GetTargetType() int8
|
||||||
|
IsOffensive() bool
|
||||||
|
IsHealing() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemManager interface for weapon/equipment access
|
||||||
|
type ItemManager interface {
|
||||||
|
GetItem(itemID int32) Item
|
||||||
|
GetEquippedItem(entityID int32, slot int8) Item
|
||||||
|
GetWeaponSkill(weapon Item) int8
|
||||||
|
GetArmorMitigation(entityID int32) float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZoneManager interface for zone operations
|
||||||
|
type ZoneManager interface {
|
||||||
|
GetEntitiesInRange(x, y, z, range_ float32, zoneID int32) []Entity
|
||||||
|
SendCombatMessage(zoneID int32, message string, x, y, z float32)
|
||||||
|
ProcessEntityCommand(entityID int32, command string, args []string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombatPacketBuilder handles combat packet construction
|
||||||
|
type CombatPacketBuilder interface {
|
||||||
|
BuildAttackPacket(attacker, defender Entity, result *AttackResult) ([]byte, error)
|
||||||
|
BuildDamagePacket(victim Entity, damage *DamageInfo) ([]byte, error)
|
||||||
|
BuildHealPacket(target Entity, healing *HealInfo) ([]byte, error)
|
||||||
|
BuildCombatLogPacket(event *CombatEvent) ([]byte, error)
|
||||||
|
BuildInterruptPacket(target Entity) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCombatSession creates a new combat session
|
||||||
|
func NewCombatSession(attackerID, defenderID int32, combatType int8) *CombatSession {
|
||||||
|
return &CombatSession{
|
||||||
|
SessionID: generateSessionID(),
|
||||||
|
AttackerID: attackerID,
|
||||||
|
DefenderID: defenderID,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
LastActivity: time.Now(),
|
||||||
|
IsActive: true,
|
||||||
|
CombatType: combatType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHateList creates a new hate list for an entity
|
||||||
|
func NewHateList(ownerID int32) *HateList {
|
||||||
|
return &HateList{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
HateEntries: make(map[int32]*HateEntry),
|
||||||
|
MostHated: 0,
|
||||||
|
LastUpdate: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHateEntry creates a new hate entry
|
||||||
|
func NewHateEntry(targetID int32, initialHate int32) *HateEntry {
|
||||||
|
return &HateEntry{
|
||||||
|
TargetID: targetID,
|
||||||
|
HateAmount: initialHate,
|
||||||
|
DamageAmount: 0,
|
||||||
|
LastHit: time.Now(),
|
||||||
|
IsLocked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSessionID generates a unique session ID
|
||||||
|
func generateSessionID() int32 {
|
||||||
|
// Simple implementation - in production would use proper ID generation
|
||||||
|
return int32(time.Now().UnixNano() & 0x7FFFFFFF)
|
||||||
|
}
|
386
internal/combat/weapon_timing.go
Normal file
386
internal/combat/weapon_timing.go
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
package combat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WeaponTimingManager manages weapon attack timing and delays
|
||||||
|
type WeaponTimingManager struct {
|
||||||
|
timings map[int32]*WeaponTiming // entityID -> timing
|
||||||
|
timingMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWeaponTimingManager creates a new weapon timing manager
|
||||||
|
func NewWeaponTimingManager() *WeaponTimingManager {
|
||||||
|
return &WeaponTimingManager{
|
||||||
|
timings: make(map[int32]*WeaponTiming),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWeaponTiming gets or creates weapon timing for an entity
|
||||||
|
func (wtm *WeaponTimingManager) GetWeaponTiming(entityID int32) *WeaponTiming {
|
||||||
|
wtm.timingMutex.RLock()
|
||||||
|
timing, exists := wtm.timings[entityID]
|
||||||
|
wtm.timingMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return timing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new timing entry
|
||||||
|
wtm.timingMutex.Lock()
|
||||||
|
defer wtm.timingMutex.Unlock()
|
||||||
|
|
||||||
|
// Check again in case another goroutine created it
|
||||||
|
if timing, exists = wtm.timings[entityID]; exists {
|
||||||
|
return timing
|
||||||
|
}
|
||||||
|
|
||||||
|
timing = &WeaponTiming{
|
||||||
|
PrimaryReady: true,
|
||||||
|
SecondaryReady: true,
|
||||||
|
RangedReady: true,
|
||||||
|
}
|
||||||
|
wtm.timings[entityID] = timing
|
||||||
|
return timing
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrimaryWeaponReady checks if primary weapon is ready to attack
|
||||||
|
func (wtm *WeaponTimingManager) IsPrimaryWeaponReady(entity Entity) bool {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.RLock()
|
||||||
|
defer timing.mutex.RUnlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
// Check if enough time has passed since last attack
|
||||||
|
if timing.LastPrimaryTime > 0 {
|
||||||
|
timeSinceLastAttack := currentTime - timing.LastPrimaryTime
|
||||||
|
return timeSinceLastAttack >= timing.PrimaryDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
return timing.PrimaryReady
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSecondaryWeaponReady checks if secondary weapon is ready to attack
|
||||||
|
func (wtm *WeaponTimingManager) IsSecondaryWeaponReady(entity Entity) bool {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.RLock()
|
||||||
|
defer timing.mutex.RUnlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
// Check if enough time has passed since last attack
|
||||||
|
if timing.LastSecondaryTime > 0 {
|
||||||
|
timeSinceLastAttack := currentTime - timing.LastSecondaryTime
|
||||||
|
return timeSinceLastAttack >= timing.SecondaryDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
return timing.SecondaryReady
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRangedWeaponReady checks if ranged weapon is ready to attack
|
||||||
|
func (wtm *WeaponTimingManager) IsRangedWeaponReady(entity Entity) bool {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.RLock()
|
||||||
|
defer timing.mutex.RUnlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
// Check if enough time has passed since last attack
|
||||||
|
if timing.LastRangedTime > 0 {
|
||||||
|
timeSinceLastAttack := currentTime - timing.LastRangedTime
|
||||||
|
return timeSinceLastAttack >= timing.RangedDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
return timing.RangedReady
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateWeaponDelay calculates attack delay based on weapon and entity stats
|
||||||
|
func (wtm *WeaponTimingManager) CalculateWeaponDelay(entity Entity, weapon Item, attackType int8) int64 {
|
||||||
|
baseDelay := int64(4000) // 4 seconds default
|
||||||
|
|
||||||
|
if weapon != nil {
|
||||||
|
// Use weapon speed as base delay
|
||||||
|
weaponSpeed := weapon.GetSpeed()
|
||||||
|
baseDelay = int64(weaponSpeed * 1000) // Convert to milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply haste modifiers based on stats
|
||||||
|
hasteModifier := wtm.calculateHasteModifier(entity, attackType)
|
||||||
|
finalDelay := int64(float32(baseDelay) / hasteModifier)
|
||||||
|
|
||||||
|
// Apply minimum delay cap
|
||||||
|
minDelay := int64(1000) // 1 second minimum
|
||||||
|
if finalDelay < minDelay {
|
||||||
|
finalDelay = minDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateHasteModifier calculates haste modifier based on entity stats
|
||||||
|
func (wtm *WeaponTimingManager) calculateHasteModifier(entity Entity, attackType int8) float32 {
|
||||||
|
hasteModifier := float32(1.0)
|
||||||
|
|
||||||
|
// Different stats affect different attack types
|
||||||
|
switch attackType {
|
||||||
|
case AttackTypePrimary, AttackTypeSecondary:
|
||||||
|
// Melee attacks use DEX for attack speed
|
||||||
|
dexBonus := float32(entity.GetStat(StatDEX)) * 0.002
|
||||||
|
hasteModifier += dexBonus
|
||||||
|
case AttackTypeRanged:
|
||||||
|
// Ranged attacks use AGI for attack speed
|
||||||
|
agiBonus := float32(entity.GetStat(StatAGI)) * 0.002
|
||||||
|
hasteModifier += agiBonus
|
||||||
|
case AttackTypeSpell:
|
||||||
|
// Spell casting uses INT for casting speed
|
||||||
|
intBonus := float32(entity.GetStat(StatINT)) * 0.001
|
||||||
|
hasteModifier += intBonus
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap haste modifier
|
||||||
|
if hasteModifier > 3.0 {
|
||||||
|
hasteModifier = 3.0
|
||||||
|
} else if hasteModifier < 0.5 {
|
||||||
|
hasteModifier = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasteModifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrimaryWeaponTimer sets the primary weapon timer after an attack
|
||||||
|
func (wtm *WeaponTimingManager) SetPrimaryWeaponTimer(entity Entity, weapon Item) {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.Lock()
|
||||||
|
defer timing.mutex.Unlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
delay := wtm.CalculateWeaponDelay(entity, weapon, AttackTypePrimary)
|
||||||
|
|
||||||
|
timing.LastPrimaryTime = currentTime
|
||||||
|
timing.PrimaryDelay = delay
|
||||||
|
timing.PrimaryReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSecondaryWeaponTimer sets the secondary weapon timer after an attack
|
||||||
|
func (wtm *WeaponTimingManager) SetSecondaryWeaponTimer(entity Entity, weapon Item) {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.Lock()
|
||||||
|
defer timing.mutex.Unlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
delay := wtm.CalculateWeaponDelay(entity, weapon, AttackTypeSecondary)
|
||||||
|
|
||||||
|
timing.LastSecondaryTime = currentTime
|
||||||
|
timing.SecondaryDelay = delay
|
||||||
|
timing.SecondaryReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRangedWeaponTimer sets the ranged weapon timer after an attack
|
||||||
|
func (wtm *WeaponTimingManager) SetRangedWeaponTimer(entity Entity, weapon Item) {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.Lock()
|
||||||
|
defer timing.mutex.Unlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
delay := wtm.CalculateWeaponDelay(entity, weapon, AttackTypeRanged)
|
||||||
|
|
||||||
|
timing.LastRangedTime = currentTime
|
||||||
|
timing.RangedDelay = delay
|
||||||
|
timing.RangedReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessWeaponTimers updates weapon readiness based on elapsed time
|
||||||
|
func (wtm *WeaponTimingManager) ProcessWeaponTimers() {
|
||||||
|
wtm.timingMutex.RLock()
|
||||||
|
defer wtm.timingMutex.RUnlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
for _, timing := range wtm.timings {
|
||||||
|
timing.mutex.Lock()
|
||||||
|
|
||||||
|
// Check primary weapon
|
||||||
|
if !timing.PrimaryReady && timing.LastPrimaryTime > 0 {
|
||||||
|
if currentTime-timing.LastPrimaryTime >= timing.PrimaryDelay {
|
||||||
|
timing.PrimaryReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check secondary weapon
|
||||||
|
if !timing.SecondaryReady && timing.LastSecondaryTime > 0 {
|
||||||
|
if currentTime-timing.LastSecondaryTime >= timing.SecondaryDelay {
|
||||||
|
timing.SecondaryReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ranged weapon
|
||||||
|
if !timing.RangedReady && timing.LastRangedTime > 0 {
|
||||||
|
if currentTime-timing.LastRangedTime >= timing.RangedDelay {
|
||||||
|
timing.RangedReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timing.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWeaponAttack validates if a weapon attack is allowed
|
||||||
|
func (wtm *WeaponTimingManager) ValidateWeaponAttack(entity Entity, weapon Item, attackType int8) (bool, string) {
|
||||||
|
// Check weapon readiness
|
||||||
|
switch attackType {
|
||||||
|
case AttackTypePrimary:
|
||||||
|
if !wtm.IsPrimaryWeaponReady(entity) {
|
||||||
|
return false, "Primary weapon not ready"
|
||||||
|
}
|
||||||
|
case AttackTypeSecondary:
|
||||||
|
if !wtm.IsSecondaryWeaponReady(entity) {
|
||||||
|
return false, "Secondary weapon not ready"
|
||||||
|
}
|
||||||
|
// Check if entity can dual wield
|
||||||
|
if !entity.IsDualWield() {
|
||||||
|
return false, "Cannot dual wield"
|
||||||
|
}
|
||||||
|
case AttackTypeRanged:
|
||||||
|
if !wtm.IsRangedWeaponReady(entity) {
|
||||||
|
return false, "Ranged weapon not ready"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate weapon requirements
|
||||||
|
if weapon != nil {
|
||||||
|
if !wtm.validateWeaponRequirements(entity, weapon) {
|
||||||
|
return false, "Weapon requirements not met"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateWeaponRequirements checks if entity meets weapon requirements
|
||||||
|
func (wtm *WeaponTimingManager) validateWeaponRequirements(entity Entity, weapon Item) bool {
|
||||||
|
// This would integrate with item requirements system
|
||||||
|
// For now, simplified validation
|
||||||
|
|
||||||
|
// Check if weapon is appropriate for entity level
|
||||||
|
entityLevel := entity.GetLevel()
|
||||||
|
if entityLevel < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// All basic validation passed
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWeaponCooldownRemaining gets remaining cooldown time for a weapon type
|
||||||
|
func (wtm *WeaponTimingManager) GetWeaponCooldownRemaining(entity Entity, attackType int8) int64 {
|
||||||
|
timing := wtm.GetWeaponTiming(entity.GetID())
|
||||||
|
|
||||||
|
timing.mutex.RLock()
|
||||||
|
defer timing.mutex.RUnlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
switch attackType {
|
||||||
|
case AttackTypePrimary:
|
||||||
|
if timing.LastPrimaryTime > 0 {
|
||||||
|
elapsed := currentTime - timing.LastPrimaryTime
|
||||||
|
if elapsed < timing.PrimaryDelay {
|
||||||
|
return timing.PrimaryDelay - elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case AttackTypeSecondary:
|
||||||
|
if timing.LastSecondaryTime > 0 {
|
||||||
|
elapsed := currentTime - timing.LastSecondaryTime
|
||||||
|
if elapsed < timing.SecondaryDelay {
|
||||||
|
return timing.SecondaryDelay - elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case AttackTypeRanged:
|
||||||
|
if timing.LastRangedTime > 0 {
|
||||||
|
elapsed := currentTime - timing.LastRangedTime
|
||||||
|
if elapsed < timing.RangedDelay {
|
||||||
|
return timing.RangedDelay - elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetWeaponTimers resets all weapon timers for an entity
|
||||||
|
func (wtm *WeaponTimingManager) ResetWeaponTimers(entityID int32) {
|
||||||
|
wtm.timingMutex.Lock()
|
||||||
|
defer wtm.timingMutex.Unlock()
|
||||||
|
|
||||||
|
if timing, exists := wtm.timings[entityID]; exists {
|
||||||
|
timing.mutex.Lock()
|
||||||
|
timing.PrimaryReady = true
|
||||||
|
timing.SecondaryReady = true
|
||||||
|
timing.RangedReady = true
|
||||||
|
timing.LastPrimaryTime = 0
|
||||||
|
timing.LastSecondaryTime = 0
|
||||||
|
timing.LastRangedTime = 0
|
||||||
|
timing.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveEntity removes timing data for an entity (cleanup)
|
||||||
|
func (wtm *WeaponTimingManager) RemoveEntity(entityID int32) {
|
||||||
|
wtm.timingMutex.Lock()
|
||||||
|
defer wtm.timingMutex.Unlock()
|
||||||
|
|
||||||
|
delete(wtm.timings, entityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWeaponTimingStats returns statistics about weapon timings
|
||||||
|
func (wtm *WeaponTimingManager) GetWeaponTimingStats() map[string]interface{} {
|
||||||
|
wtm.timingMutex.RLock()
|
||||||
|
defer wtm.timingMutex.RUnlock()
|
||||||
|
|
||||||
|
stats := map[string]interface{}{
|
||||||
|
"active_timings": len(wtm.timings),
|
||||||
|
"timestamp": time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count ready weapons
|
||||||
|
primaryReady := 0
|
||||||
|
secondaryReady := 0
|
||||||
|
rangedReady := 0
|
||||||
|
|
||||||
|
for _, timing := range wtm.timings {
|
||||||
|
timing.mutex.RLock()
|
||||||
|
if timing.PrimaryReady {
|
||||||
|
primaryReady++
|
||||||
|
}
|
||||||
|
if timing.SecondaryReady {
|
||||||
|
secondaryReady++
|
||||||
|
}
|
||||||
|
if timing.RangedReady {
|
||||||
|
rangedReady++
|
||||||
|
}
|
||||||
|
timing.mutex.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["primary_ready"] = primaryReady
|
||||||
|
stats["secondary_ready"] = secondaryReady
|
||||||
|
stats["ranged_ready"] = rangedReady
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown cleans up the weapon timing manager
|
||||||
|
func (wtm *WeaponTimingManager) Shutdown() {
|
||||||
|
wtm.timingMutex.Lock()
|
||||||
|
defer wtm.timingMutex.Unlock()
|
||||||
|
|
||||||
|
wtm.timings = make(map[int32]*WeaponTiming)
|
||||||
|
}
|
@ -79,6 +79,7 @@ const (
|
|||||||
OP_TitleUpdateMsg
|
OP_TitleUpdateMsg
|
||||||
OP_CharacterTitles
|
OP_CharacterTitles
|
||||||
OP_SetActiveTitleMsg
|
OP_SetActiveTitleMsg
|
||||||
|
OP_UpdateTitleCmd
|
||||||
|
|
||||||
// NPC system
|
// NPC system
|
||||||
OP_NPCAttackMsg
|
OP_NPCAttackMsg
|
||||||
@ -115,11 +116,50 @@ const (
|
|||||||
OP_SkillInfoResponse
|
OP_SkillInfoResponse
|
||||||
OP_TradeskillList
|
OP_TradeskillList
|
||||||
|
|
||||||
|
// Tradeskill/Crafting system
|
||||||
|
OP_CreateFromRecipe
|
||||||
|
OP_ItemCreationUI
|
||||||
|
OP_StopCrafting
|
||||||
|
OP_CounterReaction
|
||||||
|
OP_UpdateCreateItem
|
||||||
|
OP_TradeskillEventTriggered
|
||||||
|
OP_CraftingResults
|
||||||
|
OP_UpdateCraftingUI
|
||||||
|
OP_SendCreateFromRecipe
|
||||||
|
|
||||||
|
// Traits system
|
||||||
|
OP_TraitsList
|
||||||
|
OP_TraitRewardPackMsg
|
||||||
|
OP_SelectTraits
|
||||||
|
OP_UpdateTraits
|
||||||
|
|
||||||
|
// Transmutation system
|
||||||
|
OP_EqTargetItemCmd
|
||||||
|
OP_ChoiceWindow
|
||||||
|
OP_QuestComplete
|
||||||
|
OP_TransmuteItem
|
||||||
|
|
||||||
|
// Trade system
|
||||||
|
OP_TradeRequestMsg
|
||||||
|
OP_TradeRequestReplyMsg
|
||||||
|
OP_TradeAcceptMsg
|
||||||
|
OP_TradeCancelMsg
|
||||||
|
OP_TradeCompleteMsg
|
||||||
|
OP_UpdateTradeMsg
|
||||||
|
OP_AddItemToTradeMsg
|
||||||
|
OP_RemoveItemFromTradeMsg
|
||||||
|
OP_TradeCoinUpdate
|
||||||
|
|
||||||
// Sign and widget system
|
// Sign and widget system
|
||||||
OP_EqCreateSignWidgetCmd
|
OP_EqCreateSignWidgetCmd
|
||||||
OP_EqUpdateSignWidgetCmd
|
OP_EqUpdateSignWidgetCmd
|
||||||
OP_SignalMsg
|
OP_SignalMsg
|
||||||
|
|
||||||
|
// Widget system
|
||||||
|
OP_UpdateWidgetCmd
|
||||||
|
OP_WidgetTimerUpdate
|
||||||
|
OP_WidgetCloseCmd
|
||||||
|
|
||||||
// Spawn and object system
|
// Spawn and object system
|
||||||
OP_TintWidgetsMsg
|
OP_TintWidgetsMsg
|
||||||
OP_MoveableObjectPlacementCriteri
|
OP_MoveableObjectPlacementCriteri
|
||||||
@ -235,6 +275,19 @@ const (
|
|||||||
OP_HeroicOpportunityErrorMsg
|
OP_HeroicOpportunityErrorMsg
|
||||||
OP_HeroicOpportunityShiftMsg
|
OP_HeroicOpportunityShiftMsg
|
||||||
|
|
||||||
|
// Combat system opcodes
|
||||||
|
OP_CombatLogDataMsg
|
||||||
|
OP_OutgoingAttackMsg
|
||||||
|
OP_IncomingAttackMsg
|
||||||
|
OP_CombatMitigationMsg
|
||||||
|
OP_WeaponReadyMsg
|
||||||
|
OP_CombatDamageMsg
|
||||||
|
OP_CombatHealMsg
|
||||||
|
OP_CombatInterruptMsg
|
||||||
|
OP_PVPKillMsg
|
||||||
|
OP_HateThreatUpdateMsg
|
||||||
|
OP_CombatSessionUpdateMsg
|
||||||
|
|
||||||
// Add more opcodes as needed...
|
// Add more opcodes as needed...
|
||||||
_maxInternalOpcode // Sentinel value
|
_maxInternalOpcode // Sentinel value
|
||||||
)
|
)
|
||||||
@ -283,6 +336,7 @@ var OpcodeNames = map[InternalOpcode]string{
|
|||||||
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
|
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
|
||||||
OP_CharacterTitles: "OP_CharacterTitles",
|
OP_CharacterTitles: "OP_CharacterTitles",
|
||||||
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
|
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
|
||||||
|
OP_UpdateTitleCmd: "OP_UpdateTitleCmd",
|
||||||
OP_NPCAttackMsg: "OP_NPCAttackMsg",
|
OP_NPCAttackMsg: "OP_NPCAttackMsg",
|
||||||
OP_NPCTargetMsg: "OP_NPCTargetMsg",
|
OP_NPCTargetMsg: "OP_NPCTargetMsg",
|
||||||
OP_NPCInfoMsg: "OP_NPCInfoMsg",
|
OP_NPCInfoMsg: "OP_NPCInfoMsg",
|
||||||
@ -315,11 +369,50 @@ var OpcodeNames = map[InternalOpcode]string{
|
|||||||
OP_SkillInfoResponse: "OP_SkillInfoResponse",
|
OP_SkillInfoResponse: "OP_SkillInfoResponse",
|
||||||
OP_TradeskillList: "OP_TradeskillList",
|
OP_TradeskillList: "OP_TradeskillList",
|
||||||
|
|
||||||
|
// Tradeskill/Crafting system opcodes
|
||||||
|
OP_CreateFromRecipe: "OP_CreateFromRecipe",
|
||||||
|
OP_ItemCreationUI: "OP_ItemCreationUI",
|
||||||
|
OP_StopCrafting: "OP_StopCrafting",
|
||||||
|
OP_CounterReaction: "OP_CounterReaction",
|
||||||
|
OP_UpdateCreateItem: "OP_UpdateCreateItem",
|
||||||
|
OP_TradeskillEventTriggered: "OP_TradeskillEventTriggered",
|
||||||
|
OP_CraftingResults: "OP_CraftingResults",
|
||||||
|
OP_UpdateCraftingUI: "OP_UpdateCraftingUI",
|
||||||
|
OP_SendCreateFromRecipe: "OP_SendCreateFromRecipe",
|
||||||
|
|
||||||
|
// Traits system opcodes
|
||||||
|
OP_TraitsList: "OP_TraitsList",
|
||||||
|
OP_TraitRewardPackMsg: "OP_TraitRewardPackMsg",
|
||||||
|
OP_SelectTraits: "OP_SelectTraits",
|
||||||
|
OP_UpdateTraits: "OP_UpdateTraits",
|
||||||
|
|
||||||
|
// Transmutation system opcodes
|
||||||
|
OP_EqTargetItemCmd: "OP_EqTargetItemCmd",
|
||||||
|
OP_ChoiceWindow: "OP_ChoiceWindow",
|
||||||
|
OP_QuestComplete: "OP_QuestComplete",
|
||||||
|
OP_TransmuteItem: "OP_TransmuteItem",
|
||||||
|
|
||||||
|
// Trade system opcodes
|
||||||
|
OP_TradeRequestMsg: "OP_TradeRequestMsg",
|
||||||
|
OP_TradeRequestReplyMsg: "OP_TradeRequestReplyMsg",
|
||||||
|
OP_TradeAcceptMsg: "OP_TradeAcceptMsg",
|
||||||
|
OP_TradeCancelMsg: "OP_TradeCancelMsg",
|
||||||
|
OP_TradeCompleteMsg: "OP_TradeCompleteMsg",
|
||||||
|
OP_UpdateTradeMsg: "OP_UpdateTradeMsg",
|
||||||
|
OP_AddItemToTradeMsg: "OP_AddItemToTradeMsg",
|
||||||
|
OP_RemoveItemFromTradeMsg: "OP_RemoveItemFromTradeMsg",
|
||||||
|
OP_TradeCoinUpdate: "OP_TradeCoinUpdate",
|
||||||
|
|
||||||
// Sign and widget system opcodes
|
// Sign and widget system opcodes
|
||||||
OP_EqCreateSignWidgetCmd: "OP_EqCreateSignWidgetCmd",
|
OP_EqCreateSignWidgetCmd: "OP_EqCreateSignWidgetCmd",
|
||||||
OP_EqUpdateSignWidgetCmd: "OP_EqUpdateSignWidgetCmd",
|
OP_EqUpdateSignWidgetCmd: "OP_EqUpdateSignWidgetCmd",
|
||||||
OP_SignalMsg: "OP_SignalMsg",
|
OP_SignalMsg: "OP_SignalMsg",
|
||||||
|
|
||||||
|
// Widget system opcodes
|
||||||
|
OP_UpdateWidgetCmd: "OP_UpdateWidgetCmd",
|
||||||
|
OP_WidgetTimerUpdate: "OP_WidgetTimerUpdate",
|
||||||
|
OP_WidgetCloseCmd: "OP_WidgetCloseCmd",
|
||||||
|
|
||||||
// Spawn and object system opcodes
|
// Spawn and object system opcodes
|
||||||
OP_TintWidgetsMsg: "OP_TintWidgetsMsg",
|
OP_TintWidgetsMsg: "OP_TintWidgetsMsg",
|
||||||
OP_MoveableObjectPlacementCriteri: "OP_MoveableObjectPlacementCriteri",
|
OP_MoveableObjectPlacementCriteri: "OP_MoveableObjectPlacementCriteri",
|
||||||
@ -426,6 +519,19 @@ var OpcodeNames = map[InternalOpcode]string{
|
|||||||
OP_HeroicOpportunityProgressMsg: "OP_HeroicOpportunityProgressMsg",
|
OP_HeroicOpportunityProgressMsg: "OP_HeroicOpportunityProgressMsg",
|
||||||
OP_HeroicOpportunityErrorMsg: "OP_HeroicOpportunityErrorMsg",
|
OP_HeroicOpportunityErrorMsg: "OP_HeroicOpportunityErrorMsg",
|
||||||
OP_HeroicOpportunityShiftMsg: "OP_HeroicOpportunityShiftMsg",
|
OP_HeroicOpportunityShiftMsg: "OP_HeroicOpportunityShiftMsg",
|
||||||
|
|
||||||
|
// Combat system opcodes
|
||||||
|
OP_CombatLogDataMsg: "OP_CombatLogDataMsg",
|
||||||
|
OP_OutgoingAttackMsg: "OP_OutgoingAttackMsg",
|
||||||
|
OP_IncomingAttackMsg: "OP_IncomingAttackMsg",
|
||||||
|
OP_CombatMitigationMsg: "OP_CombatMitigationMsg",
|
||||||
|
OP_WeaponReadyMsg: "OP_WeaponReadyMsg",
|
||||||
|
OP_CombatDamageMsg: "OP_CombatDamageMsg",
|
||||||
|
OP_CombatHealMsg: "OP_CombatHealMsg",
|
||||||
|
OP_CombatInterruptMsg: "OP_CombatInterruptMsg",
|
||||||
|
OP_PVPKillMsg: "OP_PVPKillMsg",
|
||||||
|
OP_HateThreatUpdateMsg: "OP_HateThreatUpdateMsg",
|
||||||
|
OP_CombatSessionUpdateMsg: "OP_CombatSessionUpdateMsg",
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpcodeManager handles the mapping between client-specific opcodes and internal opcodes
|
// OpcodeManager handles the mapping between client-specific opcodes and internal opcodes
|
||||||
|
@ -2,6 +2,7 @@ package skills
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -91,9 +92,7 @@ func (m *Manager) GetStatistics() map[string]any {
|
|||||||
|
|
||||||
// Copy individual skill statistics
|
// Copy individual skill statistics
|
||||||
skillStats := make(map[int32]int64)
|
skillStats := make(map[int32]int64)
|
||||||
for skillID, count := range m.skillUpsBySkill {
|
maps.Copy(skillStats, m.skillUpsBySkill)
|
||||||
skillStats[skillID] = count
|
|
||||||
}
|
|
||||||
stats["skill_ups_by_skill"] = skillStats
|
stats["skill_ups_by_skill"] = skillStats
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
Loading…
x
Reference in New Issue
Block a user