Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/DataModel/Configuration/Skill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ public partial class Skill
[MemberOfAggregate]
public virtual AreaSkillSettings? AreaSkillSettings { get; set; }

/// <summary>
/// Gets or sets the number of hits per attack.
/// </summary>
public short NumberOfHitsPerAttack { get; set; }

/// <inheritdoc />
public override string? ToString()
{
Expand Down
28 changes: 20 additions & 8 deletions src/GameLogic/AttackableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ public static async ValueTask<HitInfo> CalculateDamageAsync(this IAttacker attac
else
{
var defenseAttribute = defender.GetDefenseAttribute(attacker);
defense = (int)defender.Attributes[defenseAttribute];
defense -= (int)(defense * defender.Attributes[Stats.InnovationDefDecrement]);
defense = (int)(defender.Attributes[defenseAttribute] * defender.Attributes[Stats.DefenseDecrement]);
if (defense < 0)
{
defense = 0;
Expand Down Expand Up @@ -638,14 +637,27 @@ private static bool Overrates(this IAttackable defender, IAttacker attacker)
return defender.Attributes[Stats.DefenseRatePvm] > attacker.Attributes[Stats.AttackRatePvm];
}

private static int GetDamage(this SkillEntry skill)
private static int GetDamage(this SkillEntry skillEntry, IAttacker attacker)
{
skill.ThrowNotInitializedProperty(skill.Skill is null, nameof(skill.Skill));
skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));
var skill = skillEntry.Skill;

var result = skill.Skill.AttackDamage;
if (skill.Skill.MasterDefinition != null)
var result = skill.AttackDamage;
if (attacker is Player { } player && skill.MasterDefinition is { } masterDefinition)
{
result += (int)skill.CalculateValue();
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();
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the TargetAttribute is null we assume the skill adds damage. We need to go all the way back to the ancestor master skills because some may add damage.

}

return result;
Expand Down Expand Up @@ -698,7 +710,7 @@ private static void GetSkillDmg(this IAttacker attacker, SkillEntry? skillEntry,
}

damageType = skill.DamageType;
var skillDamage = skillEntry.GetDamage();
var skillDamage = skillEntry.GetDamage(attacker);
skillMinimumDamage += skillDamage;
skillMaximumDamage += skillDamage + (skillDamage / 2);

Expand Down
1 change: 1 addition & 0 deletions src/GameLogic/Attributes/MonsterAttributeHolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class MonsterAttributeHolder : IAttributeSystem
{ Stats.DamageReceiveDecrement, m => 1.0f },
{ Stats.AttackDamageIncrease, m => 1.0f },
{ Stats.ShieldBypassChance, m => 1.0f },
{ Stats.DefenseDecrement, m => 1.0f - m.Attributes.GetValueOfAttribute(Stats.InnovationDefDecrement) },
};

private static readonly IDictionary<AttributeDefinition, Action<AttackableNpcBase, float>> SetterMapping =
Expand Down
22 changes: 12 additions & 10 deletions src/GameLogic/Attributes/Stats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ public class Stats
/// <see cref="AggregateType.Multiplicate"/> values include:
/// <see cref="DefenseIncreaseWithEquippedShield"/>.
/// <see cref="AggregateType.AddFinal"/> values include:
/// Greater defense buff; MST bonus defense with shield (shield strengthener); MST dark horse strengthener; Jack O'Lantern Cry bonus (halved).
/// Greater defense buff; MST bonus defense with shield (shield strengthener); MST dark horse strengthener; Jack O'Lantern Cry bonus (halved); Berserker defense reduction.
/// </remarks>
public static AttributeDefinition DefenseFinal { get; } = new(new Guid("0888AD48-0CC8-47CA-B6A3-99F3771AA5FC"), "Final Defense", string.Empty);

Expand All @@ -747,10 +747,6 @@ public class Stats
/// <remarks>
/// <see cref="AggregateType.AddRaw"/> values include:
/// <see cref="DefenseFinal"/>.
/// <see cref="AggregateType.Multiplicate"/> values include:
/// Fire slash defense reduction.
/// <see cref="AggregateType.AddFinal"/> values include:
/// Berserker defense reduction.
/// </remarks>
public static AttributeDefinition DefensePvm { get; } = new(new Guid("B4201610-2824-4EC1-A145-76B15DB9DEC6"), "Defense (PvM)", string.Empty);

Expand All @@ -760,10 +756,6 @@ public class Stats
/// <remarks>
/// <see cref="AggregateType.AddRaw"/> values include:
/// <see cref="DefenseFinal"/>; pants guardian option (halved).
/// <see cref="AggregateType.Multiplicate"/> values include:
/// Fire slash defense reduction.
/// <see cref="AggregateType.AddFinal"/> values include:
/// Berserker defense reduction.
/// </remarks>
public static AttributeDefinition DefensePvp { get; } = new(new Guid("28D14EB7-1049-45BE-A7B7-D5E28E63943B"), "Defense (PvP)", string.Empty);

Expand All @@ -781,7 +773,7 @@ public class Stats
/// <see cref="AggregateType.Multiplicate"/> values include:
/// Complete set bonus multiplier (+10%); excellent DR option; socket DR option; MST PvM defense rate increase.
/// <see cref="AggregateType.AddFinal"/> values include:
/// MST bonus defense rate with shield (shield mastery).
/// MST bonus defense rate with shield (shield mastery); Phoenix Shot decrease block effect.
/// </remarks>
public static AttributeDefinition DefenseRatePvm { get; } = new(new Guid("C520DD2D-1B06-4392-95EE-3C41F33E68DA"), "Defense Rate (PvM)", string.Empty);

Expand Down Expand Up @@ -925,6 +917,16 @@ public class Stats
/// </summary>
public static AttributeDefinition InnovationDefDecrement { get; } = new(new Guid("D8B3B1C9-B409-4A07-8F4D-8F315DCB173A"), "Innovation Defense Decrement", "The defense decrement due to the magic effect of innovation skill, which is multiplied with the final defense and subtracted from it.");

/// <summary>
/// Gets the defense decrement attribute definition.
/// </summary>
/// <remarks>
/// Includes the multiplier <see cref="Stats.InnovationDefDecrement"/>, and the ones from the magic effects of beast uppercut (RF) and fire slash (MG).
/// Beast uppercut and fire slash share the same magic effect number, while innovation's is different.
/// This means that the first two effects can only exist 1 at a time, while innovation can coexist with either of them.
/// </remarks>
public static AttributeDefinition DefenseDecrement { get; } = new(new Guid("D19A0E33-5C9A-4B8E-AF12-3C4D5E6F7890"), "Defense Decrement", "The defense decrement due to magic effects of various skills, which is multiplied with the final defense and subtracted from it.");

/// <summary>
/// Gets the 'is shield equipped' attribute definition.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/GameLogic/DamageAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ public enum DamageAttributes
/// The damage includes the combo bonus.
/// </summary>
Combo = 128,

/// <summary>
/// The damage is a non-final hit of a quick sequence from a rage fighter skill.
/// </summary>
RageFighterStreakHit = 256,

/// <summary>
/// The damage is the final hit of a quick sequence from a rage fighter skill.
/// </summary>
RageFighterStreakFinalHit = 512,
}
7 changes: 6 additions & 1 deletion src/GameLogic/IAttackable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ public interface IAttackable : IIdentifiable, ILocateable
/// <param name="skill">The skill.</param>
/// <param name="isCombo">If set to <c>true</c>, the attacker did a combination of skills.</param>
/// <param name="damageFactor">The damage factor.</param>
/// <param name="isFinalStreakHit">
/// Not <c>null</c> when it's a rage fighter multiple hit skill:
/// <c>true</c>, if it's the final hit;
/// <c>false</c>, for other hits.
/// </param>
/// <returns>Returns information about the damage inflicted.</returns>
ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0);
ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null);

