Skip to content

Commit dc5011d

Browse files
committed
First commit
1 parent cfe526f commit dc5011d

34 files changed

Lines changed: 5900 additions & 55 deletions

src/DataModel/Configuration/AreaSkillSettings.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public partial class AreaSkillSettings
6868
/// </summary>
6969
public int MaximumNumberOfHitsPerTarget { get; set; }
7070

71+
/// <summary>
72+
/// Gets or sets the minimum number of hits per attack, after which subsequent hits have a reduced chance to hit.
73+
/// </summary>
74+
public int MinimumNumberOfHitsPerAttack { get; set; }
75+
7176
/// <summary>
7277
/// Gets or sets the maximum number of hits per attack.
7378
/// </summary>
@@ -76,7 +81,7 @@ public partial class AreaSkillSettings
7681
/// <summary>
7782
/// Gets or sets the hit chance per distance multiplier.
7883
/// E.g. when set to 0.9 and the target is 5 steps away,
79-
/// the chance to hit is 5^0.9 = 0.59.
84+
/// the chance to hit is 0.9^5 = 0.59.
8085
/// </summary>
8186
public float HitChancePerDistanceMultiplier { get; set; }
8287

@@ -88,6 +93,11 @@ public partial class AreaSkillSettings
8893
/// </summary>
8994
public int ProjectileCount { get; set; }
9095

96+
/// <summary>
97+
/// Gets or sets the effect range of the skill, which is the maximum distance from the target area center.
98+
/// </summary>
99+
public int EffectRange { get; set; }
100+
91101
/// <inheritdoc />
92102
public override string ToString()
93103
{

src/DataModel/Configuration/Skill.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,17 @@ public partial class Skill
286286
/// </summary>
287287
public virtual AttributeDefinition? ElementalModifierTarget { get; set; }
288288

289+
/// <summary>
290+
/// Gets or sets a value indicating whether the elemental modifier resistance should be ignored, which means the skill uses a specific logic.
291+
/// </summary>
292+
/// <remarks>
293+
/// Not all skills have magic effects corresponding to their element,
294+
/// e.g. Pollution (book of lagle, lightning) has a 100% chance iceing effect.
295+
/// Other skills have magic effects which may or may not be related to their element, but which apply regardless of resistance,
296+
/// e.g. Explosion (book of samut, fire) and Requiem (book of neil, wind) have 100% bleeding effects.
297+
/// </remarks>
298+
public bool SkipElementalModifier { get; set; }
299+
289300
/// <summary>
290301
/// Gets or sets the magic effect definition. It will be applied for buff skills.
291302
/// </summary>

src/GameLogic/AttackableExtensions.cs

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ public static HitInfo GetHitInfo(this IAttackable defender, uint damage, DamageA
314314
/// <param name="target">The target.</param>
315315
/// <param name="attacker">The attacker.</param>
316316
/// <param name="skillEntry">The skill entry.</param>
317-
public static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry)
317+
/// <param name="hitInfo">The hit information.</param>
318+
public static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry, HitInfo? hitInfo = null)
318319
{
319320
if (skillEntry.PowerUps is null && attacker is Player player)
320321
{
@@ -329,7 +330,7 @@ public static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAt
329330

330331
var duration = target is Player ? skillEntry.PowerUpDurationPvp! : skillEntry.PowerUpDuration!;
331332
var powerUps = target is Player ? skillEntry.PowerUpsPvp! : skillEntry.PowerUps!;
332-
await target.ApplyMagicEffectAsync(attacker, skillEntry.Skill!.MagicEffectDef!, duration, powerUps).ConfigureAwait(false);
333+
await target.ApplyMagicEffectAsync(attacker, skillEntry.Skill!.MagicEffectDef!, duration, hitInfo, powerUps).ConfigureAwait(false);
333334
}
334335

335336
/// <summary>
@@ -373,8 +374,9 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl
373374
/// <param name="target">The target.</param>
374375
/// <param name="attacker">The attacker.</param>
375376
/// <param name="skillEntry">The skill entry.</param>
377+
/// <param name="hitInfo">The hit information.</param>
376378
/// <returns>The success of the appliance.</returns>
377-
public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry)
379+
public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry, HitInfo? hitInfo = null)
378380
{
379381
if (!target.IsAlive)
380382
{
@@ -383,15 +385,15 @@ public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackab
383385

384386
skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));
385387
var modifier = skillEntry.Skill.ElementalModifierTarget;
386-
if (modifier is null)
387-
{
388-
return false;
389-
}
388+
var skipModifier = skillEntry.Skill.SkipElementalModifier;
390389

