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
20 changes: 19 additions & 1 deletion src/GameLogic/AttackableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -861,9 +861,27 @@ private static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IA
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.StunChance);
if (stunChancePowerUp.Boost is null || !Rand.NextRandomBool(Convert.ToDouble(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)).ToArray());
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);
Expand Down
2 changes: 1 addition & 1 deletion src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1633,7 +1633,7 @@ void AddSkillPowersToResult(ICollection<PowerUpDefinition> powerUps, ref (Attrib
var extendsDuration = masterSkillEntry.Skill?.MasterDefinition?.ExtendsDuration ?? false;
if (extendsDuration && !durationExtended)
{
durationElement = new CombinedElement(durationElement, new ConstantElement(skillEntry.CalculateValue()));
durationElement = new CombinedElement(durationElement, new ConstantElement(masterSkillEntry.CalculateValue()));
}
else if (extendsDuration)
{
Expand Down
17 changes: 15 additions & 2 deletions src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// <copyright file="AreaSkillAttackAction.cs" company="MUnique">
// <copyright file="AreaSkillAttackAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;

using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
Expand All @@ -21,6 +20,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
public class AreaSkillAttackAction
{
private const int UndefinedTarget = 0xFFFF;
private const short ElectricSpikeSkillId = 65;

private static readonly ConcurrentDictionary<AreaSkillSettings, FrustumBasedTargetFilter> FrustumFilters = new();

Expand Down Expand Up @@ -68,6 +68,7 @@ private static bool AreaSkillSettingsAreDefault([NotNullWhen(true)] AreaSkillSet
&& settings.DelayPerOneDistance <= TimeSpan.Zero
&& settings.MinimumNumberOfHitsPerTarget == 1
&& settings.MaximumNumberOfHitsPerTarget == 1
&& settings.MinimumNumberOfHitsPerAttack == 0
&& settings.MaximumNumberOfHitsPerAttack == 0
&& Math.Abs(settings.HitChancePerDistanceMultiplier - 1.0) <= 0.00001f;
}
Expand Down Expand Up @@ -335,6 +336,18 @@ private async ValueTask PerformAutomaticHitsAsync(Player player, ushort extraTar
}
}

if (skillEntry.Skill?.Number == ElectricSpikeSkillId && attackCount > 0 && player.Attributes![Stats.NearbyPartyMemberCount] > 0)
{
foreach (var partyMember in player.Party?.PartyList.OfType<Player>().Where(m => m.Observers.Contains(player)) ?? [])
{
if (partyMember.Attributes is { } memberAttributes)
{
memberAttributes[Stats.CurrentHealth] *= 0.8f;
memberAttributes[Stats.CurrentMana] *= 0.95f;
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

return extraTarget;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackab
}

var currentDistance = startingPoint.EuclideanDistanceTo(currentTarget);
while (currentDistance < skillEntry.Skill.Range)
for (int i = 0; i < 3; i++)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

{
var nextTarget = currentTarget.CalculateTargetPoint(direction);
if (!currentMap.Terrain.WalkMap[nextTarget.X, nextTarget.Y]
Expand Down
99 changes: 99 additions & 0 deletions src/GameLogic/PlayerActions/Skills/FireScreamSkillPlugIn.cs
Copy link
Copy Markdown
Contributor Author

@ze-dom ze-dom Apr 9, 2026

Choose a reason for hiding this comment

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

zTeamS6.3: 0, 1, 2
emu: 0, 1, 2

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// <copyright file="FireScreamSkillPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;

using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.GuildWar;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// Handles the fire scream skill of the dark lord class. Based on a chance, it does an additional damage (explosion) to any targets in a radius which origin is the target itself.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.FireScreamSkillPlugIn_Name), Description = nameof(PlugInResources.FireScreamSkillPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("7E2F9B4A-D6C1-4F8E-A3B5-21E8C7F9D541")]
public class FireScreamSkillPlugIn : IAreaSkillPlugIn
{
/// <inheritdoc />
public short Key => 78;

private short Radius => 2;

/// <inheritdoc />
public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
{
if (hitInfo is not { } hit || !Rand.NextRandomBool(0.3))
{
return;
}

var attackDamage = hit.HealthDamage + hit.ShieldDamage;
var explosionDamage = attackDamage / 10;
if (explosionDamage < 1)
{
return;
}

bool FilterTarget(IAttackable attackable)
{
if (attackable == attacker)
{
return false;
}

if (attackable is Monster { SummonedBy: null } or Destructible)
{
return true;
}

if (attackable is Monster { SummonedBy: not null } summoned)
{
return FilterTarget(summoned.SummonedBy);
}

if (attackable is Player { DuelRoom.State: DuelState.DuelStarted } targetPlayer
&& attacker is Player { DuelRoom.State: DuelState.DuelStarted } duelPlayer
&& targetPlayer.DuelRoom == duelPlayer.DuelRoom
&& targetPlayer.DuelRoom.IsDuelist(targetPlayer) && targetPlayer.DuelRoom.IsDuelist(duelPlayer))
{
return true;
}

if (attackable is Player { GuildWarContext.State: GuildWarState.Started } guildWarTarget
&& attacker is Player { GuildWarContext.State: GuildWarState.Started } guildWarAttacker
&& guildWarTarget.GuildWarContext == guildWarAttacker.GuildWarContext)
{
return true;
}

return false;
}

var explosionTargets = target.CurrentMap?
.GetAttackablesInRange(target.Position, this.Radius)
.Where(a => a.GetDistanceTo(target) <= this.Radius)
.Where(FilterTarget) ?? [];
if (!explosionTargets.Any())
{
return;
}

// Delay the explosion a little bit, so the client can show the hit values staggered
await Task.Delay(100).ConfigureAwait(false);

foreach (var explosionTarget in explosionTargets)
{
if (explosionTarget.IsActive())
{
// We just need to apply the damage, so we can resort to the bleeding damage method which has DamageAttributes.Undefined
await explosionTarget.ApplyBleedingDamageAsync(attacker, explosionDamage).ConfigureAwait(false);
}
}
}
}
18 changes: 18 additions & 0 deletions src/GameLogic/Properties/PlugInResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/GameLogic/Properties/PlugInResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@
<data name="EarthShakeSkillPlugIn_Description" xml:space="preserve">
<value>Handles the earth shake skill of the dark horse. Pushes the targets away from the attacker.</value>
</data>
<data name="FireScreamSkillPlugIn_Name" xml:space="preserve">
<value>Fire scream skill</value>
</data>
<data name="FireScreamSkillPlugIn_Description" xml:space="preserve">
<value>Handles the fire scream skill of the dark lord class. Based on a chance, it does an additional damage (explosion) to any targets in a radius which origin is the target itself.</value>
</data>
<data name="ForceSkillAction_Name" xml:space="preserve">
<value>ForceSkillAction</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ public override void Initialize()
magicEffect.Number = (byte)MagicEffectNumber.CriticalDamageIncrease;
magicEffect.Name = "Critical Damage Increase Skill Effect";
magicEffect.InformObservers = true;
magicEffect.SubType = 17;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

zTeamS6.3; emu: 1, 2

magicEffect.SendDuration = false;
magicEffect.StopByDeath = true;
magicEffect.Duration = this.Context.CreateNew<PowerUpDefinitionValue>();
magicEffect.Duration.ConstantValue.Value = 60f;
magicEffect.Duration.MaximumValue = 180f;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

emu


var durationPerEnergy = this.Context.CreateNew<AttributeRelationship>();
durationPerEnergy.InputAttribute = Stats.TotalEnergy.GetPersistent(this.GameConfiguration);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// <copyright file="CriticalDamageIncreaseMasteryEffectInitializer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Persistence.Initialization.Skills;

using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Attributes;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Attributes;

/// <summary>
/// Initializer which initializes the critical damage increase mastery effect.
/// </summary>
public class CriticalDamageIncreaseMasteryEffectInitializer : InitializerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="CriticalDamageIncreaseMasteryEffectInitializer"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="gameConfiguration">The game configuration.</param>
public CriticalDamageIncreaseMasteryEffectInitializer(IContext context, GameConfiguration gameConfiguration)
: base(context, gameConfiguration)
{
}

/// <inheritdoc/>
public override void Initialize()
{
var magicEffect = this.Context.CreateNew<MagicEffectDefinition>();
this.GameConfiguration.MagicEffects.Add(magicEffect);
magicEffect.Number = (byte)MagicEffectNumber.CriticalDamageIncreaseMastery;
magicEffect.Name = "Critical Damage Increase Mastery Skill Effect";

var critDmgIncEffect = this.GameConfiguration.MagicEffects.First(e => e.Number == (short)MagicEffectNumber.CriticalDamageIncrease);
magicEffect.InformObservers = critDmgIncEffect.InformObservers;
magicEffect.SubType = critDmgIncEffect.SubType;
magicEffect.SendDuration = critDmgIncEffect.SendDuration;
magicEffect.StopByDeath = critDmgIncEffect.StopByDeath;
magicEffect.Duration = this.Context.CreateNew<PowerUpDefinitionValue>();
magicEffect.Duration.ConstantValue.Value = critDmgIncEffect.Duration!.ConstantValue.Value;
magicEffect.Duration.MaximumValue = critDmgIncEffect.Duration.MaximumValue;

foreach (var durationRelatedValue in critDmgIncEffect.Duration.RelatedValues)
{
var durationRelatedValueCopy = this.Context.CreateNew<AttributeRelationship>();
durationRelatedValueCopy.InputAttribute = durationRelatedValue.InputAttribute!.GetPersistent(this.GameConfiguration);
durationRelatedValueCopy.InputOperator = durationRelatedValue.InputOperator;
durationRelatedValueCopy.InputOperand = durationRelatedValue.InputOperand;
magicEffect.Duration.RelatedValues.Add(durationRelatedValueCopy);
}

foreach (var powerUp in critDmgIncEffect.PowerUpDefinitions)
{
var powerUpCopy = this.Context.CreateNew<PowerUpDefinition>();
magicEffect.PowerUpDefinitions.Add(powerUpCopy);
powerUpCopy.TargetAttribute = powerUp.TargetAttribute!.GetPersistent(this.GameConfiguration);
powerUpCopy.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
powerUpCopy.Boost.ConstantValue.Value = powerUp.Boost!.ConstantValue.Value;

foreach (var boostRelatedValue in powerUp.Boost.RelatedValues)
{
var boostRelatedValueCopy = this.Context.CreateNew<AttributeRelationship>();
boostRelatedValueCopy.InputAttribute = boostRelatedValue.InputAttribute!.GetPersistent(this.GameConfiguration);
boostRelatedValueCopy.InputOperator = boostRelatedValue.InputOperator;
boostRelatedValueCopy.InputOperand = boostRelatedValue.InputOperand;
powerUpCopy.Boost.RelatedValues.Add(boostRelatedValueCopy);
}
}

var critChancePowerUpDefinition = this.Context.CreateNew<PowerUpDefinition>();
magicEffect.PowerUpDefinitions.Add(critChancePowerUpDefinition);
critChancePowerUpDefinition.TargetAttribute = Stats.CriticalDamageChance.GetPersistent(this.GameConfiguration);
critChancePowerUpDefinition.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
critChancePowerUpDefinition.Boost.ConstantValue.Value = 0;
}
}
5 changes: 5 additions & 0 deletions src/Persistence/Initialization/Skills/MagicEffectNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ internal enum MagicEffectNumber : short
/// </summary>
WizEnhance3 = 139,

/// <summary>
/// The critical damage increase mastery effect.
/// </summary>
CriticalDamageIncreaseMastery = 148,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mastery uses a new magic effect number.
Previously (bugged) would display both:

Image

#region Artificial effects which are not sent to the client, starting at 200.

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,20 @@ public override void Initialize()
magicEffect.InformObservers = true;
magicEffect.SendDuration = false;
magicEffect.StopByDeath = true;
magicEffect.Duration = this.Context.CreateNew<PowerUpDefinitionValue>();
magicEffect.Duration.ConstantValue.Value = 2; // 2 seconds
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

DK also has a stun effect of 2s in his MST (to do)
Image

Image


var isStunnedPowerUpDefinition = this.Context.CreateNew<PowerUpDefinition>();
magicEffect.PowerUpDefinitions.Add(isStunnedPowerUpDefinition);
isStunnedPowerUpDefinition.TargetAttribute = Stats.IsStunned.GetPersistent(this.GameConfiguration);
isStunnedPowerUpDefinition.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
isStunnedPowerUpDefinition.Boost.ConstantValue.Value = 1;

// Placeholder for master skills that use this effect
var stunChancePowerUpDefinition = this.Context.CreateNew<PowerUpDefinition>();
magicEffect.PowerUpDefinitions.Add(stunChancePowerUpDefinition);
stunChancePowerUpDefinition.TargetAttribute = Stats.StunChance.GetPersistent(this.GameConfiguration);
stunChancePowerUpDefinition.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
stunChancePowerUpDefinition.Boost.ConstantValue.Value = 0;
}
}
Loading