// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.DataModel.Attributes; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.Pet; using MUnique.OpenMU.GameLogic.Views.World; using MUnique.OpenMU.Pathfinding; /// /// Extensions for s. Contains the damage calculation. /// public static class AttackableExtensions { private const short ExplosionMagicEffectNumber = 75; // 0x4B private const short StunnedMagicEffectNumber = 61; // 0x3D private static readonly IDictionary ReductionModifiers = new Dictionary { { Stats.CurrentMana, Stats.ManaUsageReduction }, { Stats.CurrentAbility, Stats.AbilityUsageReduction }, }; extension(IAttackable attackable) { /// /// Gets a value indicating whether this instance is a summoned monster. /// public bool IsSummonedMonster => attackable is Monster { SummonedBy: not null }; } /// /// Determines whether an attack is blocked because the target or the player attacker is in a safezone. /// /// The attack target. /// The attacker. /// true, if the attack is blocked; otherwise, false. public static bool IsAttackBlockedBySafezone(this IAttackable target, IAttacker attacker) { if (target.IsAtSafezone()) { return true; } var attackerPlayer = attacker as Player ?? (attacker as IPlayerSurrogate)?.Owner; return attackerPlayer?.IsAtSafezone() is true; } /// /// Calculates the damage using a skill. /// /// The object that is attacking. /// The object that is defending. /// The skill that is used. /// If set to true, the damage gets increased by a combo bonus. /// The damage factor. /// /// The hit information. /// public static async ValueTask CalculateDamageAsync(this IAttacker attacker, IAttackable defender, SkillEntry? skill, bool isCombo, double damageFactor = 1.0) { if (!attacker.IsAttackSuccessfulTo(defender)) { return new HitInfo(0, 0, DamageAttributes.Undefined); } Player? attackerPlayer = attacker is AttackerSurrogate surrogate ? surrogate.Owner : (attacker is Player player ? player : null); bool isPvp = attackerPlayer is not null && defender is Player; bool isClassicPvp = isPvp && defender.Attributes[Stats.MaximumShield] == 0; bool isClassicPvpDuel = isClassicPvp && attackerPlayer!.DuelRoom?.Opponent == defender; var duelDmgDec = isClassicPvpDuel ? 0.6 : 1; DamageAttributes attributes = DamageAttributes.Undefined; bool isCriticalHit = Rand.NextRandomBool(attacker.Attributes[Stats.CriticalDamageChance]); bool isExcellentHit = Rand.NextRandomBool(attacker.Attributes[Stats.ExcellentDamageChance]); bool isIgnoringDefense = Rand.NextRandomBool(attacker.Attributes[Stats.DefenseIgnoreChance]); var defense = 0; if (isIgnoringDefense) { attributes |= DamageAttributes.IgnoreDefense; } else { var defenseAttribute = defender.GetDefenseAttribute(attacker); defense = (int)(defender.Attributes[defenseAttribute] * defender.Attributes[Stats.DefenseDecrement]); if (defense < 0) { defense = 0; } } attacker.GetBaseDmg(skill, out int baseMinDamage, out int baseMaxDamage, out DamageType damageType); int dmg; if (damageType == DamageType.Physical) { if (isExcellentHit) { dmg = (int)((baseMaxDamage * 1.2) + attacker.Attributes[Stats.ExcellentDamageBonus]); dmg += (int)(attacker.Attributes[Stats.CriticalDamageBonus] + attacker.Attributes[Stats.BerserkerMaxPhysDmgBonus]); attributes |= DamageAttributes.Excellent; } else if (isCriticalHit) { dmg = baseMaxDamage + (int)(attacker.Attributes[Stats.CriticalDamageBonus] + attacker.Attributes[Stats.BerserkerMaxPhysDmgBonus]); attributes |= DamageAttributes.Critical; } else { baseMinDamage += (int)attacker.Attributes[Stats.BerserkerMinPhysDmgBonus]; baseMaxDamage += (int)attacker.Attributes[Stats.BerserkerMaxPhysDmgBonus]; if (baseMaxDamage <= baseMinDamage) { dmg = baseMinDamage; } else { dmg = Rand.NextInt(baseMinDamage, baseMaxDamage); } } if (attacker.Attributes[Stats.HasDoubleWield] > 0) { // Double wield => 110% dmg (55% + 55%). dmg += dmg; } dmg = (int)((dmg * duelDmgDec) - defense); dmg += GetMasterSkillTreePhysicalPassiveDamageBonus(attacker, true); if (attacker.Attributes[Stats.IsTwoHandedWeaponEquipped] > 0) { dmg += (int)(dmg * attacker.Attributes[Stats.TwoHandedWeaponDamageIncrease]); } if (attacker is AttackerSurrogate) { dmg += (int)attacker.Attributes[Stats.RavenBonusDamage]; if ((attributes & (DamageAttributes.Excellent | DamageAttributes.Critical)) == DamageAttributes.Undefined) { dmg = (int)(dmg / 1.5); } } } else { // Wizardry, Curse, and Fenrir. if (isExcellentHit) { dmg = (int)((baseMaxDamage * duelDmgDec) - defense); dmg = (int)((dmg * 1.2) + attacker.Attributes[Stats.ExcellentDamageBonus]); attributes |= DamageAttributes.Excellent; } else if (isCriticalHit) { dmg = (int)((baseMaxDamage * duelDmgDec) - defense); dmg += (int)attacker.Attributes[Stats.CriticalDamageBonus]; attributes |= DamageAttributes.Critical; } else { if (baseMaxDamage <= baseMinDamage) { dmg = baseMinDamage; } else { dmg = Rand.NextInt(baseMinDamage, baseMaxDamage); } dmg = (int)((dmg * duelDmgDec) - defense); } } dmg += (int)attacker.Attributes[Stats.GreaterDamageBonus]; /*Scroll of Wrath/Wizardry and Remedy of Love go here (but for now they don't exist)*/ if (damageType == DamageType.Wizardry || damageType == DamageType.Curse) { dmg += (int)(dmg * attacker.Attributes[Stats.BerserkerProficiencyMultiplier]); } else if (damageType == DamageType.Physical) { dmg -= (int)(dmg * attacker.Attributes[Stats.WeaknessPhysDmgDecrement]); dmg += GetMasterSkillTreePhysicalPassiveDamageBonus(attacker, false); } else { // nothing to do } /*Scroll of Battle (crit dmg)/Strengthener (exc dmg) go here (but for now they don't exist)*/ if (!isPvp && defender.Overrates(attacker)) { dmg = (int)(dmg * 0.3); } dmg -= (int)(dmg * defender.Attributes[Stats.ArmorDamageDecrease]); var attackerLevel = attacker is Player ? attacker.Attributes[Stats.TotalLevel] : attacker is AttackerSurrogate ? attackerPlayer!.Attributes![Stats.Level] : attacker.Attributes[Stats.Level]; var minLevelDmg = Math.Max(1, (int)attackerLevel / 10); if (dmg < minLevelDmg) { dmg = minLevelDmg; } dmg = (int)(dmg * attacker.Attributes[Stats.AttackDamageIncrease]); if (dmg > 1) { dmg = (int)(dmg * defender.Attributes[Stats.DamageReceiveDecrement]); } if (skill != null) { var multiplier = attacker.Attributes[Stats.SkillMultiplier]; if (skill.EnsureSkillAttributes(attacker.Attributes) is { } skillAttributes) { dmg += (int)skillAttributes[Stats.SkillFinalDamageBonus]; var skillMultiplier = skillAttributes[Stats.SkillFinalMultiplier]; if (skillMultiplier > 0) { multiplier = skillMultiplier; // DragonSlasher. if (skill.Skill!.Number == 265 && !isPvp) { multiplier *= 3; } } } dmg = (int)(dmg * multiplier * damageFactor); } else if (attacker.Attributes[Stats.IsDinorantEquipped] > 0) { dmg = (int)(dmg * 1.3); } else { // Nothing to do } float manaToll = 0; var soulBarrierManaToll = defender.Attributes[Stats.SoulBarrierManaTollPerHit]; if (soulBarrierManaToll > 0 && defender.Attributes[Stats.CurrentMana] > soulBarrierManaToll) { manaToll = soulBarrierManaToll; float soulBarrierReduction = defender.Attributes[Stats.SoulBarrierReceiveDecrement]; soulBarrierReduction = Math.Clamp(soulBarrierReduction, 0f, 0.9f); dmg -= (int)(dmg * soulBarrierReduction); } dmg += (int)attacker.Attributes[Stats.FinalDamageBonus]; if (isPvp && attacker is not AttackerSurrogate) { dmg += (int)attacker.Attributes[Stats.FinalDamageIncreasePvp]; dmg += GetMasterSkillTreeMasteryPvpDamageBonus(attacker); if (attackerPlayer!.CurrentMiniGame?.Definition.Type == MiniGameType.ChaosCastle) { // In Chaos Castle PvP damage is halved (except for raven) dmg /= 2; } } if (dmg > 0) { // To-do: Ice arrow magic effect duration is reduced by 1s for every successful hit (dmg >0) // Sleep magic effect is canceled for successful hit (dmg > 0) if (isCombo) { dmg += (int)attacker.Attributes[Stats.ComboBonus]; attributes |= DamageAttributes.Combo; } bool isDoubleDamage = Rand.NextRandomBool(attacker.Attributes[Stats.DoubleDamageChance]); if (isDoubleDamage) { dmg *= 2; attributes |= DamageAttributes.Double; } if (isClassicPvp && attackerPlayer!.CurrentMiniGame?.Definition.Type == MiniGameType.ChaosCastle) { // Further halve damage in Chaos Castle for classic PvP dmg /= 2; } } return defender.GetHitInfo((uint)dmg, attributes, attacker, (uint)manaToll); } /// /// Gets the hit information, calculates which part of the damage damages the shield and which the health. /// /// The defender. /// The damage. /// The attributes. /// The attacker. /// The mana reduction amount that is traded for damage (e.g. from Soul Barrier). /// The calculated hit info. public static HitInfo GetHitInfo(this IAttackable defender, uint damage, DamageAttributes attributes, IAttacker attacker, uint manaToll = 0) { var shieldBypass = Rand.NextRandomBool(attacker.Attributes[Stats.ShieldBypassChance]); if (shieldBypass || defender.Attributes[Stats.CurrentShield] < 1) { return new HitInfo(damage, 0, attributes, manaToll); } var shieldRatio = 0.90; shieldRatio -= attacker.Attributes[Stats.ShieldDecreaseRateIncrease]; shieldRatio += defender.Attributes[Stats.ShieldRateIncrease]; shieldRatio = Math.Max(0, shieldRatio); shieldRatio = Math.Min(1, shieldRatio); return new HitInfo((uint)(damage * (1 - shieldRatio)), (uint)(damage * shieldRatio), attributes, manaToll); } /// /// Applies the magic effect of the player's skill to the target. /// /// The target. /// The attacker. /// The skill entry. /// The hit information. public static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry, HitInfo? hitInfo = null) { if (skillEntry.PowerUps is null && attacker is Player player) { player.CreateMagicEffectPowerUp(skillEntry); } float chance = target is Player ? skillEntry.PowerUpChancePvp!.Value : skillEntry.PowerUpChance!.Value; if (!Rand.NextRandomBool(chance)) { return; } var duration = target is Player ? skillEntry.PowerUpDurationPvp! : skillEntry.PowerUpDuration!; var powerUps = target is Player ? skillEntry.PowerUpsPvp! : skillEntry.PowerUps!; await target.ApplyMagicEffectAsync(attacker, skillEntry.Skill!.MagicEffectDef!, duration, hitInfo, powerUps).ConfigureAwait(false); } /// /// Applies the regeneration of the player's skill to the target. /// /// The target. /// The player. /// The skill entry. public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Player player, SkillEntry skillEntry) { if (player.Attributes is null) { return; } skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill)); var skill = skillEntry.Skill; foreach (var powerUpDefinition in skill.MagicEffectDef?.PowerUpDefinitions ?? Enumerable.Empty()) { var regeneration = Stats.IntervalRegenerationAttributes.FirstOrDefault(r => r.CurrentAttribute == powerUpDefinition.TargetAttribute); if (regeneration != null) { var regenerationValue = player.Attributes.CreateElement(powerUpDefinition); var value = skillEntry.Level == 0 ? regenerationValue.Value : regenerationValue.Value + skillEntry.CalculateValue(); target.Attributes[regeneration.CurrentAttribute] = Math.Min( target.Attributes[regeneration.CurrentAttribute] + value, target.Attributes[regeneration.MaximumAttribute]); } else { player.Logger.LogWarning( $"Regeneration skill {skill.Name} is configured to regenerate a non-regeneration-able target attribute {powerUpDefinition.TargetAttribute}."); } } } /// /// Applies the elemental effects of a player's skill to the target. /// /// The target. /// The attacker. /// The skill entry. /// The hit information. /// The success of the appliance. public static async ValueTask TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry, HitInfo? hitInfo = null) { if (!target.IsAlive) { return false; } skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill)); var modifier = skillEntry.Skill.ElementalModifierTarget; var skipModifier = skillEntry.Skill.SkipElementalModifier; if (modifier is not null && !skipModifier) { var resistance = target.Attributes[modifier]; if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1))) { return false; } } var applied = false; if (skillEntry.Skill.MagicEffectDef is { } effectDefinition && !target.MagicEffectList.ActiveEffects.ContainsKey(effectDefinition.Number)) { // power-up is the wrong term here... it's more like a power-down ;-) await target.ApplyMagicEffectAsync(attacker, skillEntry, hitInfo).ConfigureAwait(false); applied = true; } if (modifier == Stats.LightningResistance && !skipModifier) { await target.MoveRandomlyAsync().ConfigureAwait(false); applied = true; } return applied; } /// /// Applies the elemental effects of a monster's skill to the target. /// /// The target. /// The attacker. /// The skill. /// The power ups. /// The duration. /// The hit information. /// The success of the appliance. public static async ValueTask TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IReadOnlyCollection<(AttributeDefinition Target, IElement Boost)> powerUps, IElement? duration, HitInfo? hitInfo) { if (!target.IsAlive) { return false; } var modifier = skill.ElementalModifierTarget; if (modifier is null) { return false; } var resistance = target.Attributes[modifier]; if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1))) { return false; } var applied = false; if (skill.MagicEffectDef is { } effectDefinition && !target.MagicEffectList.ActiveEffects.ContainsKey(effectDefinition.Number) && duration is not null && powerUps.Count > 0) { // power-up is the wrong term here... it's more like a power-down ;-) await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, [.. powerUps]).ConfigureAwait(false); applied = true; } if (modifier == Stats.LightningResistance) { await target.MoveRandomlyAsync().ConfigureAwait(false); applied = true; } return applied; } /// /// Applies the ammunition consumption. /// /// The attacker. /// The hit information. public static void ApplyAmmunitionConsumption(this IAttacker attacker, HitInfo hitInfo) { if (!hitInfo.Attributes.HasFlag(DamageAttributes.Reflected) && attacker.Attributes[Stats.AmmunitionConsumptionRate] > 0.0) { // Every hit needs ammo, missed or not if (attacker.Attributes[Stats.AmmunitionAmount] < attacker.Attributes[Stats.AmmunitionConsumptionRate]) { return; } attacker.Attributes[Stats.AmmunitionAmount] -= attacker.Attributes[Stats.AmmunitionConsumptionRate]; } } /// /// Checks if the target can be attacked by the player, based on the target restrictions of the skill. /// /// The target. /// The player. /// The skill. /// True, if the target can be attacked; Otherwise, false. public static bool CheckSkillTargetRestrictions(this IAttackable target, Player player, Skill skill) { var result = true; if (skill.TargetRestriction == SkillTargetRestriction.Self && target != player) { player.Logger.LogWarning($"Player '{player.Name}' tried to perform Skill '{skill.Name}' on target '{target}', but the skill is restricted to himself."); result = false; } else if (skill.TargetRestriction == SkillTargetRestriction.Party && target != player && target is IPartyMember partyMember && (player.Party is null || !player.Party.PartyList.Contains(partyMember))) { player.Logger.LogWarning($"Player '{player.Name}' tried to perform Skill '{skill.Name}' on target '{target}', but the skill is restricted to his party."); result = false; } else if (skill.TargetRestriction == SkillTargetRestriction.Player && target is not Player && target is Monster { SummonedBy: null }) { player.Logger.LogWarning($"Player '{player.Name}' tried to perform Skill '{skill.Name}' on target '{target}', but the skill is restricted to players."); result = false; } else { // everything fine } return result; } /// Moves the target randomly. /// The target. public static async ValueTask MoveRandomlyAsync(this IAttackable target) { if (target is IMovable movable && target.CurrentMap is { } map) { var terrain = map.Terrain; var newX = target.Position.X + Rand.NextInt(-1, 2); var newY = target.Position.Y + Rand.NextInt(-1, 2); var isNewXAllowed = newX is >= byte.MinValue and <= byte.MaxValue; var isNewYAllowed = newY is >= byte.MinValue and <= byte.MaxValue; if (isNewXAllowed && isNewYAllowed && terrain.AIgrid[newX, newY] == 1) { await movable.MoveAsync(new Point((byte)newX, (byte)newY)).ConfigureAwait(false); } } } /// /// Gets the required value of the . /// /// The attacker. /// The attribute requirement, e.g. of a skill. /// If set to true, applies. /// The required value. public static int GetRequiredValue(this IAttacker attacker, AttributeRequirement attributeRequirement, bool addExtraManaCost = false) { var modifier = 1.0f; if (ReductionModifiers.TryGetValue( attributeRequirement.Attribute ?? throw Error.NotInitializedProperty(attributeRequirement, nameof(attributeRequirement.Attribute)), out var reductionAttribute)) { modifier -= attacker.Attributes[reductionAttribute]; } var extraCost = 0; if (addExtraManaCost && attributeRequirement.Attribute == Stats.CurrentMana) { extraCost = (int)attacker.Attributes[Stats.SkillExtraManaCost]; } return (int)((attributeRequirement.MinimumValue + extraCost) * modifier); } /// /// Calculates the base experience for a killed target object. /// /// The killed target object. /// The level of the killer. /// The calculated base experience. public static double CalculateBaseExperience(this IAttackable killedObject, float killerLevel) { if (killedObject.IsSummonedMonster) { // Summoned monsters should not yield experience. return 0; } var targetLevel = killedObject.Attributes[Stats.Level]; var tempExperience = (targetLevel + 25) * targetLevel / 3.0; if (killerLevel > targetLevel + 10) { tempExperience *= (targetLevel + 10) / killerLevel; } if (killedObject.Attributes[Stats.Level] >= 65) { tempExperience += (targetLevel - 64) * (targetLevel / 4); } return Math.Max(tempExperience, 0) * 1.25; } /// /// Applies the mace mastery stun effect to the specified attackable. /// /// The attacker. /// The attackable. /// A task representing the asynchronous operation. public static async ValueTask ApplyMaceMasteryStunEffectAsync(this Player attacker, IAttackable attackable) { var stunEffectDefinition = attacker.GameContext.Configuration.MagicEffects.First(m => m.Number == StunnedMagicEffectNumber); var powerUp = attackable.Attributes.CreateElement(stunEffectDefinition.PowerUpDefinitions.First(pu => pu.TargetAttribute == Stats.IsStunned)); var magicEffect = new MagicEffect(TimeSpan.FromSeconds(2), stunEffectDefinition, [new MagicEffect.ElementWithTarget(powerUp, Stats.IsStunned)]); await attackable.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false); } private static bool IsAttackSuccessfulTo(this IAttacker attacker, IAttackable defender) { var hitChance = attacker.GetHitChanceTo(defender); return Rand.NextRandomBool(hitChance); } private static float GetHitChanceTo(this IAttacker attacker, IAttackable defender) { float defenseRate; float attackRate; if (defender is Player && attacker is Player) { defenseRate = defender.Attributes[Stats.DefenseRatePvp]; attackRate = attacker.Attributes[Stats.AttackRatePvp]; } else { defenseRate = defender.Attributes[Stats.DefenseRatePvm]; attackRate = attacker.Attributes[Stats.AttackRatePvm]; } float hitChance = 0.03f; if (defenseRate < attackRate) { hitChance = 1.0f - (defenseRate / attackRate); } return hitChance; } private static AttributeDefinition GetDefenseAttribute(this IAttackable defender, IAttacker attacker) { if (attacker is Player or AttackerSurrogate && defender is Player) { return Stats.DefensePvp; } return Stats.DefensePvm; } private static bool Overrates(this IAttackable defender, IAttacker attacker) { return defender.Attributes[Stats.DefenseRatePvm] > attacker.Attributes[Stats.AttackRatePvm]; } private static int GetDamage(this SkillEntry skillEntry, IAttacker attacker) { skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill)); var skill = skillEntry.Skill; var result = skill.AttackDamage; if (attacker is Player { } player && skill.MasterDefinition is { } masterDefinition) { if (masterDefinition.TargetAttribute is null) { result += (int)skillEntry.CalculateValue(); } foreach (var masterSkill in skill.GetBaseSkills(true)) { if (masterSkill.MasterDefinition!.TargetAttribute is null && player.SkillList!.GetSkill((ushort)masterSkill.Number) is { } masterSkillEntry) { result += (int)masterSkillEntry.CalculateValue(); } } } return result; } private static IAttributeSystem? EnsureSkillAttributes(this SkillEntry skillEntry, IAttributeSystem attackerSystem) { var skillAttributes = skillEntry.Attributes; if (skillAttributes is not null) { return skillAttributes; } if (skillEntry.Skill is not { } skill) { return skillAttributes; } var baseSkills = new List { skill }; baseSkills.AddRange(skillEntry.Skill.GetBaseSkills()); if (baseSkills.All(s => s.AttributeRelationships.Count == 0)) { return skillAttributes; } skillAttributes = skillEntry.Attributes = new AttributeSystem([], [], []); var levelElement = new SimpleElement(skillEntry.Level, AggregateType.AddRaw); skillEntry.PropertyChanged += (_, _) => levelElement.Value = skillEntry.Level; skillAttributes.AddElement(levelElement, Stats.SkillLevel); foreach (var relationship in baseSkills.SelectMany(s => s.AttributeRelationships)) { skillAttributes.AddAttributeRelationship(relationship, attackerSystem, relationship.AggregateType); } return skillAttributes; } private static void GetSkillDmg(this IAttacker attacker, SkillEntry? skillEntry, out int skillMinimumDamage, out int skillMaximumDamage, out DamageType damageType, out bool isSummonerSkill) { skillMinimumDamage = 0; skillMaximumDamage = 0; var attackerStats = attacker.Attributes; damageType = DamageType.Physical; isSummonerSkill = attackerStats[Stats.MinimumCurseBaseDmg] > 0 && damageType != DamageType.Fenrir; if (skillEntry?.Skill is not { } skill) { return; } damageType = skill.DamageType; var skillDamage = skillEntry.GetDamage(attacker); skillMinimumDamage += skillDamage; skillMaximumDamage += skillDamage + (skillDamage / 2); if (skill.ElementalModifierTarget is { } resistance && Stats.ElementResistanceToDamageBonus.TryGetValue(resistance, out var dmgBonus)) { skillMinimumDamage += (int)attackerStats[dmgBonus]; skillMaximumDamage += (int)attackerStats[dmgBonus]; } if (!isSummonerSkill && damageType != DamageType.Fenrir) { // For Summoner, the skill bonus gets added last skillMinimumDamage += (int)attackerStats[Stats.SkillDamageBonus]; skillMaximumDamage += (int)attackerStats[Stats.SkillDamageBonus]; } if (skillEntry.EnsureSkillAttributes(attackerStats) is { } skillAttributes) { skillMinimumDamage += (int)skillAttributes[Stats.SkillBaseDamageBonus]; skillMaximumDamage += (int)skillAttributes[Stats.SkillBaseDamageBonus]; var multiplier = skillAttributes[Stats.SkillBaseMultiplier]; if (multiplier > 0) { skillMinimumDamage = (int)(skillMinimumDamage * multiplier); skillMaximumDamage = (int)(skillMaximumDamage * multiplier); } } if (damageType == DamageType.Physical && attackerStats[Stats.HasDoubleWield] > 0) { // Because double wield dmg will be doubled later, we only take half of the skill dmg here (the skill is from a single weapon) skillMinimumDamage /= 2; skillMaximumDamage /= 2; } if (isSummonerSkill) { skillMinimumDamage += (int)(attackerStats[Stats.WizardryAndCurseBaseDmgBonus] + attackerStats[Stats.MinWizardryAndCurseDmgBonus]); skillMaximumDamage += (int)attackerStats[Stats.WizardryAndCurseBaseDmgBonus]; if (damageType == DamageType.Wizardry) { skillMinimumDamage += (int)attackerStats[Stats.BerserkerMinWizDmgBonus]; skillMaximumDamage += (int)attackerStats[Stats.BerserkerMaxWizDmgBonus]; } else { skillMinimumDamage += (int)attackerStats[Stats.BerserkerMinCurseDmgBonus]; skillMaximumDamage += (int)attackerStats[Stats.BerserkerMaxCurseDmgBonus]; } } } /// /// Returns the base damage of the attacker, using a specific skill. /// /// The attacker. /// Skill which is used. /// Minimum base damage. /// Maximum base damage. /// The damage type. private static void GetBaseDmg(this IAttacker attacker, SkillEntry? skill, out int minimumBaseDamage, out int maximumBaseDamage, out DamageType damageType) { minimumBaseDamage = 0; maximumBaseDamage = 0; var attackerStats = attacker.Attributes; GetSkillDmg(attacker, skill, out var skillMinimumDamage, out var skillMaximumDamage, out damageType, out var isSummonerSkill); switch (damageType) { // Common damage types case DamageType.Wizardry: minimumBaseDamage = (int)((attackerStats[Stats.MinimumWizBaseDmg] + skillMinimumDamage) * attackerStats[Stats.WizardryAttackDamageIncrease]); maximumBaseDamage = (int)((attackerStats[Stats.MaximumWizBaseDmg] + skillMaximumDamage) * attackerStats[Stats.WizardryAttackDamageIncrease]); break; case DamageType.Curse: minimumBaseDamage = (int)((attackerStats[Stats.MinimumCurseBaseDmg] + skillMinimumDamage) * attackerStats[Stats.CurseAttackDamageIncrease]); maximumBaseDamage = (int)((attackerStats[Stats.MaximumCurseBaseDmg] + skillMaximumDamage) * attackerStats[Stats.CurseAttackDamageIncrease]); break; case DamageType.Physical: minimumBaseDamage = (int)attackerStats[Stats.MinimumPhysBaseDmg] + skillMinimumDamage; maximumBaseDamage = (int)attackerStats[Stats.MaximumPhysBaseDmg] + skillMaximumDamage; break; case DamageType.Fenrir: minimumBaseDamage = (int)attackerStats[Stats.FenrirBaseDmg] + skillMinimumDamage; maximumBaseDamage = (int)attackerStats[Stats.FenrirBaseDmg] + skillMaximumDamage; break; default: // The skill has some other damage type defined that is not applicable to this calculation. break; } if (isSummonerSkill) { minimumBaseDamage += (int)attackerStats[Stats.SkillDamageBonus]; maximumBaseDamage += (int)attackerStats[Stats.SkillDamageBonus]; } } /// /// Applies the magic effect of the attacker's skill to the target. /// /// The target. /// The attacker. /// The magic effect definition. /// The duration. /// The hit information. /// The power ups of the effect. private static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, MagicEffectDefinition magicEffectDefinition, IElement duration, HitInfo? hitInfo, params (AttributeDefinition Target, IElement Boost)[] powerUps) { float finalDuration = duration.Value; if (magicEffectDefinition.DurationDependsOnTargetLevel) { var divisor = target is Player ? magicEffectDefinition.PlayerTargetLevelDivisor : magicEffectDefinition.MonsterTargetLevelDivisor; if (divisor != 0) { finalDuration -= target.Attributes[Stats.Level] / divisor; } } TimeSpan durationSpan = TimeSpan.FromSeconds(finalDuration); if (durationSpan < TimeSpan.FromSeconds(1)) { return; } MagicEffect magicEffect; if (magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsPoisoned)) { magicEffect = new PoisonMagicEffect(powerUps[0].Boost, magicEffectDefinition, durationSpan, attacker, target); } else if (magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsBleeding)) { if (hitInfo is not { } hit || hit.HealthDamage + hit.ShieldDamage < 1) { return; } var multiplier = magicEffectDefinition.Number == ExplosionMagicEffectNumber ? attacker.Attributes[Stats.BleedingDamageMultiplier] : 0.6f; var damage = (hit.HealthDamage + hit.ShieldDamage) * multiplier; magicEffect = new BleedingMagicEffect(powerUps[0].Boost, magicEffectDefinition, durationSpan, attacker, target, damage); } else if (magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsStunned)) { var stunChancePowerUp = powerUps.FirstOrDefault(p => p.Target == Stats.MasteryStunChance); if (stunChancePowerUp.Boost is null || !Rand.NextRandomBool(stunChancePowerUp.Boost.Value)) { return; } magicEffect = new MagicEffect(durationSpan, magicEffectDefinition, [.. powerUps.Where(p => p != stunChancePowerUp).Select(p => new MagicEffect.ElementWithTarget(p.Boost, p.Target))]); } else { magicEffect = new MagicEffect(durationSpan, magicEffectDefinition, [.. powerUps.Select(p => new MagicEffect.ElementWithTarget(p.Boost, p.Target))]); } if (magicEffect.Definition.SubType > 0 && await target.MagicEffectList.TryGetActiveEffectOfSubTypeAsync(magicEffect.Definition.SubType).ConfigureAwait(false) is { } existingEffect && existingEffect.Id != magicEffect.Id) { // The new effect replaces an existing effect with a different number await existingEffect.DisposeAsync().ConfigureAwait(false); } await target.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false); if (target is ISupportWalk walkSupporter && walkSupporter.IsWalking && magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsFrozen || e.TargetAttribute == Stats.IsStunned || e.TargetAttribute == Stats.IsAsleep)) { await walkSupporter.StopWalkingAsync().ConfigureAwait(false); // Since the actual coordinates could be out of sync with the client // coordinates, we simply update the position on the client side. if (walkSupporter is IObservable observable) { await observable.ForEachWorldObserverAsync(p => p.ObjectMovedAsync(walkSupporter, MoveType.Instant), true).ConfigureAwait(false); } } } private static int GetMasterSkillTreePhysicalPassiveDamageBonus(IAttacker attacker, bool preBuffsStage) { if (preBuffsStage) { if (attacker.Attributes[Stats.IsBowEquipped] > 0) { return (int)attacker.Attributes[Stats.BowStrBonusDamage]; } else if (attacker.Attributes[Stats.IsCrossBowEquipped] > 0) { return (int)attacker.Attributes[Stats.CrossBowStrBonusDamage]; } else if (attacker.Attributes[Stats.IsTwoHandedSwordEquipped] > 0) { return (int)attacker.Attributes[Stats.TwoHandedSwordStrBonusDamage]; } else { return 0; } } else { int bonusDamage = 0; // Always two-handed. if (attacker.Attributes[Stats.IsSpearEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.SpearBonusDamage]; } // Impossible to double wield. else if (attacker.Attributes[Stats.IsScepterEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.ScepterStrBonusDamage]; } // Impossible to double wield. else if (attacker.Attributes[Stats.IsGloveWeaponEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.GloveWeaponBonusDamage]; } else { if (attacker.Attributes[Stats.IsOneHandedSwordEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.OneHandedSwordBonusDamage]; } if (attacker.Attributes[Stats.IsMaceEquipped] > 0) { // In case of a double wielding with different possible bonuses, take the average. bonusDamage = (int)(bonusDamage == 0 ? attacker.Attributes[Stats.MaceBonusDamage] : (bonusDamage + attacker.Attributes[Stats.MaceBonusDamage]) / 2); } } return bonusDamage + (int)attacker.Attributes[Stats.MasterSkillPhysBonusDmg]; } } private static int GetMasterSkillTreeMasteryPvpDamageBonus(IAttacker attacker) { if (attacker.Attributes[Stats.IsTwoHandedSwordEquipped] > 0) { return (int)attacker.Attributes[Stats.TwoHandedSwordMasteryBonusDamage]; } else if (attacker.Attributes[Stats.IsTwoHandedStaffEquipped] > 0) { return (int)attacker.Attributes[Stats.TwoHandedStaffMasteryBonusDamage]; } else if (attacker.Attributes[Stats.IsCrossBowEquipped] > 0) { return (int)attacker.Attributes[Stats.CrossBowMasteryBonusDamage]; } else if (attacker.Attributes[Stats.IsStickEquipped] > 0) { return (int)attacker.Attributes[Stats.StickMasteryBonusDamage]; } else if (attacker.Attributes[Stats.IsScepterEquipped] > 0) { return (int)attacker.Attributes[Stats.ScepterMasteryBonusDamage]; } else { return 0; } } }