/// <summary>
/// Reflects the damage which was done previously with <see cref="AttackByAsync" /> or even <see cref="ReflectDamageAsync" /> to the <paramref name="reflector" />.
Expand Down
23 changes: 19 additions & 4 deletions src/GameLogic/NPC/AttackableNpcBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected AttackableNpcBase(MonsterSpawnArea spawnInfo, MonsterDefinition stats,
/// <value>
/// <c>true</c> if teleporting; otherwise, <c>false</c>.
/// </value>
/// <remarks>Teleporting for monsters oor npcs is not implemented yet.</remarks>
/// <remarks>Teleporting for monsters or npcs is not implemented yet.</remarks>
public bool IsTeleporting => false;

/// <inheritdoc />
Expand Down Expand Up @@ -98,7 +98,7 @@ public int Health
|| (this.SpawnArea.SpawnTrigger == SpawnTrigger.AutomaticDuringWave && (this._eventStateProvider?.IsSpawnWaveActive(this.SpawnArea.WaveNumber) ?? false));

/// <inheritdoc />
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null)
{
if (this.Definition.ObjectKind == NpcObjectKind.Guard)
{
Expand All @@ -112,7 +112,7 @@ public int Health
attacker.ApplyAmmunitionConsumption(hitInfo);
}

await this.HitAsync(hitInfo, attacker, skill?.Skill).ConfigureAwait(false);
await this.HitAsync(hitInfo, attacker, skill?.Skill, isFinalStreakHit).ConfigureAwait(false);

if (hitInfo.HealthDamage > 0)
{
Expand Down Expand Up @@ -181,7 +181,12 @@ protected virtual void OnRemoveFromMap()
/// <param name="hitInfo">The hit information.</param>
/// <param name="attacker">The attacker.</param>
/// <param name="skill">The skill.</param>
protected async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? skill)
/// <param name="isFinalStreakHit">
/// Not <c>null</c> when it's a rage fighter multiple hit skill:
/// <c>true</c>, if it's the final hit;
/// <c>false</c>, for other hits.
/// </param>
protected async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? skill, bool? isFinalStreakHit = null)
{
if (!this.IsAlive)
{
Expand All @@ -193,6 +198,16 @@ protected async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? s
var player = this.GetHitNotificationTarget(attacker);
if (player is not null)
{
if (isFinalStreakHit.HasValue)
{
hitInfo.Attributes |= DamageAttributes.RageFighterStreakHit;

if (isFinalStreakHit.Value || killed)
{
hitInfo.Attributes |= DamageAttributes.RageFighterStreakFinalHit;
}
}
Comment on lines +201 to +209
Copy link
Contributor Author

@ze-dom ze-dom Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zTeamS6.3, MuMain 5.2
Apparently the original didn't check if the target has died, which would cause the damage values not to appear if the target died on a non-final hit: https://www.youtube.com/watch?v=Vo6aI34Y3FE
But this way we can always show the damage values.


await player.InvokeViewPlugInAsync<IShowHitPlugIn>(p => p.ShowHitAsync(this, hitInfo)).ConfigureAwait(false);
player.GameContext.PlugInManager.GetPlugInPoint<IAttackableGotHitPlugIn>()?.AttackableGotHit(this, attacker, hitInfo);
}
Expand Down
2 changes: 1 addition & 1 deletion src/GameLogic/NPC/SoccerBall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public SoccerBall(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap m
public DeathInformation? LastDeath => null;

/// <inheritdoc />
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null)
{
var direction = attacker.GetDirectionTo(this);
await this.MoveToDirectionAsync(direction, skill is { }).ConfigureAwait(false);
Expand Down
21 changes: 16 additions & 5 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ public ValueTask ShowBlueMessageAsync(string message)
}

/// <inheritdoc/>
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null)
{
if (this.Attributes is null)
{
Expand Down Expand Up @@ -722,7 +722,7 @@ public ValueTask ShowBlueMessageAsync(string message)
this.Attributes[Stats.CurrentMana] = (manaFullyRecovered ? this.Attributes[Stats.MaximumMana] : this.Attributes[Stats.CurrentMana]) - hitInfo.ManaToll;
}

await this.HitAsync(hitInfo, attacker, skill?.Skill).ConfigureAwait(false);
await this.HitAsync(hitInfo, attacker, skill?.Skill, isFinalStreakHit).ConfigureAwait(false);
await this.DecreaseItemDurabilityAfterHitAsync(hitInfo).ConfigureAwait(false);

if (attacker as IPlayerSurrogate is { } playerSurrogate)
Expand Down Expand Up @@ -1610,8 +1610,8 @@ IElement AppedMasterSkillPowerUp(SkillEntry masterSkillEntry, PowerUpDefinition
return powerUp;
}

if (masterSkillDefinition.TargetAttribute is not { } masterSkillTargetAttribute
|| masterSkillTargetAttribute == powerUpDef.TargetAttribute)
if (masterSkillDefinition.TargetAttribute is { } masterSkillTargetAttribute
&& masterSkillTargetAttribute == powerUpDef.TargetAttribute)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer blindly add it. Only if the target power-up matches. Like indicated above, if it's null, then it adds to damage and doesn't belong here.

{
var additionalValue = new SimpleElement(masterSkillEntry.CalculateValue(), masterSkillEntry.Skill.MasterDefinition?.Aggregation ?? powerUp.AggregateType);
powerUp = new CombinedElement(powerUp, additionalValue);
Expand Down Expand Up @@ -2085,7 +2085,7 @@ private async ValueTask<ExitGate> GetSpawnGateOfCurrentMapAsync()
?? throw new InvalidOperationException($"Game map {spawnTargetMapDefinition} has no spawn gate.");
}

private async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? skill)
private async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? skill, bool? isFinalStreakHit = null)
{
this.Summon?.Item2.RegisterHit(attacker);
var healthDamage = hitInfo.HealthDamage;
Expand All @@ -2101,6 +2101,17 @@ private async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? ski
}

