diff --git a/src/DataModel/Configuration/AreaSkillSettings.cs b/src/DataModel/Configuration/AreaSkillSettings.cs
index 726019f17..12675b1d8 100644
--- a/src/DataModel/Configuration/AreaSkillSettings.cs
+++ b/src/DataModel/Configuration/AreaSkillSettings.cs
@@ -68,6 +68,11 @@ public partial class AreaSkillSettings
///
public int MaximumNumberOfHitsPerTarget { get; set; }
+ ///
+ /// Gets or sets the minimum number of hits per attack, after which subsequent hits have a reduced chance to hit.
+ ///
+ public int MinimumNumberOfHitsPerAttack { get; set; }
+
///
/// Gets or sets the maximum number of hits per attack.
///
@@ -76,7 +81,7 @@ public partial class AreaSkillSettings
///
/// Gets or sets the hit chance per distance multiplier.
/// E.g. when set to 0.9 and the target is 5 steps away,
- /// the chance to hit is 5^0.9 = 0.59.
+ /// the chance to hit is 0.9^5 = 0.59.
///
public float HitChancePerDistanceMultiplier { get; set; }
@@ -88,6 +93,11 @@ public partial class AreaSkillSettings
///
public int ProjectileCount { get; set; }
+ ///
+ /// Gets or sets the effect range of the skill, which is the maximum distance from the target area center.
+ ///
+ public int EffectRange { get; set; }
+
///
public override string ToString()
{
diff --git a/src/DataModel/Configuration/Skill.cs b/src/DataModel/Configuration/Skill.cs
index af9053749..70653575c 100644
--- a/src/DataModel/Configuration/Skill.cs
+++ b/src/DataModel/Configuration/Skill.cs
@@ -286,6 +286,17 @@ public partial class Skill
///
public virtual AttributeDefinition? ElementalModifierTarget { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the elemental modifier resistance should be ignored, which means the skill uses a specific logic.
+ ///
+ ///
+ /// Not all skills have magic effects corresponding to their element,
+ /// e.g. Pollution (book of lagle, lightning) has a 100% chance iceing effect.
+ /// Other skills have magic effects which may or may not be related to their element, but which apply regardless of resistance,
+ /// e.g. Explosion (book of samut, fire) and Requiem (book of neil, wind) have 100% bleeding effects.
+ ///
+ public bool SkipElementalModifier { get; set; }
+
///
/// Gets or sets the magic effect definition. It will be applied for buff skills.
///
diff --git a/src/GameLogic/AttackableExtensions.cs b/src/GameLogic/AttackableExtensions.cs
index abba95f01..9bcfc14bc 100644
--- a/src/GameLogic/AttackableExtensions.cs
+++ b/src/GameLogic/AttackableExtensions.cs
@@ -19,6 +19,8 @@ namespace MUnique.OpenMU.GameLogic;
///
public static class AttackableExtensions
{
+ private const short ExplosionMagicEffectNumber = 75; // 0x4B
+
private static readonly IDictionary ReductionModifiers =
new Dictionary
{
@@ -314,7 +316,8 @@ public static HitInfo GetHitInfo(this IAttackable defender, uint damage, DamageA
/// The target.
/// The attacker.
/// The skill entry.
- public static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry)
+ /// 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)
{
@@ -329,7 +332,7 @@ public static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IAt
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, powerUps).ConfigureAwait(false);
+ await target.ApplyMagicEffectAsync(attacker, skillEntry.Skill!.MagicEffectDef!, duration, hitInfo, powerUps).ConfigureAwait(false);
}
///
@@ -373,8 +376,9 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl
/// 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)
+ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, SkillEntry skillEntry, HitInfo? hitInfo = null)
{
if (!target.IsAlive)
{
@@ -383,15 +387,15 @@ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackab
skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));
var modifier = skillEntry.Skill.ElementalModifierTarget;
- if (modifier is null)
- {
- return false;
- }
+ var skipModifier = skillEntry.Skill.SkipElementalModifier;
- var resistance = target.Attributes[modifier];
- if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1)))
+ if (modifier is not null && !skipModifier)
{
- return false;
+ var resistance = target.Attributes[modifier];
+ if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1)))
+ {
+ return false;
+ }
}
var applied = false;
@@ -400,11 +404,11 @@ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackab
&& !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).ConfigureAwait(false);
+ await target.ApplyMagicEffectAsync(attacker, skillEntry, hitInfo).ConfigureAwait(false);
applied = true;
}
- if (modifier == Stats.LightningResistance)
+ if (modifier == Stats.LightningResistance && !skipModifier)
{
await target.MoveRandomlyAsync().ConfigureAwait(false);
applied = true;
@@ -422,10 +426,9 @@ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackab
/// The power up.
/// The duration.
/// The target attribute.
- ///
- /// The success of the appliance.
- ///
- public static async ValueTask TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IElement? powerUp, IElement? duration, AttributeDefinition? targetAttribute)
+ /// The hit information.
+ /// The success of the appliance.
+ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IElement? powerUp, IElement? duration, AttributeDefinition? targetAttribute, HitInfo? hitInfo)
{
if (!target.IsAlive)
{
@@ -453,7 +456,7 @@ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackab
&& targetAttribute is not null)
{
// power-up is the wrong term here... it's more like a power-down ;-)
- await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, (targetAttribute, powerUp)).ConfigureAwait(false);
+ await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, (targetAttribute, powerUp)).ConfigureAwait(false);
applied = true;
}
@@ -819,8 +822,9 @@ private static void GetBaseDmg(this IAttacker attacker, SkillEntry? skill, out i
/// 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, params (AttributeDefinition Target, IElement Boost)[] powerUps)
+ 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;
@@ -839,10 +843,26 @@ private static async ValueTask ApplyMagicEffectAsync(this IAttackable target, IA
return;
}
- var isPoisonEffect = magicEffectDefinition.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsPoisoned);
- var magicEffect = isPoisonEffect
- ? new PoisonMagicEffect(powerUps[0].Boost, magicEffectDefinition, durationSpan, attacker, target)
- : new MagicEffect(durationSpan, magicEffectDefinition, powerUps.Select(p => new MagicEffect.ElementWithTarget(p.Boost, p.Target)).ToArray());
+ 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
+ {
+ magicEffect = new MagicEffect(durationSpan, magicEffectDefinition, powerUps.Select(p => new MagicEffect.ElementWithTarget(p.Boost, p.Target)).ToArray());
+ }
await target.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
if (target is ISupportWalk walkSupporter
diff --git a/src/GameLogic/Attributes/Stats.cs b/src/GameLogic/Attributes/Stats.cs
index a1a541dbc..de92cd28a 100644
--- a/src/GameLogic/Attributes/Stats.cs
+++ b/src/GameLogic/Attributes/Stats.cs
@@ -957,6 +957,12 @@ public class Stats
///
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.");
+ ///
+ /// Gets the attribute definition, which defines if a player has explosion effect applied.
+ ///
+ /// This can be caused by Explosion (book of samut) and Requiem (book of neil) skills.
+ public static AttributeDefinition IsBleeding { get; } = new(new Guid("BD5C685D-C360-4CC5-A43E-46644AD61F09"), "Is bleeding", "The player is damaged every second for a while.");
+
///
/// Gets the ice resistance attribute definition. Value range from 0 to 1.
///
@@ -1057,6 +1063,11 @@ public class Stats
///
public static AttributeDefinition PoisonDamageMultiplier { get; } = new(new Guid("8581CD4D-C6AE-4C35-9147-9642DE7CC013"), "Poison Damage Multiplier", string.Empty);
+ ///
+ /// Gets the bleeding damage multiplier attribute definition.
+ ///
+ public static AttributeDefinition BleedingDamageMultiplier { get; } = new(new Guid("12C20F28-F219-4044-899D-E9277D251515"), "Bleeding Damage Multiplier", string.Empty);
+
///
/// Gets the mana recovery absolute attribute definition.
///
@@ -1118,6 +1129,16 @@ public class Stats
///
public static AttributeDefinition DoubleDamageChance { get; } = new(new Guid("2B8A03E6-1CC2-48A0-8633-3F36E17050F4"), "Double Damage Chance", string.Empty);
+ ///
+ /// Gets the stun chance attribute definition.
+ ///
+ public static AttributeDefinition StunChance { get; } = new(new Guid("610D3259-1158-424A-8738-9EB7A71DE600"), "Stun Chance", string.Empty);
+
+ ///
+ /// Gets the pollution skill MST target move chance, which rises with lightning tome mastery.
+ ///
+ 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.");
+
///
/// Gets the mana after monster kill attribute definition.
///
diff --git a/src/GameLogic/BleedingMagicEffect.cs b/src/GameLogic/BleedingMagicEffect.cs
new file mode 100644
index 000000000..71e5424e6
--- /dev/null
+++ b/src/GameLogic/BleedingMagicEffect.cs
@@ -0,0 +1,73 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic;
+
+using System.Timers;
+using MUnique.OpenMU.AttributeSystem;
+
+///
+/// The magic effect for bleeding, which will damage the character every second until the effect ends.
+///
+public sealed class BleedingMagicEffect : MagicEffect
+{
+ private readonly Timer _damageTimer;
+ private readonly float _damage;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The power up.
+ /// The definition.
+ /// The duration.
+ /// The attacker.
+ /// The owner.
+ /// The bleeding damage.
+ public BleedingMagicEffect(IElement powerUp, MagicEffectDefinition definition, TimeSpan duration, IAttacker attacker, IAttackable owner, float damage)
+ : base(powerUp, definition, duration)
+ {
+ this.Attacker = attacker;
+ this.Owner = owner;
+ this._damage = damage;
+ this._damageTimer = new Timer(1000);
+ this._damageTimer.Elapsed += this.OnDamageTimerElapsed;
+ this._damageTimer.Start();
+ }
+
+ ///
+ /// Gets the owner of the effect.
+ ///
+ public IAttackable Owner { get; }
+
+ ///
+ /// Gets the attacker which applied the effect.
+ ///
+ public IAttacker Attacker { get; }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ this._damageTimer.Stop();
+ this._damageTimer.Dispose();
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
+ private async void OnDamageTimerElapsed(object? sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ if (!this.Owner.IsAlive || this.IsDisposed || this.IsDisposing || this._damage <= 0)
+ {
+ return;
+ }
+
+ await this.Owner.ApplyBleedingDamageAsync(this.Attacker, (uint)this._damage).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ (this.Owner as ILoggerOwner)?.Logger.LogError(ex, "Error when applying bleeding damage");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameLogic/IAttackable.cs b/src/GameLogic/IAttackable.cs
index 0d4a781f7..1c8e454a3 100644
--- a/src/GameLogic/IAttackable.cs
+++ b/src/GameLogic/IAttackable.cs
@@ -77,6 +77,13 @@ public interface IAttackable : IIdentifiable, ILocateable
/// The damage.
ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage);
+ ///
+ /// Applies the bleeding damage.
+ ///
+ /// The initial attacker.
+ /// The damage.
+ ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage);
+
///
/// Kills the attackable instantly.
///
diff --git a/src/GameLogic/MagicEffectsList.cs b/src/GameLogic/MagicEffectsList.cs
index 54ad80a60..2e8d888af 100644
--- a/src/GameLogic/MagicEffectsList.cs
+++ b/src/GameLogic/MagicEffectsList.cs
@@ -97,7 +97,7 @@ public async ValueTask ClearAllEffectsAsync()
///
/// Clear the effects that produce a specific stat.
///
- /// The stat produced by effect
+ /// The stat produced by effect.
public async ValueTask ClearAllEffectsProducingSpecificStatAsync(AttributeDefinition stat)
{
var effects = this.ActiveEffects.Values.ToArray();
diff --git a/src/GameLogic/NPC/AttackableNpcBase.cs b/src/GameLogic/NPC/AttackableNpcBase.cs
index da2a70fb6..847bfefc2 100644
--- a/src/GameLogic/NPC/AttackableNpcBase.cs
+++ b/src/GameLogic/NPC/AttackableNpcBase.cs
@@ -141,6 +141,9 @@ public int Health
///
public abstract ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage);
+ ///
+ public abstract ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage);
+
///
public ValueTask KillInstantlyAsync()
{
diff --git a/src/GameLogic/NPC/Destructible.cs b/src/GameLogic/NPC/Destructible.cs
index 4af69935a..11bcd7595 100644
--- a/src/GameLogic/NPC/Destructible.cs
+++ b/src/GameLogic/NPC/Destructible.cs
@@ -39,4 +39,11 @@ public override ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint
// A destructible is not an organism which can be poisoned.
return ValueTask.CompletedTask;
}
+
+ ///
+ public override ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
+ {
+ // A destructible is not an organism which can bleed.
+ return ValueTask.CompletedTask;
+ }
}
\ No newline at end of file
diff --git a/src/GameLogic/NPC/Monster.cs b/src/GameLogic/NPC/Monster.cs
index 29f33c9d2..03a439719 100644
--- a/src/GameLogic/NPC/Monster.cs
+++ b/src/GameLogic/NPC/Monster.cs
@@ -102,12 +102,12 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map,
/// The target.
public async ValueTask AttackAsync(IAttackable target)
{
- await target.AttackByAsync(this, null, false).ConfigureAwait(false);
+ var hitInfo = await target.AttackByAsync(this, null, false).ConfigureAwait(false);
await this.ForEachWorldObserverAsync(p => p.ShowMonsterAttackAnimationAsync(this, target, this.GetDirectionTo(target)), true).ConfigureAwait(false);
if (this.Definition.AttackSkill is { } attackSkill)
{
- await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget).ConfigureAwait(false);
+ await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget, hitInfo).ConfigureAwait(false);
await this.ForEachWorldObserverAsync(p => p.ShowSkillAnimationAsync(this, target, attackSkill, true), true).ConfigureAwait(false);
}
@@ -226,6 +226,12 @@ public override ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint
return this.HitAsync(new HitInfo(damage, 0, DamageAttributes.Poison), initialAttacker, null);
}
+ ///
+ public override ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
+ {
+ return this.HitAsync(new HitInfo(damage, 0, DamageAttributes.Undefined), initialAttacker, null);
+ }
+
///
public ValueTask MoveAsync(Point target)
{
diff --git a/src/GameLogic/NPC/SoccerBall.cs b/src/GameLogic/NPC/SoccerBall.cs
index c207b912b..9fa2264c9 100644
--- a/src/GameLogic/NPC/SoccerBall.cs
+++ b/src/GameLogic/NPC/SoccerBall.cs
@@ -76,6 +76,13 @@ public ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage)
return ValueTask.CompletedTask;
}
+ ///
+ public ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
+ {
+ // A ball doesn't take any damage
+ return ValueTask.CompletedTask;
+ }
+
///
public ValueTask KillInstantlyAsync()
{
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index ad425c8b8..3d85e9f75 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -760,6 +760,12 @@ public ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage)
return this.HitAsync(new HitInfo(damage, 0, DamageAttributes.Poison), initialAttacker, null);
}
+ ///
+ public ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage)
+ {
+ return this.HitAsync(this.GetHitInfo(damage, DamageAttributes.Undefined, initialAttacker), initialAttacker, null);
+ }
+
///
/// Teleports this player to the specified target with the specified skill animation.
///
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
index 068cf936e..f5a36b6b7 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
@@ -113,8 +113,9 @@ private static IEnumerable GetTargets(Player player, Point targetAr
private static IEnumerable GetTargetsInRange(Player player, Point targetAreaCenter, Skill skill, byte rotation)
{
+ var range = skill.AreaSkillSettings?.EffectRange > 0 ? skill.AreaSkillSettings.EffectRange : skill.Range;
var targetsInRange = player.CurrentMap?
- .GetAttackablesInRange(targetAreaCenter, skill.Range)
+ .GetAttackablesInRange(targetAreaCenter, range)
.Where(a => a != player)
.Where(a => !a.IsAtSafezone())
?? [];
@@ -217,6 +218,7 @@ private async ValueTask PerformAutomaticHitsAsync(Player player, ushort extraTar
IAttackable? extraTarget = null;
var attackCount = 0;
var maxAttacks = areaSkillSettings.MaximumNumberOfHitsPerAttack == 0 ? int.MaxValue : areaSkillSettings.MaximumNumberOfHitsPerAttack;
+ var minAttacks = areaSkillSettings.MinimumNumberOfHitsPerAttack == 0 ? maxAttacks : areaSkillSettings.MinimumNumberOfHitsPerAttack;
var currentDelay = TimeSpan.Zero;
// Order targets by distance to process nearest targets first
@@ -281,9 +283,20 @@ private async ValueTask PerformAutomaticHitsAsync(Player player, ushort extraTar
continue; // This projectile cannot hit this target
}
- var hitChance = attackRound < areaSkillSettings.MinimumNumberOfHitsPerTarget
- ? 1.0
- : Math.Min(areaSkillSettings.HitChancePerDistanceMultiplier, Math.Pow(areaSkillSettings.HitChancePerDistanceMultiplier, player.GetDistanceTo(target)));
+ double hitChance;
+ if (attackRound >= areaSkillSettings.MinimumNumberOfHitsPerTarget)
+ {
+ hitChance = Math.Pow(areaSkillSettings.HitChancePerDistanceMultiplier, player.GetDistanceTo(target));
+ }
+ else if (attackCount >= minAttacks)
+ {
+ hitChance = 0.5;
+ }
+ else
+ {
+ hitChance = 1.0;
+ }
+
if (hitChance < 1.0 && !Rand.NextRandomBool(hitChance))
{
continue;
@@ -335,7 +348,7 @@ private async ValueTask ApplySkillAsync(Player player, SkillEntry skillEntry, IA
}
var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo, 1, skill.NumberOfHitsPerAttack > 1 ? false : null).ConfigureAwait(false);
- await target.TryApplyElementalEffectsAsync(player, skillEntry).ConfigureAwait(false);
+ await target.TryApplyElementalEffectsAsync(player, skillEntry, hitInfo).ConfigureAwait(false);
for (int hit = 2; hit <= skill.NumberOfHitsPerAttack; hit++)
{
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs
index e0483071a..1c7d31a21 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs
@@ -33,8 +33,8 @@ public async ValueTask AttackTargetAsync(Player player, IAttackable target, Skil
if (target.CheckSkillTargetRestrictions(player, skill.Skill))
{
- await target.AttackByAsync(player, skill, false).ConfigureAwait(false);
- await target.TryApplyElementalEffectsAsync(player, skill).ConfigureAwait(false);
+ var hitInfo = await target.AttackByAsync(player, skill, false).ConfigureAwait(false);
+ await target.TryApplyElementalEffectsAsync(player, skill, hitInfo).ConfigureAwait(false);
}
}
}
\ No newline at end of file
diff --git a/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs
index 359819af8..c865d0aa6 100644
--- a/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs
+++ b/src/GameLogic/PlayerActions/Skills/ChainLightningSkillPlugIn.cs
@@ -78,14 +78,14 @@ bool FilterTarget(IAttackable attackable)
await Task.Delay(300).ConfigureAwait(false);
// first attack 70 %
- await secondTarget.AttackByAsync(attacker, skillEntry, false, 0.7).ConfigureAwait(false);
- await secondTarget.TryApplyElementalEffectsAsync(attacker, skillEntry).ConfigureAwait(false);
+ var hit2Info = await secondTarget.AttackByAsync(attacker, skillEntry, false, 0.7).ConfigureAwait(false);
+ await secondTarget.TryApplyElementalEffectsAsync(attacker, skillEntry, hit2Info).ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
// second attack 50%
- await thirdTarget.AttackByAsync(attacker, skillEntry, false, 0.5).ConfigureAwait(false);
- await thirdTarget.TryApplyElementalEffectsAsync(attacker, skillEntry).ConfigureAwait(false);
+ var hit3Info = await thirdTarget.AttackByAsync(attacker, skillEntry, false, 0.5).ConfigureAwait(false);
+ await thirdTarget.TryApplyElementalEffectsAsync(attacker, skillEntry, hit3Info).ConfigureAwait(false);
});
}
}
\ No newline at end of file
diff --git a/src/GameLogic/PlayerActions/Skills/PollutionSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/PollutionSkillPlugIn.cs
new file mode 100644
index 000000000..89331648f
--- /dev/null
+++ b/src/GameLogic/PlayerActions/Skills/PollutionSkillPlugIn.cs
@@ -0,0 +1,59 @@
+//
+// 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.NPC;
+using MUnique.OpenMU.GameLogic.PlugIns;
+using MUnique.OpenMU.Pathfinding;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles the pollution skill (book of lagle) of the summoner class. Based on a chance, it may push the targets 2 squares away from the attacker.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.PollutionSkillPlugIn_Name), Description = nameof(PlugInResources.PollutionSkillPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("A2B3C4D5-E6F7-4812-3456-7890ABCDEF12")]
+public class PollutionSkillPlugIn : IAreaSkillPlugIn
+{
+ ///
+ public short Key => 225;
+
+ ///
+ 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
+ || !Rand.NextRandomBool(Convert.ToDouble(attacker.Attributes[Stats.PollutionMoveTargetChance])))
+ {
+ return;
+ }
+
+ var startingPoint = attacker.Position;
+ var currentTarget = target.Position;
+ var direction = startingPoint.GetDirectionTo(currentTarget);
+ if (direction == Direction.Undefined)
+ {
+ direction = (Direction)Rand.NextInt(1, 9);
+ }
+
+ for (int i = 0; i < 2; i++)
+ {
+ var nextTarget = currentTarget.CalculateTargetPoint(direction);
+ if (!currentMap.Terrain.WalkMap[nextTarget.X, nextTarget.Y]
+ || (target is NonPlayerCharacter && target.CurrentMap.Terrain.SafezoneMap[nextTarget.X, nextTarget.Y]))
+ {
+ // we don't want to push the target into a non-reachable area, through walls or monsters into the safe zone.
+ break;
+ }
+
+ currentTarget = nextTarget;
+ }
+
+ await movableTarget.MoveAsync(currentTarget).ConfigureAwait(false);
+ }
+}
\ No newline at end of file
diff --git a/src/GameLogic/PlayerActions/Skills/RequiemSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/RequiemSkillPlugIn.cs
new file mode 100644
index 000000000..a3ec49ab9
--- /dev/null
+++ b/src/GameLogic/PlayerActions/Skills/RequiemSkillPlugIn.cs
@@ -0,0 +1,55 @@
+//
+// 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.World;
+using MUnique.OpenMU.Pathfinding;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles the requiem skill (book of neil) of the summoner class. Based on a chance, it may stun the target.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.RequiemSkillPlugIn_Name), Description = nameof(PlugInResources.RequiemSkillPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("A1B2C3D4-E5F6-7890-ABCD-EF1234567890")]
+public class RequiemSkillPlugIn : IAreaSkillPlugIn
+{
+ private const int StunnedMagicEffectNumber = 61; // 0x3D
+
+ private MagicEffectDefinition? _stunEffectDefinition;
+
+ ///
+ public short Key => 224;
+
+ ///
+ public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
+ {
+ this._stunEffectDefinition ??= ((Player)attacker).GameContext.Configuration.MagicEffects.First(m => m.Number == StunnedMagicEffectNumber);
+
+ if (!target.IsAlive || !Rand.NextRandomBool(Convert.ToDouble(attacker.Attributes[Stats.StunChance])))
+ {
+ return;
+ }
+
+ var powerUp = attacker.Attributes.CreateElement(this._stunEffectDefinition.PowerUpDefinitions.First());
+ var magicEffect = new MagicEffect(powerUp, this._stunEffectDefinition, TimeSpan.FromSeconds(3));
+ await target.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
+
+ if (target is ISupportWalk walkSupporter && walkSupporter.IsWalking)
+ {
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index a725b3b98..b020ed3ce 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -263,9 +263,9 @@ private async ValueTask ApplySkillAsync(Player player, IAttackable targete
if (!target.IsAtSafezone() && !player.IsAtSafezone() && target != player)
{
- await target.AttackByAsync(player, skillEntry, isCombo, 1, skill.NumberOfHitsPerAttack > 1 ? false : null).ConfigureAwait(false);
+ var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo, 1, skill.NumberOfHitsPerAttack > 1 ? false : null).ConfigureAwait(false);
player.LastAttackedTarget.SetTarget(target);
- success = await target.TryApplyElementalEffectsAsync(player, skillEntry).ConfigureAwait(false) || success;
+ success = await target.TryApplyElementalEffectsAsync(player, skillEntry, hitInfo).ConfigureAwait(false) || success;
for (int hit = 2; hit <= skill.NumberOfHitsPerAttack; hit++)
{
diff --git a/src/GameLogic/PoisonMagicEffect.cs b/src/GameLogic/PoisonMagicEffect.cs
index 2284e84b3..8834a48d2 100644
--- a/src/GameLogic/PoisonMagicEffect.cs
+++ b/src/GameLogic/PoisonMagicEffect.cs
@@ -28,7 +28,7 @@ public PoisonMagicEffect(IElement powerUp, MagicEffectDefinition definition, Tim
{
this.Attacker = attacker;
this.Owner = owner;
- this._damageTimer = new System.Timers.Timer(3000);
+ this._damageTimer = new Timer(3000);
this._damageTimer.Elapsed += this.OnDamageTimerElapsed;
this._damageTimer.Start();
}
@@ -71,7 +71,7 @@ private async void OnDamageTimerElapsed(object? sender, ElapsedEventArgs e)
}
catch (Exception ex)
{
- (this.Owner as ILoggerOwner)?.Logger.LogError(ex, "Error when applying posion damage");
+ (this.Owner as ILoggerOwner)?.Logger.LogError(ex, "Error when applying poison damage");
}
}
}
\ No newline at end of file
diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs
index 258a76f79..b7f5be489 100644
--- a/src/GameLogic/Properties/PlugInResources.Designer.cs
+++ b/src/GameLogic/Properties/PlugInResources.Designer.cs
@@ -1842,6 +1842,24 @@ public static string PlayerLosesMoneyAfterDeathPlugInConfiguration_VaultMoneyLos
}
}
+ ///
+ /// Looks up a localized string similar to Handles the pollution skill (book of lagle) of the summoner class. Based on a chance, it may push the targets 2 squares away from the attacker..
+ ///
+ public static string PollutionSkillPlugIn_Description {
+ get {
+ return ResourceManager.GetString("PollutionSkillPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Pollution Skill.
+ ///
+ public static string PollutionSkillPlugIn_Name {
+ get {
+ return ResourceManager.GetString("PollutionSkillPlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Handles the chat command '/post message'. Sends a global blue system message to all players of the game..
///
@@ -2015,6 +2033,24 @@ public static string RemoveNpcChatCommand_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Handles the requiem skill (book of neil) of the summoner class. Based on a chance, it may stun the target..
+ ///
+ public static string RequiemSkillPlugIn_Description {
+ get {
+ return ResourceManager.GetString("RequiemSkillPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Requiem Skill.
+ ///
+ public static string RequiemSkillPlugIn_Name {
+ get {
+ return ResourceManager.GetString("RequiemSkillPlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Handle Reset Character NPC Request.
///
diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx
index 4e828642e..63a0c9e07 100644
--- a/src/GameLogic/Properties/PlugInResources.resx
+++ b/src/GameLogic/Properties/PlugInResources.resx
@@ -423,6 +423,18 @@
Handles the plasma storm skill of the fenrir pet. It randomly halves the durability of a target's equipped item.
+
+ Pollution Skill
+
+
+ Handles the pollution skill (book of lagle) of the summoner class. Based on a chance, it may push the targets 2 squares away from the attacker.
+
+
+ Requiem Skill
+
+
+ Handles the requiem skill (book of neil) of the summoner class. Based on a chance, it may stun the target.
+
SoulBarrierProficieSkillAction
diff --git a/src/GameServer/MessageHandler/AreaSkillAttackHandlerPlugIn.cs b/src/GameServer/MessageHandler/AreaSkillAttackHandlerPlugIn.cs
index 37c22e991..58c39fd21 100644
--- a/src/GameServer/MessageHandler/AreaSkillAttackHandlerPlugIn.cs
+++ b/src/GameServer/MessageHandler/AreaSkillAttackHandlerPlugIn.cs
@@ -22,6 +22,8 @@ namespace MUnique.OpenMU.GameServer.MessageHandler;
[MinimumClient(1, 0, ClientLanguage.Invariant)]
internal class AreaSkillAttackHandlerPlugIn : IPacketHandlerPlugIn
{
+ private const int PollutionSkillId = 225;
+
private readonly AreaSkillAttackAction _attackAction = new();
///
@@ -47,5 +49,21 @@ public async ValueTask HandlePacketAsync(Player player, Memory packet)
}
await this._attackAction.AttackAsync(player, message.ExtraTargetId, message.SkillId, new Point(message.TargetX, message.TargetY), message.Rotation).ConfigureAwait(false);
+
+ if (message.SkillId == PollutionSkillId)
+ {
+ var point = new Point(message.TargetX, message.TargetY);
+ var extraTargetId = message.ExtraTargetId;
+ var rotation = message.Rotation;
+
+ _ = Task.Run(async () =>
+ {
+ for (int i = 1; i <= 5; i++)
+ {
+ await Task.Delay(1000).ConfigureAwait(false);
+ await this._attackAction.AttackAsync(player, extraTargetId, PollutionSkillId, point, rotation).ConfigureAwait(false);
+ }
+ });
+ }
}
}
\ No newline at end of file
diff --git a/src/Persistence/EntityFramework/Migrations/20260324211344_ExtendAreaSkillSettings.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260324211344_ExtendAreaSkillSettings.Designer.cs
new file mode 100644
index 000000000..a1cd9187c
--- /dev/null
+++ b/src/Persistence/EntityFramework/Migrations/20260324211344_ExtendAreaSkillSettings.Designer.cs
@@ -0,0 +1,5229 @@
+//
+using System;
+using MUnique.OpenMU.Persistence.EntityFramework;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
+{
+ [DbContext(typeof(EntityDataContext))]
+ [Migration("20260324211344_ExtendAreaSkillSettings")]
+ partial class ExtendAreaSkillSettings
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatBanUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EMail")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsTemplate")
+ .HasColumnType("boolean");
+
+ b.Property("IsVaultExtended")
+ .HasColumnType("boolean");
+
+ b.Property("LanguageIsoCode")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(3)
+ .HasColumnType("character varying(3)")
+ .HasDefaultValue("en");
+
+ b.Property("LoginName")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("RegistrationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SecurityCode")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("State")
+ .HasColumnType("integer");
+
+ b.Property("TimeZone")
+ .HasColumnType("smallint");
+
+ b.Property("VaultId")
+ .HasColumnType("uuid");
+
+ b.Property("VaultPassword")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LoginName")
+ .IsUnique();
+
+ b.HasIndex("VaultId")
+ .IsUnique();
+
+ b.ToTable("Account", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b =>
+ {
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.HasKey("AccountId", "CharacterClassId");
+
+ b.HasIndex("CharacterClassId");
+
+ b.ToTable("AccountCharacterClass", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("FullAncientSetEquipped")
+ .HasColumnType("boolean");
+
+ b.Property("Pose")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CharacterClassId");
+
+ b.ToTable("AppearanceData", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("DelayBetweenHits")
+ .HasColumnType("interval");
+
+ b.Property("DelayPerOneDistance")
+ .HasColumnType("interval");
+
+ b.Property("EffectRange")
+ .HasColumnType("integer");
+
+ b.Property("FrustumDistance")
+ .HasColumnType("real");
+
+ b.Property("FrustumEndWidth")
+ .HasColumnType("real");
+
+ b.Property("FrustumStartWidth")
+ .HasColumnType("real");
+
+ b.Property("HitChancePerDistanceMultiplier")
+ .HasColumnType("real");
+
+ b.Property("MaximumNumberOfHitsPerAttack")
+ .HasColumnType("integer");
+
+ b.Property("MaximumNumberOfHitsPerTarget")
+ .HasColumnType("integer");
+
+ b.Property("MinimumNumberOfHitsPerAttack")
+ .HasColumnType("integer");
+
+ b.Property("MinimumNumberOfHitsPerTarget")
+ .HasColumnType("integer");
+
+ b.Property("ProjectileCount")
+ .HasColumnType("integer");
+
+ b.Property("TargetAreaDiameter")
+ .HasColumnType("real");
+
+ b.Property("UseDeferredHits")
+ .HasColumnType("boolean");
+
+ b.Property("UseFrustumFilter")
+ .HasColumnType("boolean");
+
+ b.Property("UseTargetAreaFilter")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("AreaSkillSettings", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Designation")
+ .HasColumnType("text");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("MaximumValue")
+ .HasColumnType("real");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameConfigurationId");
+
+ b.ToTable("AttributeDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AggregateType")
+ .HasColumnType("integer");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("InputAttributeId")
+ .HasColumnType("uuid");
+
+ b.Property("InputOperand")
+ .HasColumnType("real");
+
+ b.Property("InputOperator")
+ .HasColumnType("integer");
+
+ b.Property("OperandAttributeId")
+ .HasColumnType("uuid");
+
+ b.Property("PowerUpDefinitionValueId")
+ .HasColumnType("uuid");
+
+ b.Property("SkillId")
+ .HasColumnType("uuid");
+
+ b.Property("TargetAttributeId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CharacterClassId");
+
+ b.HasIndex("GameConfigurationId");
+
+ b.HasIndex("InputAttributeId");
+
+ b.HasIndex("OperandAttributeId");
+
+ b.HasIndex("PowerUpDefinitionValueId");
+
+ b.HasIndex("SkillId");
+
+ b.HasIndex("TargetAttributeId");
+
+ b.ToTable("AttributeRelationship", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AttributeId")
+ .HasColumnType("uuid");
+
+ b.Property("GameMapDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("ItemDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("MinimumValue")
+ .HasColumnType("integer");
+
+ b.Property("SkillId")
+ .HasColumnType("uuid");
+
+ b.Property("SkillId1")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AttributeId");
+
+ b.HasIndex("GameMapDefinitionId");
+
+ b.HasIndex("ItemDefinitionId");
+
+ b.HasIndex("SkillId");
+
+ b.HasIndex("SkillId1");
+
+ b.ToTable("AttributeRequirement", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("GroundId")
+ .HasColumnType("uuid");
+
+ b.Property("LeftGoalId")
+ .HasColumnType("uuid");
+
+ b.Property("LeftTeamSpawnPointX")
+ .HasColumnType("smallint");
+
+ b.Property("LeftTeamSpawnPointY")
+ .HasColumnType("smallint");
+
+ b.Property("RightGoalId")
+ .HasColumnType("uuid");
+
+ b.Property("RightTeamSpawnPointX")
+ .HasColumnType("smallint");
+
+ b.Property("RightTeamSpawnPointY")
+ .HasColumnType("smallint");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroundId")
+ .IsUnique();
+
+ b.HasIndex("LeftGoalId")
+ .IsUnique();
+
+ b.HasIndex("RightGoalId")
+ .IsUnique();
+
+ b.ToTable("BattleZoneDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterSlot")
+ .HasColumnType("smallint");
+
+ b.Property("CharacterStatus")
+ .HasColumnType("integer");
+
+ b.Property("CreateDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrentMapId")
+ .HasColumnType("uuid");
+
+ b.Property("Experience")
+ .HasColumnType("bigint");
+
+ b.Property("InventoryExtensions")
+ .HasColumnType("integer");
+
+ b.Property("InventoryId")
+ .HasColumnType("uuid");
+
+ b.Property("IsStoreOpened")
+ .HasColumnType("boolean");
+
+ b.Property("KeyConfiguration")
+ .HasColumnType("bytea");
+
+ b.Property("LevelUpPoints")
+ .HasColumnType("integer");
+
+ b.Property("MasterExperience")
+ .HasColumnType("bigint");
+
+ b.Property("MasterLevelUpPoints")
+ .HasColumnType("integer");
+
+ b.Property("MuHelperConfiguration")
+ .HasColumnType("bytea");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("PlayerKillCount")
+ .HasColumnType("integer");
+
+ b.Property("Pose")
+ .HasColumnType("smallint");
+
+ b.Property("PositionX")
+ .HasColumnType("smallint");
+
+ b.Property("PositionY")
+ .HasColumnType("smallint");
+
+ b.Property("State")
+ .HasColumnType("integer");
+
+ b.Property("StateRemainingSeconds")
+ .HasColumnType("integer");
+
+ b.Property("StoreName")
+ .HasColumnType("text");
+
+ b.Property("UsedFruitPoints")
+ .HasColumnType("integer");
+
+ b.Property("UsedNegFruitPoints")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("CharacterClassId");
+
+ b.HasIndex("CurrentMapId");
+
+ b.HasIndex("InventoryId")
+ .IsUnique();
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Character", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanGetCreated")
+ .HasColumnType("boolean");
+
+ b.Property("ComboDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("CreationAllowedFlag")
+ .HasColumnType("smallint");
+
+ b.Property("FruitCalculation")
+ .HasColumnType("integer");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("HomeMapId")
+ .HasColumnType("uuid");
+
+ b.Property("IsMasterClass")
+ .HasColumnType("boolean");
+
+ b.Property("LevelRequirementByCreation")
+ .HasColumnType("smallint");
+
+ b.Property("LevelWarpRequirementReductionPercent")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NextGenerationClassId")
+ .HasColumnType("uuid");
+
+ b.Property("Number")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ComboDefinitionId")
+ .IsUnique();
+
+ b.HasIndex("GameConfigurationId");
+
+ b.HasIndex("HomeMapId");
+
+ b.HasIndex("NextGenerationClassId");
+
+ b.ToTable("CharacterClass", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b =>
+ {
+ b.Property("CharacterId")
+ .HasColumnType("uuid");
+
+ b.Property("DropItemGroupId")
+ .HasColumnType("uuid");
+
+ b.HasKey("CharacterId", "DropItemGroupId");
+
+ b.HasIndex("DropItemGroupId");
+
+ b.ToTable("CharacterDropItemGroup", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ActiveQuestId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterId")
+ .HasColumnType("uuid");
+
+ b.Property("ClientActionPerformed")
+ .HasColumnType("boolean");
+
+ b.Property("Group")
+ .HasColumnType("smallint");
+
+ b.Property("LastFinishedQuestId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ActiveQuestId");
+
+ b.HasIndex("CharacterId");
+
+ b.HasIndex("LastFinishedQuestId");
+
+ b.ToTable("CharacterQuestState", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ClientCleanUpInterval")
+ .HasColumnType("interval");
+
+ b.Property("ClientTimeout")
+ .HasColumnType("interval");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("MaximumConnections")
+ .HasColumnType("integer");
+
+ b.Property("RoomCleanUpInterval")
+ .HasColumnType("interval");
+
+ b.Property("ServerId")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.ToTable("ChatServerDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatServerDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("ClientId")
+ .HasColumnType("uuid");
+
+ b.Property("NetworkPort")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatServerDefinitionId");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ChatServerEndpoint", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ItemOptionCombinationBonusId")
+ .HasColumnType("uuid");
+
+ b.Property("MinimumCount")
+ .HasColumnType("integer");
+
+ b.Property("OptionTypeId")
+ .HasColumnType("uuid");
+
+ b.Property("SubOptionType")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemOptionCombinationBonusId");
+
+ b.HasIndex("OptionTypeId");
+
+ b.ToTable("CombinationBonusRequirement", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InstalledAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("ConfigurationUpdate", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdateState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CurrentInstalledVersion")
+ .HasColumnType("integer");
+
+ b.Property("InitializationKey")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("ConfigurationUpdateState", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CheckMaxConnectionsPerAddress")
+ .HasColumnType("boolean");
+
+ b.Property("ClientId")
+ .HasColumnType("uuid");
+
+ b.Property("ClientListenerPort")
+ .HasColumnType("integer");
+
+ b.Property("CurrentPatchVersion")
+ .HasColumnType("bytea");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DisconnectOnUnknownPacket")
+ .HasColumnType("boolean");
+
+ b.Property("ListenerBacklog")
+ .HasColumnType("integer");
+
+ b.Property("MaxConnections")
+ .HasColumnType("integer");
+
+ b.Property("MaxConnectionsPerAddress")
+ .HasColumnType("integer");
+
+ b.Property("MaxFtpRequests")
+ .HasColumnType("integer");
+
+ b.Property("MaxIpRequests")
+ .HasColumnType("integer");
+
+ b.Property("MaxServerListRequests")
+ .HasColumnType("integer");
+
+ b.Property("MaximumReceiveSize")
+ .HasColumnType("smallint");
+
+ b.Property("PatchAddress")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ServerId")
+ .HasColumnType("smallint");
+
+ b.Property("Timeout")
+ .HasColumnType("interval");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ConnectServerDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("DefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("Value")
+ .HasColumnType("real");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CharacterClassId");
+
+ b.HasIndex("DefinitionId");
+
+ b.HasIndex("GameConfigurationId");
+
+ b.ToTable("ConstValueAttribute", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Chance")
+ .HasColumnType("double precision");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("ItemLevel")
+ .HasColumnType("smallint");
+
+ b.Property("ItemType")
+ .HasColumnType("integer");
+
+ b.Property("MaximumMonsterLevel")
+ .HasColumnType("smallint");
+
+ b.Property("MinimumMonsterLevel")
+ .HasColumnType("smallint");
+
+ b.Property("MonsterId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameConfigurationId");
+
+ b.HasIndex("MonsterId");
+
+ b.ToTable("DropItemGroup", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b =>
+ {
+ b.Property("DropItemGroupId")
+ .HasColumnType("uuid");
+
+ b.Property("ItemDefinitionId")
+ .HasColumnType("uuid");
+
+ b.HasKey("DropItemGroupId", "ItemDefinitionId");
+
+ b.HasIndex("ItemDefinitionId");
+
+ b.ToTable("DropItemGroupItemDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("DuelConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("FirstPlayerGateId")
+ .HasColumnType("uuid");
+
+ b.Property("Index")
+ .HasColumnType("smallint");
+
+ b.Property("SecondPlayerGateId")
+ .HasColumnType("uuid");
+
+ b.Property("SpectatorsGateId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DuelConfigurationId");
+
+ b.HasIndex("FirstPlayerGateId");
+
+ b.HasIndex("SecondPlayerGateId");
+
+ b.HasIndex("SpectatorsGateId");
+
+ b.ToTable("DuelArea", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("EntranceFee")
+ .HasColumnType("integer");
+
+ b.Property("ExitId")
+ .HasColumnType("uuid");
+
+ b.Property("MaximumScore")
+ .HasColumnType("integer");
+
+ b.Property("MaximumSpectatorsPerDuelRoom")
+ .HasColumnType("integer");
+
+ b.Property("MinimumCharacterLevel")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ExitId");
+
+ b.ToTable("DuelConfiguration", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("GameMapDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("LevelRequirement")
+ .HasColumnType("smallint");
+
+ b.Property("Number")
+ .HasColumnType("smallint");
+
+ b.Property("TargetGateId")
+ .HasColumnType("uuid");
+
+ b.Property("X1")
+ .HasColumnType("smallint");
+
+ b.Property("X2")
+ .HasColumnType("smallint");
+
+ b.Property("Y1")
+ .HasColumnType("smallint");
+
+ b.Property("Y2")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameMapDefinitionId");
+
+ b.HasIndex("TargetGateId");
+
+ b.ToTable("EnterGate", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Direction")
+ .HasColumnType("integer");
+
+ b.Property("IsSpawnGate")
+ .HasColumnType("boolean");
+
+ b.Property("MapId")
+ .HasColumnType("uuid");
+
+ b.Property("X1")
+ .HasColumnType("smallint");
+
+ b.Property("X2")
+ .HasColumnType("smallint");
+
+ b.Property("Y1")
+ .HasColumnType("smallint");
+
+ b.Property("Y2")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MapId");
+
+ b.ToTable("ExitGate", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Friend", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Accepted")
+ .HasColumnType("boolean");
+
+ b.Property("CharacterId")
+ .HasColumnType("uuid");
+
+ b.Property("FriendId")
+ .HasColumnType("uuid");
+
+ b.Property("RequestOpen")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.HasAlternateKey("CharacterId", "FriendId");
+
+ b.ToTable("Friend", "friend");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Episode")
+ .HasColumnType("smallint");
+
+ b.Property("Language")
+ .HasColumnType("integer");
+
+ b.Property("Season")
+ .HasColumnType("smallint");
+
+ b.Property("Serial")
+ .HasColumnType("bytea");
+
+ b.Property("Version")
+ .HasColumnType("bytea");
+
+ b.HasKey("Id");
+
+ b.ToTable("GameClientDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AreaSkillHitsPlayer")
+ .HasColumnType("boolean");
+
+ b.Property("CharacterNameRegex")
+ .HasColumnType("text");
+
+ b.Property("ClampMoneyOnPickup")
+ .HasColumnType("boolean");
+
+ b.Property("DamagePerOneItemDurability")
+ .HasColumnType("double precision");
+
+ b.Property("DamagePerOnePetDurability")
+ .HasColumnType("double precision");
+
+ b.Property("DuelConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("ExperienceFormula")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValue("if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))");
+
+ b.Property("ExperienceRate")
+ .HasColumnType("real");
+
+ b.Property("HitsPerOneItemDurability")
+ .HasColumnType("double precision");
+
+ b.Property("InfoRange")
+ .HasColumnType("smallint");
+
+ b.Property("ItemDropDuration")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("interval")
+ .HasDefaultValue(new TimeSpan(0, 0, 1, 0, 0));
+
+ b.Property("LetterSendPrice")
+ .HasColumnType("integer");
+
+ b.Property("MasterExperienceFormula")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValue("(505 * level * level * level) + (35278500 * level) + (228045 * level * level)");
+
+ b.Property("MaximumCharactersPerAccount")
+ .HasColumnType("smallint");
+
+ b.Property("MaximumInventoryMoney")
+ .HasColumnType("integer");
+
+ b.Property("MaximumItemOptionLevelDrop")
+ .HasColumnType("smallint");
+
+ b.Property("MaximumLetters")
+ .HasColumnType("integer");
+
+ b.Property("MaximumLevel")
+ .HasColumnType("smallint");
+
+ b.Property("MaximumMasterLevel")
+ .HasColumnType("smallint");
+
+ b.Property("MaximumPartySize")
+ .HasColumnType("smallint");
+
+ b.Property("MaximumPasswordLength")
+ .HasColumnType("integer");
+
+ b.Property("MaximumVaultMoney")
+ .HasColumnType("integer");
+
+ b.Property("MinimumMonsterLevelForMasterExperience")
+ .HasColumnType("smallint");
+
+ b.Property("PreventExperienceOverflow")
+ .HasColumnType("boolean");
+
+ b.Property("RecoveryInterval")
+ .HasColumnType("integer");
+
+ b.Property("ShouldDropMoney")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DuelConfigurationId")
+ .IsUnique();
+
+ b.ToTable("GameConfiguration", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BattleZoneId")
+ .HasColumnType("uuid");
+
+ b.Property("Discriminator")
+ .HasColumnType("integer");
+
+ b.Property