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_CharacterTitles
|
||||
OP_SetActiveTitleMsg
|
||||
OP_UpdateTitleCmd
|
||||
|
||||
// NPC system
|
||||
OP_NPCAttackMsg
|
||||
@ -115,11 +116,50 @@ const (
|
||||
OP_SkillInfoResponse
|
||||
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
|
||||
OP_EqCreateSignWidgetCmd
|
||||
OP_EqUpdateSignWidgetCmd
|
||||
OP_SignalMsg
|
||||
|
||||
// Widget system
|
||||
OP_UpdateWidgetCmd
|
||||
OP_WidgetTimerUpdate
|
||||
OP_WidgetCloseCmd
|
||||
|
||||
// Spawn and object system
|
||||
OP_TintWidgetsMsg
|
||||
OP_MoveableObjectPlacementCriteri
|
||||
@ -235,6 +275,19 @@ const (
|
||||
OP_HeroicOpportunityErrorMsg
|
||||
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...
|
||||
_maxInternalOpcode // Sentinel value
|
||||
)
|
||||
@ -283,6 +336,7 @@ var OpcodeNames = map[InternalOpcode]string{
|
||||
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
|
||||
OP_CharacterTitles: "OP_CharacterTitles",
|
||||
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
|
||||
OP_UpdateTitleCmd: "OP_UpdateTitleCmd",
|
||||
OP_NPCAttackMsg: "OP_NPCAttackMsg",
|
||||
OP_NPCTargetMsg: "OP_NPCTargetMsg",
|
||||
OP_NPCInfoMsg: "OP_NPCInfoMsg",
|
||||
@ -315,11 +369,50 @@ var OpcodeNames = map[InternalOpcode]string{
|
||||
OP_SkillInfoResponse: "OP_SkillInfoResponse",
|
||||
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
|
||||
OP_EqCreateSignWidgetCmd: "OP_EqCreateSignWidgetCmd",
|
||||
OP_EqUpdateSignWidgetCmd: "OP_EqUpdateSignWidgetCmd",
|
||||
OP_SignalMsg: "OP_SignalMsg",
|
||||
|
||||
// Widget system opcodes
|
||||
OP_UpdateWidgetCmd: "OP_UpdateWidgetCmd",
|
||||
OP_WidgetTimerUpdate: "OP_WidgetTimerUpdate",
|
||||
OP_WidgetCloseCmd: "OP_WidgetCloseCmd",
|
||||
|
||||
// Spawn and object system opcodes
|
||||
OP_TintWidgetsMsg: "OP_TintWidgetsMsg",
|
||||
OP_MoveableObjectPlacementCriteri: "OP_MoveableObjectPlacementCriteri",
|
||||
@ -426,6 +519,19 @@ var OpcodeNames = map[InternalOpcode]string{
|
||||
OP_HeroicOpportunityProgressMsg: "OP_HeroicOpportunityProgressMsg",
|
||||
OP_HeroicOpportunityErrorMsg: "OP_HeroicOpportunityErrorMsg",
|
||||
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
|
||||
|
@ -2,6 +2,7 @@ package skills
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@ -91,9 +92,7 @@ func (m *Manager) GetStatistics() map[string]any {
|
||||
|
||||
// Copy individual skill statistics
|
||||
skillStats := make(map[int32]int64)
|
||||
for skillID, count := range m.skillUpsBySkill {
|
||||
skillStats[skillID] = count
|
||||
}
|
||||
maps.Copy(skillStats, m.skillUpsBySkill)
|
||||
stats["skill_ups_by_skill"] = skillStats
|
||||
|
||||
return stats
|
||||
|
Loading…
x
Reference in New Issue
Block a user