391-
var resistance = target.Attributes[modifier];
392-
if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1)))
390+
if (modifier is not null && !skipModifier)
393391
{
394-
return false;
392+
var resistance = target.Attributes[modifier];
393+
if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1)))
394+
{
395+
return false;
396+
}
395397
}
396398

397399
var applied = false;
@@ -400,11 +402,11 @@ public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackab
400402
&& !target.MagicEffectList.ActiveEffects.ContainsKey(effectDefinition.Number))
401403
{
402404
// power-up is the wrong term here... it's more like a power-down ;-)
403-
await target.ApplyMagicEffectAsync(attacker, skillEntry).ConfigureAwait(false);
405+
await target.ApplyMagicEffectAsync(attacker, skillEntry, hitInfo).ConfigureAwait(false);
404406
applied = true;
405407
}
406408

407-
if (modifier == Stats.LightningResistance)
409+
if (modifier == Stats.LightningResistance && !skipModifier)
408410
{
409411
await target.MoveRandomlyAsync().ConfigureAwait(false);
410412
applied = true;
@@ -422,10 +424,9 @@ public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackab
422424
/// <param name="powerUp">The power up.</param>
423425
/// <param name="duration">The duration.</param>
424426
/// <param name="targetAttribute">The target attribute.</param>
425-
/// <returns>
426-
/// The success of the appliance.
427-
/// </returns>
428-
public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IElement? powerUp, IElement? duration, AttributeDefinition? targetAttribute)
427+
/// <param name="hitInfo">The hit information.</param>
428+
/// <returns>The success of the appliance.</returns>
429+
public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IElement? powerUp, IElement? duration, AttributeDefinition? targetAttribute, HitInfo? hitInfo)
429430
{
430431
if (!target.IsAlive)
431432
{
@@ -453,7 +454,7 @@ public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackab
453454
&& targetAttribute is not null)
454455
{
455456
// power-up is the wrong term here... it's more like a power-down ;-)
456-
await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, (targetAttribute, powerUp)).ConfigureAwait(false);
457+
await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, (targetAttribute, powerUp)).ConfigureAwait(false);
457458
applied = true;
458459
}
459460

@@ -819,8 +820,9 @@ private static void GetBaseDmg(this IAttacker attacker, SkillEntry? skill, out i
819820
/// <param name="attacker">The attacker.</param>
820821
/// <param name="magicEffectDefinition">The magic effect definition.</param>
821822
/// <param name="duration">The duration.</param>
823+
/// <param name="hitInfo">The hit information.</param>
822824
/// <param name="powerUps">The power ups of the effect.</param>
823-
private static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, MagicEffectDefinition magicEffectDefinition, IElement duration, params (AttributeDefinition Target, IElement Boost)[] powerUps)
825+
private static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, MagicEffectDefinition magicEffectDefinition, IElement duration, HitInfo? hitInfo, params (AttributeDefinition Target, IElement Boost)[] powerUps)
824826
{
825827
float finalDuration = duration.Value;
826828

@@ -839,10 +841,24 @@ private static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IA
839841
return;
840842
}
841843

842-
var isPoisonEffect = magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsPoisoned);
843-
var magicEffect = isPoisonEffect
844-
? new PoisonMagicEffect(powerUps[0].Boost, magicEffectDefinition, durationSpan, attacker, target)
845-
: new MagicEffect(durationSpan, magicEffectDefinition, powerUps.Select(p => new MagicEffect.ElementWithTarget(p.Boost, p.Target)).ToArray());
844+
MagicEffect magicEffect;
845+
if (magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsPoisoned))
846+
{
847+
magicEffect = new PoisonMagicEffect(powerUps[0].Boost, magicEffectDefinition, durationSpan, attacker, target);
848+
}
849+
else if (magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsBleeding))
850+
{
851+
if (hitInfo is not { } hit || hit.HealthDamage + hit.ShieldDamage < 1)
852+
{
853+
return;
854+
}
855+
856+
magicEffect = new BleedingMagicEffect(powerUps[0].Boost, magicEffectDefinition, durationSpan, attacker, target, hit.HealthDamage + hit.ShieldDamage);
857+
}
858+
else
859+
{
860+
magicEffect = new MagicEffect(durationSpan, magicEffectDefinition, powerUps.Select(p => new MagicEffect.ElementWithTarget(p.Boost, p.Target)).ToArray());
861+
}
846862

