add combat

This commit is contained in:
Sky Johnson 2025-08-30 11:51:05 -05:00
parent fc32aa4b74
commit ab2b2600d0
11 changed files with 3200 additions and 3 deletions

406
internal/combat/attacks.go Normal file
View 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
}

View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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)
}

View 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)
}

View File

@ -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

View File

@ -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