//
// 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;
}
}
}