847863
await target.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
848864
if (target is ISupportWalk walkSupporter

src/GameLogic/Attributes/Stats.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,12 @@ public class Stats
957957
/// </summary>
958958
public static AttributeDefinition IsAsleep { get; } = new(new Guid("0518F532-7A8F-4491-8A23-98B620608CB3"), "Is asleep", "The player is asleep and can't move until hit.");
959959

960+
/// <summary>
961+
/// Gets the attribute definition, which defines if a player has explosion effect applied.
962+
/// </summary>
963+
/// <remarks>This can be caused by Explosion (book of samut) and Requiem (book of neil) skills.</remarks>
964+
public static AttributeDefinition IsBleeding { get; } = new(new Guid("BD5C685D-C360-4CC5-A43E-46644AD61F09"), "Is bleeding", "The player is damaged every second for a while.");
965+
960966
/// <summary>
961967
/// Gets the ice resistance attribute definition. Value range from 0 to 1.
962968
/// </summary>
@@ -1057,6 +1063,11 @@ public class Stats
10571063
/// </summary>
10581064
public static AttributeDefinition PoisonDamageMultiplier { get; } = new(new Guid("8581CD4D-C6AE-4C35-9147-9642DE7CC013"), "Poison Damage Multiplier", string.Empty);
10591065

1066+
/// <summary>
1067+
/// Gets the bleeding damage multiplier attribute definition.
1068+
/// </summary>
1069+
public static AttributeDefinition BleedingDamageMultiplier { get; } = new(new Guid("12C20F28-F219-4044-899D-E9277D251515"), "Bleeding Damage Multiplier", string.Empty);
1070+
10601071
/// <summary>
10611072
/// Gets the mana recovery absolute attribute definition.
10621073
/// </summary>
@@ -1118,6 +1129,16 @@ public class Stats
11181129
/// </summary>
11191130
public static AttributeDefinition DoubleDamageChance { get; } = new(new Guid("2B8A03E6-1CC2-48A0-8633-3F36E17050F4"), "Double Damage Chance", string.Empty);
11201131

1132+
/// <summary>
1133+
/// Gets the stun chance attribute definition.
1134+
/// </summary>
1135+
public static AttributeDefinition StunChance { get; } = new(new Guid("610D3259-1158-424A-8738-9EB7A71DE600"), "Stun Chance", string.Empty);
1136+
1137+
/// <summary>
1138+
/// Gets the pollution skill MST target move chance, which rises with lightning tome mastery.
1139+
/// </summary>
1140+
public static AttributeDefinition PollutionMoveTargetChance { get; } = new(new Guid("6F9619FF-8B86-D011-B42D-00C04FC964FF"), "Pollution Move Target Chance (MST)", "The pollution skill (book of lagle) move chance, which rises with lightning tome mastery.");
1141+
11211142
/// <summary>
11221143
/// Gets the mana after monster kill attribute definition.
11231144
/// </summary>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// <copyright file="BleedingMagicEffect.cs" company="MUnique">
2+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
3+
// </copyright>
4+
5+
namespace MUnique.OpenMU.GameLogic;
6+
7+
using System.Timers;
8+
using MUnique.OpenMU.AttributeSystem;
9+
using MUnique.OpenMU.GameLogic.Attributes;
10+
11+
/// <summary>
12+
/// The magic effect for bleeding, which will damage the character every second until the effect ends.
13+
/// </summary>
14+
public sealed class BleedingMagicEffect : MagicEffect
15+
{
16+
private const int ExplosionMagicEffectNumber = 75; // 0x4B
17+
private const int RequiemMagicEffectNumber = 74; // 0x4A
18+
private readonly Timer _damageTimer;
19+
private readonly float _damage;
20+
private readonly float _multiplier;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="BleedingMagicEffect"/> class.
24+
/// </summary>
25+
/// <param name="powerUp">The power up.</param>
26+
/// <param name="definition">The definition.</param>
27+
/// <param name="duration">The duration.</param>
28+
/// <param name="attacker">The attacker.</param>
29+
/// <param name="owner">The owner.</param>
30+
/// <param name="damage">The original damage.</param>
31+
public BleedingMagicEffect(IElement powerUp, MagicEffectDefinition definition, TimeSpan duration, IAttacker attacker, IAttackable owner, float damage)
32+
: base(powerUp, definition, duration)
33+
{
34+
this.Attacker = attacker;
35+
this.Owner = owner;
36+
this._damage = damage;
37+
this._multiplier = definition.Number switch
38+
{
39+
ExplosionMagicEffectNumber => attacker.Attributes[Stats.BleedingDamageMultiplier],
40+
RequiemMagicEffectNumber => 0.6f,
41+
_ => 1f,
42+
};
43+
this._damageTimer = new Timer(1000);
44+
this._damageTimer.Elapsed += this.OnDamageTimerElapsed;
45+
this._damageTimer.Start();
46+
}
47+
48+
/// <summary>
49+
/// Gets the owner of the effect.
50+
/// </summary>
51+
public IAttackable Owner { get; }
52+
53+
/// <summary>
54+
/// Gets the attacker which applied the effect.
55+
/// </summary>
56+
public IAttacker Attacker { get; }
57+
58+
/// <inheritdoc />
59+
protected override void Dispose(bool disposing)
60+
{
61+
base.Dispose(disposing);
62+
this._damageTimer.Stop();
63+
this._damageTimer.Dispose();
64+
}
65+
66+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
67+
private async void OnDamageTimerElapsed(object? sender, ElapsedEventArgs e)
68+
{
69+
try
70+
{
71+
if (!this.Owner.IsAlive || this.IsDisposed || this.IsDisposing || this._damage <= 0)
72+
{
73+
return;
74+
}
75+
76+
var damage = this._damage * this._multiplier;
77+
if (damage <= 0)
78+
{
79+
return;
80+
}
81+
82+
await this.Owner.ApplyBleedingDamageAsync(this.Attacker, (uint)damage).ConfigureAwait(false);
83+
}
84+
catch (Exception ex)
85+
{
86+
(this.Owner as ILoggerOwner)?.Logger.LogError(ex, "Error when applying bleeding damage");
87+
}
88+
}
89+
}

src/GameLogic/IAttackable.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ public interface IAttackable : IIdentifiable, ILocateable
7777
/// <param name="damage">The damage.</param>
7878
ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage);
7979

