diff --git a/src/GameLogic/IAttackable.cs b/src/GameLogic/IAttackable.cs
index 79172c3a9..f3234b1c3 100644
--- a/src/GameLogic/IAttackable.cs
+++ b/src/GameLogic/IAttackable.cs
@@ -65,7 +65,8 @@ public interface IAttackable : IIdentifiable, ILocateable
/// The skill.
/// If set to true, the attacker did a combination of skills.
/// The damage factor.
- ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0);
+ /// Returns information about the damage inflicted.
+ ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0);
///
/// Reflects the damage which was done previously with or even to the .
diff --git a/src/GameLogic/NPC/AttackableNpcBase.cs b/src/GameLogic/NPC/AttackableNpcBase.cs
index ac9738b14..5dc20de6b 100644
--- a/src/GameLogic/NPC/AttackableNpcBase.cs
+++ b/src/GameLogic/NPC/AttackableNpcBase.cs
@@ -98,11 +98,11 @@ public int Health
|| (this.SpawnArea.SpawnTrigger == SpawnTrigger.AutomaticDuringWave && (this._eventStateProvider?.IsSpawnWaveActive(this.SpawnArea.WaveNumber) ?? false));
///
- public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
+ public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
{
if (this.Definition.ObjectKind == NpcObjectKind.Guard)
{
- return;
+ return null;
}
var hitInfo = await attacker.CalculateDamageAsync(this, skill, isCombo, damageFactor).ConfigureAwait(false);
@@ -120,6 +120,8 @@ public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool
await playerSurrogate.Owner.AfterHitTargetAsync().ConfigureAwait(false);
}
}
+
+ return hitInfo;
}
///
diff --git a/src/GameLogic/NPC/SoccerBall.cs b/src/GameLogic/NPC/SoccerBall.cs
index c017fa898..deb0af2cb 100644
--- a/src/GameLogic/NPC/SoccerBall.cs
+++ b/src/GameLogic/NPC/SoccerBall.cs
@@ -55,10 +55,11 @@ public SoccerBall(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap m
public DeathInformation? LastDeath => null;
///
- public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
+ public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
{
var direction = attacker.GetDirectionTo(this);
await this.MoveToDirectionAsync(direction, skill is { }).ConfigureAwait(false);
+ return null;
}
///
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index 0fb765f67..f5b7f83c5 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -588,7 +588,7 @@ public bool IsAnySelfDefenseActive()
}
///
- public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
+ public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0)
{
if (this.Attributes is null)
{
@@ -605,7 +605,7 @@ public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool
await observer.InvokeViewPlugInAsync(p => p.ShowHitAsync(this, hitInfo)).ConfigureAwait(false);
}
- return;
+ return hitInfo;
}
attacker.ApplyAmmunitionConsumption(hitInfo);
@@ -634,6 +634,8 @@ public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool
{
await attackerPlayer.AfterHitTargetAsync().ConfigureAwait(false);
}
+
+ return hitInfo;
}
///
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
index 5e5fedd6c..012e8cb0c 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
@@ -129,13 +129,13 @@ private async ValueTask ApplySkillAsync(Player player, SkillEntry skillEntry, IA
if (target.CheckSkillTargetRestrictions(player, skillEntry.Skill))
{
- await target.AttackByAsync(player, skillEntry, isCombo).ConfigureAwait(false);
+ var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo).ConfigureAwait(false);
await target.TryApplyElementalEffectsAsync(player, skillEntry).ConfigureAwait(false);
var baseSkill = skillEntry.GetBaseSkill();
if (player.GameContext.PlugInManager.GetStrategy(baseSkill.Number) is { } strategy)
{
- await strategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter).ConfigureAwait(false);
+ await strategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter, hitInfo).ConfigureAwait(false);
}
}
}
diff --git a/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs
index f4f6c8b6e..5f9891965 100644
--- a/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs
+++ b/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs
@@ -24,7 +24,7 @@ public class ChainLightningSkillPlugIn : IAreaSkillPlugIn
public short Key => 215;
///
- public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter)
+ public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
{
bool FilterTarget(IAttackable attackable)
{
diff --git a/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs
new file mode 100644
index 000000000..05dc28838
--- /dev/null
+++ b/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs
@@ -0,0 +1,38 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.GameLogic.PlugIns;
+using MUnique.OpenMU.GameLogic.Views.Character;
+using MUnique.OpenMU.Pathfinding;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles the drain life skill of the summoner class. Additionally to the attacked target, it regains life for damage dealt.
+///
+[PlugIn(nameof(ChainLightningSkillPlugIn), "Handles the drain life skill of the summoner class. Additionally to the attacked target, it regains life for damage dealt.")]
+[Guid("9A5A5671-3A8C-4C01-984F-1A8F8E0E7BDA")]
+public class DrainLifeSkillPlugIn : IAreaSkillPlugIn
+{
+ ///
+ public short Key => 214;
+
+ ///
+ public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
+ {
+ if (attacker is Player attackerPlayer && hitInfo != null && hitInfo.Value.HealthDamage > 0)
+ {
+ var playerAttributes = attackerPlayer.Attributes;
+
+ if (playerAttributes != null)
+ {
+ playerAttributes[Stats.CurrentHealth] = (uint)Math.Min(playerAttributes[Stats.MaximumHealth], playerAttributes[Stats.CurrentHealth] + hitInfo.Value.HealthDamage);
+ await attackerPlayer.InvokeViewPlugInAsync(p => p.UpdateCurrentHealthAsync()).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/src/GameLogic/PlayerActions/Skills/EarthShakeSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/EarthShakeSkillPlugIn.cs
index ebd1f8384..9a5b3eb5e 100644
--- a/src/GameLogic/PlayerActions/Skills/EarthShakeSkillPlugIn.cs
+++ b/src/GameLogic/PlayerActions/Skills/EarthShakeSkillPlugIn.cs
@@ -21,7 +21,7 @@ public class EarthShakeSkillPlugIn : IAreaSkillPlugIn
public short Key => 62;
///
- public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter)
+ public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
{
if (!target.IsAlive || target is not IMovable movableTarget || target.CurrentMap is not { } currentMap)
{
diff --git a/src/GameLogic/PlayerActions/Skills/PlasmaStormSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/PlasmaStormSkillPlugIn.cs
index 7cda02bed..7b2e07330 100644
--- a/src/GameLogic/PlayerActions/Skills/PlasmaStormSkillPlugIn.cs
+++ b/src/GameLogic/PlayerActions/Skills/PlasmaStormSkillPlugIn.cs
@@ -21,7 +21,7 @@ public class PlasmaStormSkillPlugIn : IAreaSkillPlugIn
public short Key => 76;
///
- public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter)
+ public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
{
if (target is Player targetPlayer
&& Rand.NextRandomBool(25)
diff --git a/src/GameLogic/PlugIns/IAreaSkillPlugIn.cs b/src/GameLogic/PlugIns/IAreaSkillPlugIn.cs
index 4d691be8b..5e0ff2f25 100644
--- a/src/GameLogic/PlugIns/IAreaSkillPlugIn.cs
+++ b/src/GameLogic/PlugIns/IAreaSkillPlugIn.cs
@@ -23,5 +23,6 @@ public interface IAreaSkillPlugIn : IStrategyPlugIn
/// The target.
/// The skill entry.
/// The target area center.
- ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter);
+ /// Hit info produced by the skill.
+ ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo);
}
\ No newline at end of file
diff --git a/src/Persistence/Initialization/Updates/FixDrainLifeSkillUpdate.cs b/src/Persistence/Initialization/Updates/FixDrainLifeSkillUpdate.cs
new file mode 100644
index 000000000..0733ef302
--- /dev/null
+++ b/src/Persistence/Initialization/Updates/FixDrainLifeSkillUpdate.cs
@@ -0,0 +1,55 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Persistence.Initialization.Updates;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.DataModel.Configuration.Items;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.Persistence.Initialization.Skills;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// This adds the items required to enter the kalima map.
+///
+[PlugIn(PlugInName, PlugInDescription)]
+[Guid("A8827A3C-7F52-47CF-9EA5-562A9C06B986")]
+public class FixDrainLifeSkillUpdate : UpdatePlugInBase
+{
+ ///
+ /// The plug in name.
+ ///
+ internal const string PlugInName = "Fix Drain Life Skill";
+
+ ///
+ /// The plug in description.
+ ///
+ internal const string PlugInDescription = "Updates the attributes of the summoner's Drain Life skill to make it work properly.";
+
+ ///
+ public override UpdateVersion Version => UpdateVersion.FixDrainLifeSkill;
+
+ ///
+ public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
+
+ ///
+ public override string Name => PlugInName;
+
+ ///
+ public override string Description => PlugInDescription;
+
+ ///
+ public override bool IsMandatory => false;
+
+ ///
+ public override DateTime CreatedAt => new(2024, 08, 29, 18, 0, 0, DateTimeKind.Utc);
+
+ ///
+ protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
+ {
+ var drainLife = gameConfiguration.Skills.First(x => x.Number == (short)SkillNumber.DrainLife);
+ drainLife.SkillType = SkillType.AreaSkillExplicitTarget;
+ }
+}
\ No newline at end of file
diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs
index 41757fadf..2e6a66b2a 100644
--- a/src/Persistence/Initialization/Updates/UpdateVersion.cs
+++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs
@@ -139,4 +139,9 @@ public enum UpdateVersion
/// The version of the .
///
FixAncientDiscriminators = 26,
+
+ ///
+ /// The version of the .
+ ///
+ FixDrainLifeSkill = 27,
}
\ No newline at end of file
diff --git a/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs
index 6135da14a..142cc96a9 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs
@@ -171,7 +171,7 @@ public override void Initialize()
this.CreateSkill(SkillNumber.SpellofRestriction, "Spell of Restriction", CharacterClasses.All, distance: 3, manaConsumption: 30, elementalModifier: ElementalType.Ice, cooldownMinutes: 5);
this.CreateSkill(SkillNumber.SpellofPursuit, "Spell of Pursuit", CharacterClasses.All, manaConsumption: 30, elementalModifier: ElementalType.Ice, cooldownMinutes: 10);
this.CreateSkill(SkillNumber.ShieldBurn, "Shield-Burn", CharacterClasses.All, distance: 3, manaConsumption: 30, elementalModifier: ElementalType.Ice, cooldownMinutes: 5);
- this.CreateSkill(SkillNumber.DrainLife, "Drain Life", CharacterClasses.AllSummoners, DamageType.Curse, 35, 6, manaConsumption: 50, energyRequirement: 150);
+ this.CreateSkill(SkillNumber.DrainLife, "Drain Life", CharacterClasses.AllSummoners, DamageType.Curse, 35, 6, manaConsumption: 50, energyRequirement: 150, skillType: SkillType.AreaSkillExplicitTarget);
this.CreateSkill(SkillNumber.ChainLightning, "Chain Lightning", CharacterClasses.AllSummoners, DamageType.Curse, 70, 6, manaConsumption: 85, energyRequirement: 245, skillType: SkillType.AreaSkillExplicitTarget, skillTarget: SkillTarget.Explicit);
this.CreateSkill(SkillNumber.DamageReflection, "Damage Reflection", CharacterClasses.AllSummoners, distance: 5, abilityConsumption: 10, manaConsumption: 40, energyRequirement: 375);
this.CreateSkill(SkillNumber.Berserker, "Berserker", CharacterClasses.AllSummoners, DamageType.Curse, distance: 5, abilityConsumption: 50, manaConsumption: 100, energyRequirement: 620);