this.Attributes[Stats.CurrentHealth] -= healthDamage;

if (isFinalStreakHit.HasValue)
{
hitInfo.Attributes |= DamageAttributes.RageFighterStreakHit;

if (isFinalStreakHit.Value || this.Attributes[Stats.CurrentHealth] < 1)
{
hitInfo.Attributes |= DamageAttributes.RageFighterStreakFinalHit;
}
}
Comment on lines +2105 to +2113

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for setting the RageFighterStreakHit and RageFighterStreakFinalHit attributes is duplicated from AttackableNpcBase.HitAsync. To improve maintainability and prevent future inconsistencies, this logic should be extracted into a shared helper method. An extension method on HitInfo, such as hitInfo.SetRageFighterStreakAttributes(isFinalStreakHit, isKilled), could be a good approach.


await this.InvokeViewPlugInAsync<IShowHitPlugIn>(p => p.ShowHitAsync(this, hitInfo)).ConfigureAwait(false);
if (attacker is IWorldObserver observer)
{
Expand Down
11 changes: 9 additions & 2 deletions src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,15 +320,22 @@ private async ValueTask PerformAutomaticHitsAsync(Player player, ushort extraTar
private async ValueTask ApplySkillAsync(Player player, SkillEntry skillEntry, IAttackable target, Point targetAreaCenter, bool isCombo)
{
skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));
var skill = skillEntry.Skill;

if (skillEntry.Skill.SkillType == SkillType.Buff)
if (skill.SkillType == SkillType.Buff)
{
await target.ApplyMagicEffectAsync(player, skillEntry).ConfigureAwait(false);
return;
}

var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo).ConfigureAwait(false);
var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo, 1, skill.NumberOfHitsPerAttack > 1 ? false : null).ConfigureAwait(false);
await target.TryApplyElementalEffectsAsync(player, skillEntry).ConfigureAwait(false);

for (int hit = 2; hit <= skill.NumberOfHitsPerAttack; hit++)
{
await target.AttackByAsync(player, skillEntry, isCombo, 1, hit == skill.NumberOfHitsPerAttack).ConfigureAwait(false);
}
Comment on lines +331 to +337

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for performing a multi-hit attack is duplicated in TargetedSkillDefaultPlugin.cs and DragonRoarSkillPlugIn.cs. To improve maintainability and ensure consistent behavior, this logic should be extracted into a single, reusable helper method. For instance, an extension method like IAttackable.AttackWithMultiHitsAsync(...) would be a suitable solution.


var baseSkill = skillEntry.GetBaseSkill();

if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillPlugIn>(baseSkill.Number) is { } strategy)
Expand Down
Loading