80+
/// <summary>
81+
/// Applies the bleeding damage.
82+
/// </summary>
83+
/// <param name="initialAttacker">The initial attacker.</param>
84+
/// <param name="damage">The damage.</param>
85+
ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage);
86+
8087
/// <summary>
8188
/// Kills the attackable instantly.
8289
/// </summary>

src/GameLogic/MagicEffectsList.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public async ValueTask ClearAllEffectsAsync()
9797
/// <summary>
9898
/// Clear the effects that produce a specific stat.
9999
/// </summary>
100-
/// <param name="stat">The stat produced by effect</param>
100+
/// <param name="stat">The stat produced by effect.</param>
101101
public async ValueTask ClearAllEffectsProducingSpecificStatAsync(AttributeDefinition stat)
102102
{
103103
var effects = this.ActiveEffects.Values.ToArray();

src/GameLogic/NPC/AttackableNpcBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ public int Health
141141
/// <inheritdoc />
142142
public abstract ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage);
143143

144+
/// <inheritdoc />
145+
public abstract ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage);
146+
144147
/// <inheritdoc/>
145148
public ValueTask KillInstantlyAsync()
146149
{

src/GameLogic/NPC/Destructible.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,11 @@ public override ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint
3939
// A destructible is not an organism which can be poisoned.
4040
return ValueTask.CompletedTask;
4141
}
42+
43+
/// <inheritdoc />
44+
public override ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
45+
{
46+
// A destructible is not an organism which can be exploded.
47+
return ValueTask.CompletedTask;
48+
}
4249
}

src/GameLogic/NPC/Monster.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map,
102102
/// <param name="target">The target.</param>
103103
public async ValueTask AttackAsync(IAttackable target)
104104
{
105-
await target.AttackByAsync(this, null, false).ConfigureAwait(false);
105+
var hitInfo = await target.AttackByAsync(this, null, false).ConfigureAwait(false);
106106

107107
await this.ForEachWorldObserverAsync<IShowAnimationPlugIn>(p => p.ShowMonsterAttackAnimationAsync(this, target, this.GetDirectionTo(target)), true).ConfigureAwait(false);
108108
if (this.Definition.AttackSkill is { } attackSkill)
109109
{
110-
await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget).ConfigureAwait(false);
110+
await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget, hitInfo).ConfigureAwait(false);
111111

112112
await this.ForEachWorldObserverAsync<IShowSkillAnimationPlugIn>(p => p.ShowSkillAnimationAsync(this, target, attackSkill, true), true).ConfigureAwait(false);
113113
}
@@ -226,6 +226,12 @@ public override ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint
226226
return this.HitAsync(new HitInfo(damage, 0, DamageAttributes.Poison), initialAttacker, null);
227227
}
228228

229+
/// <inheritdoc />
230+
public override ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
231+
{
232+
return this.HitAsync(new HitInfo(damage, 0, DamageAttributes.Undefined), initialAttacker, null);
233+
}
234+
229235
/// <inheritdoc/>
230236
public ValueTask MoveAsync(Point target)
231237
{

0 commit comments

Comments
 (0)