From 8b5a8b18d73e1b2fe208ee5954f68fde782ec210 Mon Sep 17 00:00:00 2001 From: itayalroy Date: Sun, 10 May 2026 00:06:49 +0300 Subject: [PATCH 1/5] Fine-grained walk delays for players and monsters --- src/GameLogic/NPC/Monster.cs | 35 ++++- src/GameLogic/Player.cs | 144 +++++++++++++++++- src/GameLogic/Walker.cs | 34 +++-- .../BlowOfDestructionEffectInitializer.cs | 38 +++++ .../Skills/MagicEffectNumber.cs | 7 +- .../VersionSeasonSix/SkillsInitializer.cs | 2 + 6 files changed, 235 insertions(+), 25 deletions(-) create mode 100644 src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs diff --git a/src/GameLogic/NPC/Monster.cs b/src/GameLogic/NPC/Monster.cs index 62b5c6558..f65c14475 100644 --- a/src/GameLogic/NPC/Monster.cs +++ b/src/GameLogic/NPC/Monster.cs @@ -18,6 +18,11 @@ namespace MUnique.OpenMU.GameLogic.NPC; /// public sealed class Monster : AttackableNpcBase, IAttackable, IAttacker, ISupportWalk, IMovable, ISummonable { + private const short IcedEffectNumber = 0x38; + private const short BlowOfDestructionEffectNumber = 0x56; + private const double IcedMovementSpeedFactor = 0.5; + private const double BlowOfDestructionMovementSpeedFactor = 0.33; + private readonly AsyncLock _moveLock = new(); private readonly INpcIntelligence _intelligence; private readonly Walker _walker; @@ -59,7 +64,7 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, : base(spawnInfo, stats, map, eventStateProvider, dropGenerator, plugInManager) { this._pathFinderPool = pathFinderPool; - this._walker = new Walker(this, () => this.StepDelay); + this._walker = new Walker(this, this.GetStepDelay); this._intelligence = npcIntelligence; (this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget) = this.CreateMagicEffectPowerUp(); @@ -87,7 +92,7 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, public Point WalkTarget => this._walker.CurrentTarget; /// - public TimeSpan StepDelay => this.Definition.MoveDelay; + public TimeSpan StepDelay => this.GetStepDelay(null); /// /// Monsters don't do combos. @@ -356,6 +361,30 @@ private static WalkingStep GetStep(PathResultNode node) }; } + private TimeSpan GetStepDelay(WalkingStep? step) + { + var tileDistance = step is { } walkingStep ? walkingStep.From.EuclideanDistanceTo(walkingStep.To) : 1.0; + var delayMilliseconds = this.Definition.MoveDelay.TotalMilliseconds * Math.Max(1.0, tileDistance); + delayMilliseconds /= this.GetMovementSpeedFactor(); + + return TimeSpan.FromMilliseconds(delayMilliseconds); + } + + private double GetMovementSpeedFactor() + { + if (this.MagicEffectList.ActiveEffects.ContainsKey(IcedEffectNumber)) + { + return IcedMovementSpeedFactor; + } + + if (this.MagicEffectList.ActiveEffects.ContainsKey(BlowOfDestructionEffectNumber)) + { + return BlowOfDestructionMovementSpeedFactor; + } + + return 1.0; + } + /// /// Creates the magic effect power up for the given skill of a monster. /// @@ -401,4 +430,4 @@ private void ValidatePath(Memory steps) } } } -} \ No newline at end of file +} diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 2f3091b55..2e8c81860 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.GameLogic; using System.Threading; using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.DataModel.Attributes; +using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.GuildWar; using MUnique.OpenMU.GameLogic.MiniGames; @@ -41,6 +42,17 @@ namespace MUnique.OpenMU.GameLogic; /// public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacker, ITrader, IPartyMember, IRotatable, IHasBucketInformation, ISupportWalk, IMovable, ILoggerOwner { + private const short IcedEffectNumber = 0x38; + private const short BlowOfDestructionEffectNumber = 0x56; + private const double IcedMovementSpeedFactor = 0.5; + private const double BlowOfDestructionMovementSpeedFactor = 0.33; + private const byte RunningGearMinimumLevel = 5; + private const ushort AtlansMapNumber = 7; + private const ushort Kalima1MapNumber = 24; + private const ushort Kalima6MapNumber = 29; + private const ushort Kalima7MapNumber = 36; + private const ushort Doppelgaenger3MapNumber = 67; + private static readonly MagicEffectDefinition GMEffect = new GMMagicEffectDefinition { InformObservers = true, @@ -148,7 +160,7 @@ public Player(IGameContext gameContext) public bool IsWalking => this._walker.CurrentTarget != default; /// - public TimeSpan StepDelay => this.GetStepDelay(); + public TimeSpan StepDelay => this.GetStepDelay(null); /// public Point WalkTarget => this._walker.CurrentTarget; @@ -2149,19 +2161,135 @@ private async ValueTask RegenerateHeroStateAsync() } /// - /// Gets the step delay depending on the equipped items. + /// Gets the step delay depending on the equipped items and current movement effects. /// + /// The walking step for which the delay is calculated. /// The current step delay, depending on equipped items. - private TimeSpan GetStepDelay() + private TimeSpan GetStepDelay(WalkingStep? step) + { + const double referenceFrameTimeMilliseconds = 40.0; + const double terrainScale = 100.0; + + var speed = this.GetClientMovementSpeed(step?.From); + var tileDistance = step is { } walkingStep ? walkingStep.From.EuclideanDistanceTo(walkingStep.To) : 1.0; + var movementMilliseconds = terrainScale * Math.Max(1.0, tileDistance) / speed * referenceFrameTimeMilliseconds; + + return TimeSpan.FromMilliseconds(movementMilliseconds); + } + + private double GetClientMovementSpeed(Point? position = null) + { + const double walkSpeed = 12.0; + if (this.IsInClientSafezone(position)) + { + return this.ApplyMovementEffects(walkSpeed); + } + + return this.ApplyMovementEffects(this.GetMountedOrRunningSpeed(walkSpeed)); + } + + private double ApplyMovementEffects(double speed) + { + if (this.MagicEffectList.ActiveEffects.ContainsKey(IcedEffectNumber)) + { + return speed * IcedMovementSpeedFactor; + } + + if (this.MagicEffectList.ActiveEffects.ContainsKey(BlowOfDestructionEffectNumber)) + { + return speed * BlowOfDestructionMovementSpeedFactor; + } + + return speed; + } + + private double GetMountedOrRunningSpeed(double walkSpeed) { - if (this.Inventory?.EquippedItems.Any(item => item.Definition?.ItemSlot?.ItemSlots.Contains(7) ?? false) ?? false) + const double runSpeed = 15.0; + const double fastWingSpeed = 16.0; + const double horseOrFenrirRunSpeed = 17.0; + const double excellentFenrirRunSpeed = 19.0; + + var pet = this.Inventory?.GetItem(InventoryConstants.PetSlot); + if (this.IsItem(pet, 13, 37)) + { + if (this.HasFenrirMovementOption(pet)) + { + return excellentFenrirRunSpeed; + } + + return horseOrFenrirRunSpeed; + } + + if (this.IsItem(pet, 13, 4)) { - // Wings - return TimeSpan.FromMilliseconds(300); + return horseOrFenrirRunSpeed; } - // TODO: Consider pets etc. - return TimeSpan.FromMilliseconds(500); + var wings = this.Inventory?.GetItem(InventoryConstants.WingsSlot); + if (this.HasEquippedWings(wings) + || this.IsItem(pet, 13, 2) + || this.IsItem(pet, 13, 3)) + { + return this.GetWingMovementSpeed(wings, runSpeed, fastWingSpeed); + } + + return this.HasRunningGear() ? runSpeed : walkSpeed; + } + + private double GetWingMovementSpeed(Item? wings, double runSpeed, double fastWingSpeed) + { + return this.IsFastWing(wings) ? fastWingSpeed : runSpeed; + } + + private bool IsInClientSafezone(Point? position = null) + { + var checkedPosition = position ?? this.Position; + return this.CurrentMap?.Terrain.SafezoneMap[checkedPosition.X, checkedPosition.Y] ?? false; + } + + private bool HasEquippedWings(Item? item) + { + return item is { Durability: > 0.0 } + && item.ItemSlot == InventoryConstants.WingsSlot; + } + + private bool IsFastWing(Item? item) + { + return this.IsItem(item, 12, 5) + || this.IsItem(item, 12, 36); + } + + private bool HasRunningGear() + { + var slot = this.IsSwimmingMovementMap() + ? InventoryConstants.GlovesSlot + : InventoryConstants.BootsSlot; + return this.Inventory?.GetItem(slot) is { Durability: > 0.0, Level: >= RunningGearMinimumLevel }; + } + + private bool IsSwimmingMovementMap() + { + return this.CurrentMap?.MapId is AtlansMapNumber + or >= Kalima1MapNumber and <= Kalima6MapNumber + or Kalima7MapNumber + or Doppelgaenger3MapNumber; + } + + private bool IsItem(Item? item, short group, short number) + { + return item is { Durability: > 0.0 } + && item.Definition is { } definition + && definition.Group == group + && definition.Number == number; + } + + private bool HasFenrirMovementOption(Item? item) + { + return item?.ItemOptions.Any(option => + option.ItemOption?.OptionType == ItemOptionTypes.BlueFenrir + || option.ItemOption?.OptionType == ItemOptionTypes.BlackFenrir + || option.ItemOption?.OptionType == ItemOptionTypes.GoldFenrir) ?? false; } private async ValueTask GetSpawnGateOfCurrentMapAsync() diff --git a/src/GameLogic/Walker.cs b/src/GameLogic/Walker.cs index 1e92989dd..6dc57574c 100644 --- a/src/GameLogic/Walker.cs +++ b/src/GameLogic/Walker.cs @@ -16,7 +16,6 @@ namespace MUnique.OpenMU.GameLogic; public sealed class Walker : IDisposable { private readonly ISupportWalk _walkSupporter; - private readonly Func _stepDelay; private readonly Queue _nextSteps = new(5); /// @@ -39,10 +38,10 @@ public sealed class Walker : IDisposable /// /// The walk supporter. /// The delay between performing a step. - public Walker(ISupportWalk walkSupporter, Func stepDelay) + public Walker(ISupportWalk walkSupporter, Func stepDelay) { this._walkSupporter = walkSupporter; - this._stepDelay = stepDelay; + this.StepDelay = stepDelay; this._walkLock = new AsyncReaderWriterLock(); } @@ -51,6 +50,8 @@ public Walker(ISupportWalk walkSupporter, Func stepDelay) /// public Point CurrentTarget { get; private set; } + private Func StepDelay { get; } + /// /// Initializes a new walk to the specified target with the specified steps. /// @@ -180,15 +181,18 @@ public void Dispose() private async Task WalkLoopAsync(CancellationToken cancellationToken) { - var delay = this._stepDelay().Subtract(TimeSpan.FromMilliseconds(50)); - // Task.Delay might take longer than we specify. We need to compensate that. var lastOffset = TimeSpan.Zero; while (!cancellationToken.IsCancellationRequested) { var sw = Stopwatch.StartNew(); - await this.WalkStepAsync(cancellationToken).ConfigureAwait(false); + var step = await this.WalkStepAsync(cancellationToken).ConfigureAwait(false); + if (step is null) + { + continue; + } + var delay = this.StepDelay(step); var nextDelay = delay - lastOffset; if (nextDelay > TimeSpan.Zero) { @@ -207,14 +211,14 @@ private async Task WalkLoopAsync(CancellationToken cancellationToken) /// /// Performs the next step of a walk. /// - private async ValueTask WalkStepAsync(CancellationToken cancellationToken) + private async ValueTask WalkStepAsync(CancellationToken cancellationToken) { try { if (this._isDisposed) { Debug.WriteLine("walker already disposed"); - return; + return null; } bool stop; @@ -226,13 +230,13 @@ private async ValueTask WalkStepAsync(CancellationToken cancellationToken) if (stop) { await this.StopAsync().ConfigureAwait(false); - return; + return null; } // Update new coords using (await this._walkLock.WriterLockAsync(cancellationToken)) { - this.WalkNextStepIfStepAvailable(); + return this.WalkNextStepIfStepAvailable(); } } catch (OperationCanceledException) @@ -243,13 +247,15 @@ private async ValueTask WalkStepAsync(CancellationToken cancellationToken) { Debug.Fail(ex.Message, ex.StackTrace); } + + return null; } - private void WalkNextStepIfStepAvailable() + private WalkingStep? WalkNextStepIfStepAvailable() { if (this.ShouldWalkerStop()) { - return; + return null; } var nextStep = this._nextSteps.Dequeue(); @@ -259,7 +265,9 @@ private void WalkNextStepIfStepAvailable() { rotatable.Rotation = nextStep.Direction; } + + return nextStep; } private bool ShouldWalkerStop() => !((this._walkSupporter as IAttackable)?.IsActive() ?? false) || this._nextSteps.Count <= 0; -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs b/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs new file mode 100644 index 000000000..21304e419 --- /dev/null +++ b/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs @@ -0,0 +1,38 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Skills; + +using MUnique.OpenMU.DataModel.Attributes; +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// Initializer for the blow of destruction effect which results from the strike of destruction skill. +/// +public class BlowOfDestructionEffectInitializer : InitializerBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public BlowOfDestructionEffectInitializer(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + public override void Initialize() + { + var magicEffect = this.Context.CreateNew(); + this.GameConfiguration.MagicEffects.Add(magicEffect); + magicEffect.Number = (short)MagicEffectNumber.BlowOfDestruction; + magicEffect.Name = "Blow of Destruction Effect (Strike of Destruction)"; + magicEffect.InformObservers = true; + magicEffect.SendDuration = true; + magicEffect.StopByDeath = true; + magicEffect.Duration = this.Context.CreateNew(); + magicEffect.Duration.ConstantValue.Value = (float)TimeSpan.FromSeconds(10).TotalSeconds; + } +} diff --git a/src/Persistence/Initialization/Skills/MagicEffectNumber.cs b/src/Persistence/Initialization/Skills/MagicEffectNumber.cs index a38d8cb9f..10c99418e 100644 --- a/src/Persistence/Initialization/Skills/MagicEffectNumber.cs +++ b/src/Persistence/Initialization/Skills/MagicEffectNumber.cs @@ -296,6 +296,11 @@ internal enum MagicEffectNumber : short /// WizEnhance = 0x52, + /// + /// The blow of destruction effect. + /// + BlowOfDestruction = 0x56, + /// /// The ignore defense effect of the rage fighter. /// @@ -348,4 +353,4 @@ internal enum MagicEffectNumber : short #endregion -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs index 9f544a025..2de69d60c 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs @@ -71,6 +71,7 @@ internal class SkillsInitializer : SkillsInitializerBase { SkillNumber.Weakness, MagicEffectNumber.WeaknessSummoner }, { SkillNumber.Innovation, MagicEffectNumber.Innovation }, { SkillNumber.DamageReflection, MagicEffectNumber.Reflection }, + { SkillNumber.StrikeofDestruction, MagicEffectNumber.BlowOfDestruction }, { SkillNumber.BeastUppercut, MagicEffectNumber.DefenseReductionBeastUppercut }, { SkillNumber.PhoenixShot, MagicEffectNumber.DecreaseBlock }, { SkillNumber.Explosion223, MagicEffectNumber.Explosion }, @@ -688,6 +689,7 @@ private void InitializeEffects() new WeaknessSummonerEffectInitializer(this.Context, this.GameConfiguration).Initialize(); new InnovationEffectInitializer(this.Context, this.GameConfiguration).Initialize(); new ReflectionEffectInitializer(this.Context, this.GameConfiguration).Initialize(); + new BlowOfDestructionEffectInitializer(this.Context, this.GameConfiguration).Initialize(); new DefenseReductionBeastUppercutEffectInitializer(this.Context, this.GameConfiguration).Initialize(); new DecreaseBlockEffectInitializer(this.Context, this.GameConfiguration).Initialize(); new ExplosionEffectInitializer(this.Context, this.GameConfiguration).Initialize(); From e9bb1057273fae0f2126ee3acfa6128f66e68993 Mon Sep 17 00:00:00 2001 From: itayalroy Date: Fri, 8 May 2026 09:11:24 +0300 Subject: [PATCH 2/5] Block player attacks involving safe zones --- src/GameLogic/Player.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 2e8c81860..2b08361e4 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -727,6 +727,11 @@ public ValueTask ShowBlueMessageAsync(string message) throw new InvalidOperationException("AttributeSystem not set."); } + if (this.IsAttackBlockedBySafezone(attacker)) + { + return null; + } + if (!this.GameContext.PvpEnabled && this.CurrentMap?.Definition.BattleZone == null && this.CurrentMiniGame?.AllowPlayerKilling is false) { @@ -1462,6 +1467,17 @@ public async ValueTask WalkToAsync(Point target, Memory steps) /// public ValueTask StopWalkingAsync() => this._walker.StopAsync(); + private bool IsAttackBlockedBySafezone(IAttacker attacker) + { + if (this.IsAtSafezone()) + { + return true; + } + + var attackerPlayer = attacker as Player ?? (attacker as IPlayerSurrogate)?.Owner; + return attackerPlayer?.IsAtSafezone() is true; + } + /// /// Regenerates the attributes specified in . /// From 027a915655f7c628c6950e8c0a826e18af30bfec Mon Sep 17 00:00:00 2001 From: itayalroy Date: Fri, 8 May 2026 09:11:27 +0300 Subject: [PATCH 3/5] Stop sending walk directions by default --- src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs | 8 ++++---- .../RemoteView/World/ObjectMovedPlugInExtended.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs b/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs index 95072df92..c3361e1d4 100644 --- a/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs +++ b/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs @@ -40,7 +40,7 @@ public class ObjectMovedPlugIn : IObjectMovedPlugIn /// Gets or sets a value indicating whether the directions provided by should be send when an object moved. /// This is usually not required, because the game client calculates a proper path anyway and doesn't use the suggested path. /// - public bool SendWalkDirections { get; set; } = true; + public bool SendWalkDirections { get; set; } /// public async ValueTask ObjectMovedAsync(ILocateable obj, MoveType type) @@ -95,7 +95,7 @@ protected virtual async ValueTask SendWalkAsync(IConnection connection, ushort o { int Write() { - var stepsSize = steps.Length == 0 ? 1 : (steps.Length / 2) + 2; + var stepsSize = stepsLength == 0 ? 0 : (stepsLength / 2) + 2; var size = ObjectWalkedRef.GetRequiredSize(stepsSize); var span = connection.Output.GetSpan(size)[..size]; @@ -109,7 +109,7 @@ int Write() StepCount = (byte)stepsLength, }; - this.SetStepData(walkPacket, steps.Span, stepsSize); + this.SetStepData(walkPacket, steps.Span[..stepsLength], stepsSize); return size; } @@ -221,4 +221,4 @@ protected byte GetWalkCode() return (byte)PacketType.Walk; } } -} \ No newline at end of file +} diff --git a/src/GameServer/RemoteView/World/ObjectMovedPlugInExtended.cs b/src/GameServer/RemoteView/World/ObjectMovedPlugInExtended.cs index 85535a1f1..4e550cddd 100644 --- a/src/GameServer/RemoteView/World/ObjectMovedPlugInExtended.cs +++ b/src/GameServer/RemoteView/World/ObjectMovedPlugInExtended.cs @@ -40,7 +40,7 @@ protected override async ValueTask SendWalkAsync(IConnection connection, ushort { int Write() { - var stepsSize = steps.Length == 0 ? 1 : (steps.Length / 2) + 2; + var stepsSize = stepsLength == 0 ? 0 : (stepsLength / 2) + 2; var size = ObjectWalkedExtended.GetRequiredSize(stepsSize); var span = connection.Output.GetSpan(size)[..size]; @@ -56,7 +56,7 @@ int Write() StepCount = (byte)stepsLength, }; - this.SetStepData(walkPacket, steps.Span, stepsSize); + this.SetStepData(walkPacket, steps.Span[..stepsLength], stepsSize); return size; } @@ -79,4 +79,4 @@ private void SetStepData(ObjectWalkedExtendedRef walkPacket, Span ste walkPacket.StepData[index] = (byte)(firstStep << 4 | secondStep); } } -} \ No newline at end of file +} From 401f123955d68dd08bbbed5b74dc9808f2abbe53 Mon Sep 17 00:00:00 2001 From: itayalroy Date: Mon, 18 May 2026 01:50:23 +0300 Subject: [PATCH 4/5] Move speed configuration into attributes --- src/GameLogic/AttackableExtensions.cs | 12 +- .../Attributes/MonsterAttributeHolder.cs | 5 +- src/GameLogic/Attributes/Stats.cs | 20 ++ src/GameLogic/InventoryStorage.cs | 7 +- src/GameLogic/NPC/Monster.cs | 53 +-- src/GameLogic/Player.cs | 121 +------ .../Initialization/BaseMapInitializer.cs | 33 +- .../GameConfigurationInitializerBase.cs | 2 + .../Items/ArmorInitializerBase.cs | 33 +- .../Initialization/MovementSpeedConstants.cs | 21 ++ .../BlowOfDestructionEffectInitializer.cs | 9 + .../Skills/SkillsInitializerBase.cs | 12 +- .../AddMovementSpeedAttributesPlugIn075.cs | 26 ++ .../AddMovementSpeedAttributesPlugIn095d.cs | 26 ++ .../AddMovementSpeedAttributesPlugInBase.cs | 317 ++++++++++++++++++ ...AddMovementSpeedAttributesPlugInSeason6.cs | 28 ++ .../Initialization/Updates/UpdateVersion.cs | 15 + .../Initialization/Version075/Items/Pets.cs | 6 +- .../Initialization/Version075/Items/Wings.cs | 5 +- .../Initialization/Version075/Maps/Atlans.cs | 9 +- .../Initialization/Version095d/Items/Pets.cs | 18 +- .../Initialization/Version095d/Items/Wings.cs | 5 +- .../VersionSeasonSix/Items/Pets.cs | 53 ++- .../VersionSeasonSix/Items/Wings.cs | 15 +- .../VersionSeasonSix/Maps/Doppelgaenger3.cs | 9 +- .../VersionSeasonSix/Maps/KalimaBase.cs | 9 +- 26 files changed, 687 insertions(+), 182 deletions(-) create mode 100644 src/Persistence/Initialization/MovementSpeedConstants.cs create mode 100644 src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn075.cs create mode 100644 src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs create mode 100644 src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInBase.cs create mode 100644 src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInSeason6.cs diff --git a/src/GameLogic/AttackableExtensions.cs b/src/GameLogic/AttackableExtensions.cs index 5a4377795..eda92bd31 100644 --- a/src/GameLogic/AttackableExtensions.cs +++ b/src/GameLogic/AttackableExtensions.cs @@ -425,12 +425,11 @@ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackab /// The target. /// The attacker. /// The skill. - /// The power up. + /// The power ups. /// The duration. - /// The target attribute. /// 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) + public static async ValueTask TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IReadOnlyCollection<(AttributeDefinition Target, IElement Boost)> powerUps, IElement? duration, HitInfo? hitInfo) { if (!target.IsAlive) { @@ -453,12 +452,11 @@ public static async ValueTask TryApplyElementalEffectsAsync(this IAttackab if (skill.MagicEffectDef is { } effectDefinition && !target.MagicEffectList.ActiveEffects.ContainsKey(effectDefinition.Number) - && powerUp is not null && duration is not null - && targetAttribute is not null) + && powerUps.Count > 0) { // power-up is the wrong term here... it's more like a power-down ;-) - await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, (targetAttribute, powerUp)).ConfigureAwait(false); + await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, [.. powerUps]).ConfigureAwait(false); applied = true; } @@ -984,4 +982,4 @@ private static int GetMasterSkillTreeMasteryPvpDamageBonus(IAttacker attacker) return 0; } } -} \ No newline at end of file +} diff --git a/src/GameLogic/Attributes/MonsterAttributeHolder.cs b/src/GameLogic/Attributes/MonsterAttributeHolder.cs index 45b88bc2d..b32ffbf0c 100644 --- a/src/GameLogic/Attributes/MonsterAttributeHolder.cs +++ b/src/GameLogic/Attributes/MonsterAttributeHolder.cs @@ -21,6 +21,7 @@ public class MonsterAttributeHolder : IAttributeSystem { Stats.DefensePvp, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) }, { Stats.DamageReceiveDecrement, m => 1.0f }, { Stats.AttackDamageIncrease, m => 1.0f }, + { Stats.MovementSpeedFactor, m => 1.0f }, { Stats.ShieldBypassChance, m => 1.0f }, { Stats.DefenseDecrement, m => 1.0f - m.Attributes.GetValueOfAttribute(Stats.InnovationDefDecrement) }, }; @@ -131,7 +132,7 @@ public void RemoveElement(IElement element, AttributeDefinition targetAttribute) if (attributes.TryGetValue(targetAttribute, out var attribute)) { attribute.RemoveElement(element); - if (attribute.Elements.Skip(1).Take(1).Any()) + if (!attribute.Elements.Skip(1).Any()) { attributes.Remove(targetAttribute); } @@ -179,4 +180,4 @@ private IDictionary GetAttributeDicti return attributes; } } -} \ No newline at end of file +} diff --git a/src/GameLogic/Attributes/Stats.cs b/src/GameLogic/Attributes/Stats.cs index da3d82caa..99d833b8d 100644 --- a/src/GameLogic/Attributes/Stats.cs +++ b/src/GameLogic/Attributes/Stats.cs @@ -439,6 +439,21 @@ public class Stats /// public static AttributeDefinition WalkSpeed { get; } = new(new Guid("9CDDC598-E5F3-4372-9294-505455E4A40B"), "Walk Speed", string.Empty); + /// + /// Gets the maximum movement speed attribute definition. + /// + public static AttributeDefinition MaxMovementSpeed { get; } = new(new Guid("E29301BE-626B-4B42-9F68-0DFAC18B3856"), "Maximum Movement Speed", "The maximum movement speed of a character on regular terrain."); + + /// + /// Gets the maximum underwater movement speed attribute definition. + /// + public static AttributeDefinition MaxMovementSpeedUnderwater { get; } = new(new Guid("12128DC7-0740-48A5-A653-E546191CD7E0"), "Maximum Underwater Movement Speed", "The maximum movement speed of a character on underwater terrain."); + + /// + /// Gets the movement speed factor attribute definition. + /// + public static AttributeDefinition MovementSpeedFactor { get; } = new(new Guid("003E1F2E-661D-4258-BEF0-33111D5F4AD2"), "Movement Speed Factor", "The factor which is applied to the final movement speed of a character."); + /// /// Gets the attack damage increase attribute definition. /// @@ -1331,6 +1346,11 @@ public class Stats /// public static AttributeDefinition IsInSafezone { get; } = new(new Guid("82044DF9-F528-4AD6-9AAA-6FEAA4C786E7"), "Flag, if the character is located in a safezone of a game map", "Characters at the safezone recover additional health and shield."); + /// + /// Gets the attribute which defines if the character is located on an underwater game map. + /// + public static AttributeDefinition IsUnderwater { get; } = new(new Guid("72A684C1-102B-4FDE-B637-2665ADD5F4AE"), "Flag, if the character is located on an underwater game map", "Characters on underwater maps use underwater movement speed attributes."); + /// /// Gets the attribute definition, which defines if a player has MU Helper activated. /// diff --git a/src/GameLogic/InventoryStorage.cs b/src/GameLogic/InventoryStorage.cs index 2ad8c858a..9925b53f5 100644 --- a/src/GameLogic/InventoryStorage.cs +++ b/src/GameLogic/InventoryStorage.cs @@ -155,10 +155,7 @@ await this._player.ForEachWorldObserverAsync( } } - if (item.Definition!.PossibleItemSetGroups.Count > 0) - { - this.UpdateSetPowerUps(); - } + this.UpdateSetPowerUps(); var itemAdded = this.EquippedItems.Contains(item); if (itemAdded) @@ -220,4 +217,4 @@ private void UpdateSetPowerUps() var factory = this._gameContext.ItemPowerUpFactory; this._player.Attributes.ItemSetPowerUps = factory.GetSetPowerUps(this.EquippedItems, this._player.Attributes, this._player.GameContext.Configuration).ToList(); } -} \ No newline at end of file +} diff --git a/src/GameLogic/NPC/Monster.cs b/src/GameLogic/NPC/Monster.cs index f65c14475..d9205b67c 100644 --- a/src/GameLogic/NPC/Monster.cs +++ b/src/GameLogic/NPC/Monster.cs @@ -18,31 +18,21 @@ namespace MUnique.OpenMU.GameLogic.NPC; /// public sealed class Monster : AttackableNpcBase, IAttackable, IAttacker, ISupportWalk, IMovable, ISummonable { - private const short IcedEffectNumber = 0x38; - private const short BlowOfDestructionEffectNumber = 0x56; - private const double IcedMovementSpeedFactor = 0.5; - private const double BlowOfDestructionMovementSpeedFactor = 0.33; - private readonly AsyncLock _moveLock = new(); private readonly INpcIntelligence _intelligence; private readonly Walker _walker; /// - /// The power up element of the monster skill. - /// It is a "cached" element which will be created on demand and can be applied multiple times. + /// The power up elements of the monster skill. + /// These are "cached" elements which will be created on demand and can be applied multiple times. /// - private readonly IElement? _skillPowerUp; + private readonly (AttributeDefinition Target, IElement Boost)[] _skillPowerUps; /// - /// The duration of the . + /// The duration of the . /// private readonly IElement? _skillPowerUpDuration; - /// - /// The target attribute of the . - /// - private readonly AttributeDefinition? _skillPowerUpTarget; - private readonly IObjectPool _pathFinderPool; private bool _isCalculatingPath; @@ -67,7 +57,7 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, this._walker = new Walker(this, this.GetStepDelay); this._intelligence = npcIntelligence; - (this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget) = this.CreateMagicEffectPowerUp(); + (this._skillPowerUps, this._skillPowerUpDuration) = this.CreateMagicEffectPowerUps(); this._intelligence.Npc = this; } @@ -127,7 +117,7 @@ public async ValueTask AttackAsync(IAttackable target) 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, hitInfo).ConfigureAwait(false); + await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUps, this._skillPowerUpDuration, hitInfo).ConfigureAwait(false); await this.ForEachWorldObserverAsync(p => p.ShowSkillAnimationAsync(this, target, attackSkill, true), true).ConfigureAwait(false); } @@ -372,40 +362,31 @@ private TimeSpan GetStepDelay(WalkingStep? step) private double GetMovementSpeedFactor() { - if (this.MagicEffectList.ActiveEffects.ContainsKey(IcedEffectNumber)) - { - return IcedMovementSpeedFactor; - } - - if (this.MagicEffectList.ActiveEffects.ContainsKey(BlowOfDestructionEffectNumber)) - { - return BlowOfDestructionMovementSpeedFactor; - } + var movementSpeedFactor = this.Attributes[Stats.MovementSpeedFactor]; - return 1.0; + return movementSpeedFactor > 0 ? movementSpeedFactor : 1.0; } /// - /// Creates the magic effect power up for the given skill of a monster. + /// Creates the magic effect power ups for the given skill of a monster. /// - /// - /// Currently, we just support one effect for monsters. - /// - private (IElement? PowerUp, IElement? Duration, AttributeDefinition? Target) CreateMagicEffectPowerUp() + private ((AttributeDefinition Target, IElement Boost)[] PowerUps, IElement? Duration) CreateMagicEffectPowerUps() { var skill = this.Definition.AttackSkill; - if (skill?.MagicEffectDef?.PowerUpDefinitions.FirstOrDefault() is not { } powerUpDefinition - || skill.MagicEffectDef.Duration is not { } duration) + if (skill?.MagicEffectDef is not { } magicEffectDefinition + || magicEffectDefinition.Duration is not { } duration) { - return (null, null, null); + return ([], null); } - if (powerUpDefinition.Boost is null) + if (magicEffectDefinition.PowerUpDefinitions.Any(p => p.Boost is null || p.TargetAttribute is null)) { throw new InvalidOperationException($"Skill {skill.Name} ({skill.Number}) has no magic effect definition or is without a PowerUpDefinition."); } - return (this.Attributes.CreateElement(powerUpDefinition), this.Attributes.CreateDurationElement(duration), powerUpDefinition.TargetAttribute); + return ( + [.. magicEffectDefinition.PowerUpDefinitions.Select(p => (p.TargetAttribute!, this.Attributes.CreateElement(p)))], + this.Attributes.CreateDurationElement(duration)); } private void ValidatePath(Memory steps) diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 2b08361e4..db4d47330 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -42,16 +42,7 @@ namespace MUnique.OpenMU.GameLogic; /// public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacker, ITrader, IPartyMember, IRotatable, IHasBucketInformation, ISupportWalk, IMovable, ILoggerOwner { - private const short IcedEffectNumber = 0x38; - private const short BlowOfDestructionEffectNumber = 0x56; - private const double IcedMovementSpeedFactor = 0.5; - private const double BlowOfDestructionMovementSpeedFactor = 0.33; - private const byte RunningGearMinimumLevel = 5; - private const ushort AtlansMapNumber = 7; - private const ushort Kalima1MapNumber = 24; - private const ushort Kalima6MapNumber = 29; - private const ushort Kalima7MapNumber = 36; - private const ushort Doppelgaenger3MapNumber = 67; + private const double WalkMovementSpeed = 12.0; private static readonly MagicEffectDefinition GMEffect = new GMMagicEffectDefinition { @@ -2195,67 +2186,24 @@ private TimeSpan GetStepDelay(WalkingStep? step) private double GetClientMovementSpeed(Point? position = null) { - const double walkSpeed = 12.0; if (this.IsInClientSafezone(position)) { - return this.ApplyMovementEffects(walkSpeed); + return this.ApplyMovementSpeedFactor(WalkMovementSpeed); } - return this.ApplyMovementEffects(this.GetMountedOrRunningSpeed(walkSpeed)); - } - - private double ApplyMovementEffects(double speed) - { - if (this.MagicEffectList.ActiveEffects.ContainsKey(IcedEffectNumber)) - { - return speed * IcedMovementSpeedFactor; - } + var speedAttribute = this.Attributes?[Stats.IsUnderwater] > 0 + ? Stats.MaxMovementSpeedUnderwater + : Stats.MaxMovementSpeed; + var speed = this.Attributes?[speedAttribute] ?? 0; - if (this.MagicEffectList.ActiveEffects.ContainsKey(BlowOfDestructionEffectNumber)) - { - return speed * BlowOfDestructionMovementSpeedFactor; - } - - return speed; + return this.ApplyMovementSpeedFactor(Math.Max(WalkMovementSpeed, speed)); } - private double GetMountedOrRunningSpeed(double walkSpeed) + private double ApplyMovementSpeedFactor(double speed) { - const double runSpeed = 15.0; - const double fastWingSpeed = 16.0; - const double horseOrFenrirRunSpeed = 17.0; - const double excellentFenrirRunSpeed = 19.0; + var movementSpeedFactor = this.Attributes?[Stats.MovementSpeedFactor] ?? 1.0; - var pet = this.Inventory?.GetItem(InventoryConstants.PetSlot); - if (this.IsItem(pet, 13, 37)) - { - if (this.HasFenrirMovementOption(pet)) - { - return excellentFenrirRunSpeed; - } - - return horseOrFenrirRunSpeed; - } - - if (this.IsItem(pet, 13, 4)) - { - return horseOrFenrirRunSpeed; - } - - var wings = this.Inventory?.GetItem(InventoryConstants.WingsSlot); - if (this.HasEquippedWings(wings) - || this.IsItem(pet, 13, 2) - || this.IsItem(pet, 13, 3)) - { - return this.GetWingMovementSpeed(wings, runSpeed, fastWingSpeed); - } - - return this.HasRunningGear() ? runSpeed : walkSpeed; - } - - private double GetWingMovementSpeed(Item? wings, double runSpeed, double fastWingSpeed) - { - return this.IsFastWing(wings) ? fastWingSpeed : runSpeed; + return speed * (movementSpeedFactor > 0 ? movementSpeedFactor : 1.0); } private bool IsInClientSafezone(Point? position = null) @@ -2264,50 +2212,6 @@ private bool IsInClientSafezone(Point? position = null) return this.CurrentMap?.Terrain.SafezoneMap[checkedPosition.X, checkedPosition.Y] ?? false; } - private bool HasEquippedWings(Item? item) - { - return item is { Durability: > 0.0 } - && item.ItemSlot == InventoryConstants.WingsSlot; - } - - private bool IsFastWing(Item? item) - { - return this.IsItem(item, 12, 5) - || this.IsItem(item, 12, 36); - } - - private bool HasRunningGear() - { - var slot = this.IsSwimmingMovementMap() - ? InventoryConstants.GlovesSlot - : InventoryConstants.BootsSlot; - return this.Inventory?.GetItem(slot) is { Durability: > 0.0, Level: >= RunningGearMinimumLevel }; - } - - private bool IsSwimmingMovementMap() - { - return this.CurrentMap?.MapId is AtlansMapNumber - or >= Kalima1MapNumber and <= Kalima6MapNumber - or Kalima7MapNumber - or Doppelgaenger3MapNumber; - } - - private bool IsItem(Item? item, short group, short number) - { - return item is { Durability: > 0.0 } - && item.Definition is { } definition - && definition.Group == group - && definition.Number == number; - } - - private bool HasFenrirMovementOption(Item? item) - { - return item?.ItemOptions.Any(option => - option.ItemOption?.OptionType == ItemOptionTypes.BlueFenrir - || option.ItemOption?.OptionType == ItemOptionTypes.BlackFenrir - || option.ItemOption?.OptionType == ItemOptionTypes.GoldFenrir) ?? false; - } - private async ValueTask GetSpawnGateOfCurrentMapAsync() { if (this.CurrentMap is null) @@ -2599,7 +2503,7 @@ private void RaisePlayerEnteredMap(GameMap map) { foreach (var powerUpDefinition in powerUpDefinitions) { - if (powerUpDefinition.TargetAttribute is not { } targetAttribute) + if (powerUpDefinition.TargetAttribute is null) { continue; } @@ -2607,13 +2511,12 @@ private void RaisePlayerEnteredMap(GameMap map) var powerUps = PowerUpWrapper.CreateByPowerUpDefinition(powerUpDefinition, attributes); powerUps.ForEach(p => { - this.Attributes?.AddElement(p, targetAttribute); this.PlayerLeftMap += OnPlayerLeftMap; void OnPlayerLeftMap(object? o, (Player, GameMap) args) { this.PlayerLeftMap -= OnPlayerLeftMap; - attributes.RemoveElement(p, targetAttribute); + p.Dispose(); } }); } diff --git a/src/Persistence/Initialization/BaseMapInitializer.cs b/src/Persistence/Initialization/BaseMapInitializer.cs index 36f7a13b1..548a8f12b 100644 --- a/src/Persistence/Initialization/BaseMapInitializer.cs +++ b/src/Persistence/Initialization/BaseMapInitializer.cs @@ -11,9 +11,11 @@ namespace MUnique.OpenMU.Persistence.Initialization; using System.IO; using System.Reflection; using MUnique.OpenMU.AttributeSystem; +using MUnique.OpenMU.DataModel.Attributes; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Attributes; /// /// Base class for a map initializer which provides some common basic functionality. @@ -295,6 +297,35 @@ protected void CreateRequirement(AttributeDefinition attribute, int minimumValue this._mapDefinition.MapRequirements.Add(requirement); } + /// + /// Adds a character power up to the current map. + /// + /// The target attribute. + /// The power up value. + /// The aggregate type. + protected void AddCharacterPowerUp(AttributeDefinition attribute, float value, AggregateType aggregateType = AggregateType.AddRaw) + { + if (this._mapDefinition is null) + { + throw new InvalidOperationException("MapDefinition not set yet."); + } + + var powerUp = this.Context.CreateNew(); + powerUp.TargetAttribute = attribute.GetPersistent(this.GameConfiguration); + powerUp.Boost = this.Context.CreateNew(); + powerUp.Boost.ConstantValue.Value = value; + powerUp.Boost.ConstantValue.AggregateType = aggregateType; + this._mapDefinition.CharacterPowerUpDefinitions.Add(powerUp); + } + + /// + /// Marks the current map as underwater for character movement. + /// + protected void AddUnderwaterMovementPowerUp() + { + this.AddCharacterPowerUp(Stats.IsUnderwater, 1); + } + private string? GetTerrainFileName() { var assembly = Assembly.GetExecutingAssembly(); @@ -324,4 +355,4 @@ protected void CreateRequirement(AttributeDefinition attribute, int minimumValue return null; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/GameConfigurationInitializerBase.cs b/src/Persistence/Initialization/GameConfigurationInitializerBase.cs index 746203cc4..c8c8da9c0 100644 --- a/src/Persistence/Initialization/GameConfigurationInitializerBase.cs +++ b/src/Persistence/Initialization/GameConfigurationInitializerBase.cs @@ -244,6 +244,8 @@ private void AddGlobalBaseAttributeValues() var randomExperienceMaxMultiplier = this.Context.CreateNew(1.2f, Stats.RandomExperienceMaxMultiplier.GetPersistent(this.GameConfiguration)); this.GameConfiguration.GlobalBaseAttributeValues.Add(randomExperienceMaxMultiplier); + + this.GameConfiguration.GlobalBaseAttributeValues.Add(this.Context.CreateNew(1f, Stats.MovementSpeedFactor.GetPersistent(this.GameConfiguration))); } diff --git a/src/Persistence/Initialization/Items/ArmorInitializerBase.cs b/src/Persistence/Initialization/Items/ArmorInitializerBase.cs index 42c09caf2..a9835c94b 100644 --- a/src/Persistence/Initialization/Items/ArmorInitializerBase.cs +++ b/src/Persistence/Initialization/Items/ArmorInitializerBase.cs @@ -22,6 +22,7 @@ public abstract class ArmorInitializerBase : InitializerBase private ItemLevelBonusTable? _defenseIncreaseTable; private ItemLevelBonusTable? _shieldDefenseIncreaseTable; private ItemLevelBonusTable? _shieldDefenseRateIncreaseTable; + private ItemLevelBonusTable? _runningMovementSpeedTable; /// /// Initializes a new instance of the class. @@ -44,6 +45,7 @@ public override void Initialize() this._defenseIncreaseTable = this.CreateItemBonusTable(DefenseIncreaseByLevel, "Defense Increase (Armors)", "Defines the defense increase per item level for armors. It's 3 per item level until level 9, then it's always 1 more for each level."); this._shieldDefenseIncreaseTable = this.CreateItemBonusTable(ShieldDefenseIncreaseByLevel, "Defense Increase (Shields)", "Defines the defense increase per item level for shields. It's always 1 per item level."); this._shieldDefenseRateIncreaseTable = this.CreateItemBonusTable(DefenseIncreaseByLevel, "Defense Rate Increase (Shields)", "Defines the defense rate increase per item level for shields. It's 3 per item level until level 9, then it's always 1 more for each level."); + this._runningMovementSpeedTable = this.CreateRunningMovementSpeedTable(); } /// @@ -147,6 +149,7 @@ protected ItemDefinition CreateGloves(byte number, string name, byte dropLevel, gloves.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.AttackSpeedAny, attackSpeed, AggregateType.AddRaw)); } + this.AddRunningMovementSpeed(gloves, Stats.MaxMovementSpeedUnderwater); return gloves; } @@ -158,6 +161,7 @@ protected ItemDefinition CreateGloves(byte number, string name, byte dropLevel, gloves.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.AttackSpeedAny, attackSpeed, AggregateType.AddRaw)); } + this.AddRunningMovementSpeed(gloves, Stats.MaxMovementSpeedUnderwater); return gloves; } @@ -169,6 +173,7 @@ protected ItemDefinition CreateBoots(byte number, byte slot, byte width, byte he boots.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.WalkSpeed, walkSpeed, AggregateType.AddRaw)); } + this.AddRunningMovementSpeed(boots, Stats.MaxMovementSpeed); return boots; } @@ -180,9 +185,35 @@ protected ItemDefinition CreateBoots(byte number, string name, byte dropLevel, i boots.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.WalkSpeed, walkSpeed, AggregateType.AddRaw)); } + this.AddRunningMovementSpeed(boots, Stats.MaxMovementSpeed); return boots; } + private void AddRunningMovementSpeed(ItemDefinition item, AttributeDefinition targetAttribute) + { + var powerUp = this.CreateItemBasePowerUpDefinition(targetAttribute, 0, AggregateType.Maximum); + powerUp.BonusPerLevelTable = this._runningMovementSpeedTable; + item.BasePowerUpAttributes.Add(powerUp); + } + + private ItemLevelBonusTable CreateRunningMovementSpeedTable() + { + var table = this.Context.CreateNew(); + this.GameConfiguration.ItemLevelBonusTables.Add(table); + table.Name = "Running Movement Speed"; + table.Description = "Defines the running movement speed for boots and underwater gloves from item level 5."; + + for (int level = MovementSpeedConstants.RunningGearMinimumLevel; level <= this.MaximumArmorLevel; level++) + { + var levelBonus = this.Context.CreateNew(); + levelBonus.Level = level; + levelBonus.AdditionalValue = MovementSpeedConstants.RunningGearMovementSpeed; + table.BonusPerLevel.Add(levelBonus); + } + + return table; + } + protected ItemDefinition CreateArmor(byte number, byte slot, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int strengthRequirement, int agilityRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel) { var magicGladiatorClassLevel = 0; @@ -282,4 +313,4 @@ private void CreateSetGroup(byte setLevel, ItemOptionDefinition options, ICollec item.PossibleItemSetGroups.Add(setForDefense); } } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/MovementSpeedConstants.cs b/src/Persistence/Initialization/MovementSpeedConstants.cs new file mode 100644 index 000000000..cfe56213c --- /dev/null +++ b/src/Persistence/Initialization/MovementSpeedConstants.cs @@ -0,0 +1,21 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization; + +/// +/// Movement speed constants used by configuration initializers and update plugins. +/// +internal static class MovementSpeedConstants +{ + internal const int RunningGearMinimumLevel = 5; + internal const float RunningGearMovementSpeed = 15f; + internal const float DefaultWingMovementSpeed = 15f; + internal const float FastWingMovementSpeed = 16f; + internal const float BasicMountMovementSpeed = 15f; + internal const float HorseOrFenrirMovementSpeed = 17f; + internal const float UpgradedFenrirMovementSpeed = 19f; + internal const float IcedMovementSpeedFactor = 0.5f; + internal const float BlowOfDestructionMovementSpeedFactor = 0.33f; +} diff --git a/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs b/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs index 21304e419..6f6ea05f6 100644 --- a/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs +++ b/src/Persistence/Initialization/Skills/BlowOfDestructionEffectInitializer.cs @@ -4,8 +4,10 @@ namespace MUnique.OpenMU.Persistence.Initialization.Skills; +using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.DataModel.Attributes; using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic.Attributes; /// /// Initializer for the blow of destruction effect which results from the strike of destruction skill. @@ -34,5 +36,12 @@ public override void Initialize() magicEffect.StopByDeath = true; magicEffect.Duration = this.Context.CreateNew(); magicEffect.Duration.ConstantValue.Value = (float)TimeSpan.FromSeconds(10).TotalSeconds; + + var movementSpeedFactorPowerUp = this.Context.CreateNew(); + magicEffect.PowerUpDefinitions.Add(movementSpeedFactorPowerUp); + movementSpeedFactorPowerUp.TargetAttribute = Stats.MovementSpeedFactor.GetPersistent(this.GameConfiguration); + movementSpeedFactorPowerUp.Boost = this.Context.CreateNew(); + movementSpeedFactorPowerUp.Boost.ConstantValue.Value = MovementSpeedConstants.BlowOfDestructionMovementSpeedFactor; + movementSpeedFactorPowerUp.Boost.ConstantValue.AggregateType = AggregateType.Multiplicate; } } diff --git a/src/Persistence/Initialization/Skills/SkillsInitializerBase.cs b/src/Persistence/Initialization/Skills/SkillsInitializerBase.cs index 7443c6427..e719503f9 100644 --- a/src/Persistence/Initialization/Skills/SkillsInitializerBase.cs +++ b/src/Persistence/Initialization/Skills/SkillsInitializerBase.cs @@ -267,6 +267,16 @@ private MagicEffectDefinition CreateEffect(ElementalType type, MagicEffectNumber powerUpDefinition.Boost = this.Context.CreateNew(); powerUpDefinition.Boost.ConstantValue.Value = 1; powerUpDefinition.TargetAttribute = targetAttribute.GetPersistent(this.GameConfiguration); + if (targetAttribute == Stats.IsIced) + { + var movementSpeedFactorPowerUp = this.Context.CreateNew(); + effect.PowerUpDefinitions.Add(movementSpeedFactorPowerUp); + movementSpeedFactorPowerUp.Boost = this.Context.CreateNew(); + movementSpeedFactorPowerUp.Boost.ConstantValue.Value = MovementSpeedConstants.IcedMovementSpeedFactor; + movementSpeedFactorPowerUp.Boost.ConstantValue.AggregateType = AggregateType.Multiplicate; + movementSpeedFactorPowerUp.TargetAttribute = Stats.MovementSpeedFactor.GetPersistent(this.GameConfiguration); + } + return effect; } @@ -291,4 +301,4 @@ private void CreateSkillRequirementIfNeeded(Skill skill, AttributeDefinition att var requirement = this.CreateRequirement(attribute, requiredValue); skill.Requirements.Add(requirement); } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn075.cs b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn075.cs new file mode 100644 index 000000000..f68310595 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn075.cs @@ -0,0 +1,26 @@ +// +// 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.PlugIns; + +/// +/// Adds movement speed attributes to 0.75 game configurations. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("890E2FCB-EC93-4CC1-84FC-67A1B398D5C8")] +public class AddMovementSpeedAttributesPlugIn075 : AddMovementSpeedAttributesPlugInBase +{ + /// + public override UpdateVersion Version => UpdateVersion.AddMovementSpeedAttributes075; + + /// + public override string DataInitializationKey => Version075.DataInitialization.Id; + + /// + protected override int MaximumItemLevel => Version075.Items.Constants.MaximumItemLevel; +} diff --git a/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs new file mode 100644 index 000000000..83d980c2f --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs @@ -0,0 +1,26 @@ +// +// 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.PlugIns; + +/// +/// Adds movement speed attributes to 0.95d game configurations. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("7C38C30F-163B-4625-A82D-5C3A0A9ED883")] +public class AddMovementSpeedAttributesPlugIn095d : AddMovementSpeedAttributesPlugInBase +{ + /// + public override UpdateVersion Version => UpdateVersion.AddMovementSpeedAttributes095d; + + /// + public override string DataInitializationKey => Version095d.DataInitialization.Id; + + /// + protected override int MaximumItemLevel => Version095d.Items.Constants.MaximumItemLevel; +} diff --git a/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInBase.cs b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInBase.cs new file mode 100644 index 000000000..17803b016 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInBase.cs @@ -0,0 +1,317 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using MUnique.OpenMU.AttributeSystem; +using MUnique.OpenMU.DataModel; +using MUnique.OpenMU.DataModel.Attributes; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.Persistence.Initialization.Items; +using MUnique.OpenMU.Persistence.Initialization.Skills; +using AtlansMap = MUnique.OpenMU.Persistence.Initialization.Version075.Maps.Atlans; +using Doppelgaenger3Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Doppelgaenger3; +using Kalima1Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima1; +using Kalima2Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima2; +using Kalima3Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima3; +using Kalima4Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima4; +using Kalima5Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima5; +using Kalima6Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima6; +using Kalima7Map = MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps.Kalima7; + +/// +/// Adds movement speed attributes and configuration values. +/// +public abstract class AddMovementSpeedAttributesPlugInBase : UpdatePlugInBase +{ + private const byte PetItemGroup = (byte)ItemGroups.Misc1; + private const byte UniriaNumber = 2; + private const byte DinorantNumber = 3; + private const byte DarkHorseNumber = 4; + private const byte WingsOfDragonNumber = 5; + private const byte WingOfStormNumber = 36; + private const byte FenrirNumber = 37; + private const string RunningMovementSpeedTableName = "Running Movement Speed"; + private const int BlackFenrirMovementSpeedCombinationBonusNumber = 101; + private const int BlackFenrirUnderwaterMovementSpeedCombinationBonusNumber = 102; + private const int BlueFenrirMovementSpeedCombinationBonusNumber = 103; + private const int BlueFenrirUnderwaterMovementSpeedCombinationBonusNumber = 104; + private const int GoldFenrirMovementSpeedCombinationBonusNumber = 105; + private const int GoldFenrirUnderwaterMovementSpeedCombinationBonusNumber = 106; + + private static readonly short[] UnderwaterMapNumbers = + [ + AtlansMap.Number, + Kalima1Map.Number, + Kalima2Map.Number, + Kalima3Map.Number, + Kalima4Map.Number, + Kalima5Map.Number, + Kalima6Map.Number, + Kalima7Map.Number, + Doppelgaenger3Map.Number, + ]; + + /// + /// The plug in name. + /// + internal const string PlugInName = "Add Movement Speed Attributes"; + + /// + /// The plug in description. + /// + internal const string PlugInDescription = "Adds attribute-based movement speed configuration for players, monsters, items, effects, and underwater maps."; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override bool IsMandatory => true; + + /// + public override DateTime CreatedAt => new(2026, 05, 15, 20, 0, 0, DateTimeKind.Utc); + + /// + /// Gets the maximum item level of the target game version. + /// + protected abstract int MaximumItemLevel { get; } + + /// + protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + AddStatIfNotExists(context, gameConfiguration, Stats.MaxMovementSpeed); + AddStatIfNotExists(context, gameConfiguration, Stats.MaxMovementSpeedUnderwater); + AddStatIfNotExists(context, gameConfiguration, Stats.MovementSpeedFactor); + AddStatIfNotExists(context, gameConfiguration, Stats.IsUnderwater); + + this.AddGlobalMovementSpeedFactor(context, gameConfiguration); + this.AddEffectMovementSpeedFactors(context, gameConfiguration); + this.AddItemMovementSpeeds(context, gameConfiguration); + this.AddUnderwaterMapPowerUps(context, gameConfiguration); + return ValueTask.CompletedTask; + } + + private void AddGlobalMovementSpeedFactor(IContext context, GameConfiguration gameConfiguration) + { + if (gameConfiguration.GlobalBaseAttributeValues.Any(a => a.Definition?.Id == Stats.MovementSpeedFactor.Id)) + { + return; + } + + gameConfiguration.GlobalBaseAttributeValues.Add(context.CreateNew(1f, Stats.MovementSpeedFactor.GetPersistent(gameConfiguration))); + } + + private void AddEffectMovementSpeedFactors(IContext context, GameConfiguration gameConfiguration) + { + foreach (var icedEffect in gameConfiguration.MagicEffects.Where(e => e.Number == (short)MagicEffectNumber.Iced)) + { + this.AddMovementSpeedFactorPowerUp(context, gameConfiguration, icedEffect, MovementSpeedConstants.IcedMovementSpeedFactor); + } + + var blowOfDestructionEffect = gameConfiguration.MagicEffects.FirstOrDefault(e => e.Number == (short)MagicEffectNumber.BlowOfDestruction); + if (blowOfDestructionEffect is null + && gameConfiguration.Skills.Any(s => s.Number == (short)SkillNumber.StrikeofDestruction)) + { + new BlowOfDestructionEffectInitializer(context, gameConfiguration).Initialize(); + blowOfDestructionEffect = gameConfiguration.MagicEffects.FirstOrDefault(e => e.Number == (short)MagicEffectNumber.BlowOfDestruction); + } + + if (blowOfDestructionEffect is not null) + { + this.AddMovementSpeedFactorPowerUp(context, gameConfiguration, blowOfDestructionEffect, MovementSpeedConstants.BlowOfDestructionMovementSpeedFactor); + foreach (var skill in gameConfiguration.Skills.Where(s => s.Number == (short)SkillNumber.StrikeofDestruction)) + { + skill.MagicEffectDef = blowOfDestructionEffect; + } + } + } + + private void AddMovementSpeedFactorPowerUp(IContext context, GameConfiguration gameConfiguration, MagicEffectDefinition magicEffect, float value) + { + if (magicEffect.PowerUpDefinitions.Any(p => p.TargetAttribute?.Id == Stats.MovementSpeedFactor.Id)) + { + return; + } + + magicEffect.PowerUpDefinitions.Add(this.CreatePowerUpDefinition(context, gameConfiguration, Stats.MovementSpeedFactor, value, AggregateType.Multiplicate)); + } + + private void AddItemMovementSpeeds(IContext context, GameConfiguration gameConfiguration) + { + var runningMovementSpeedTable = this.GetOrCreateRunningMovementSpeedTable(context, gameConfiguration); + + foreach (var boots in gameConfiguration.Items.Where(item => item.Group == (byte)ItemGroups.Boots)) + { + this.AddItemBasePowerUp(context, gameConfiguration, boots, Stats.MaxMovementSpeed, 0, AggregateType.Maximum, runningMovementSpeedTable); + } + + foreach (var gloves in gameConfiguration.Items.Where(item => item.Group == (byte)ItemGroups.Gloves)) + { + this.AddItemBasePowerUp(context, gameConfiguration, gloves, Stats.MaxMovementSpeedUnderwater, 0, AggregateType.Maximum, runningMovementSpeedTable); + } + + foreach (var wing in gameConfiguration.Items.Where(IsWingSlotItem)) + { + this.AddMovementSpeedPowerUps(context, gameConfiguration, wing, GetWingMovementSpeed(wing)); + } + + foreach (var pet in gameConfiguration.Items.Where(item => item.Group == PetItemGroup)) + { + var speed = GetPetMovementSpeed(pet); + + if (speed > 0) + { + this.AddMovementSpeedPowerUps(context, gameConfiguration, pet, speed); + } + } + + this.AddFenrirMovementSpeedCombinationBonuses(context, gameConfiguration); + } + + private ItemLevelBonusTable GetOrCreateRunningMovementSpeedTable(IContext context, GameConfiguration gameConfiguration) + { + if (gameConfiguration.ItemLevelBonusTables.FirstOrDefault(t => t.Name == RunningMovementSpeedTableName) is { } existingTable) + { + return existingTable; + } + + var table = context.CreateNew(); + gameConfiguration.ItemLevelBonusTables.Add(table); + table.Name = RunningMovementSpeedTableName; + table.Description = "Defines the running movement speed for boots and underwater gloves from item level 5."; + for (int level = MovementSpeedConstants.RunningGearMinimumLevel; level <= this.MaximumItemLevel; level++) + { + var levelBonus = context.CreateNew(); + levelBonus.Level = level; + levelBonus.AdditionalValue = MovementSpeedConstants.RunningGearMovementSpeed; + table.BonusPerLevel.Add(levelBonus); + } + + return table; + } + + private static bool IsWingSlotItem(ItemDefinition item) + { + return item.ItemSlot?.ItemSlots.Contains(InventoryConstants.WingsSlot) ?? false; + } + + private static float GetWingMovementSpeed(ItemDefinition wing) + { + return wing.Number is WingsOfDragonNumber or WingOfStormNumber + ? MovementSpeedConstants.FastWingMovementSpeed + : MovementSpeedConstants.DefaultWingMovementSpeed; + } + + private static float GetPetMovementSpeed(ItemDefinition pet) + { + return pet.Number switch + { + UniriaNumber or DinorantNumber => MovementSpeedConstants.BasicMountMovementSpeed, + DarkHorseNumber or FenrirNumber => MovementSpeedConstants.HorseOrFenrirMovementSpeed, + _ => 0f, + }; + } + + private void AddMovementSpeedPowerUps(IContext context, GameConfiguration gameConfiguration, ItemDefinition item, float speed) + { + this.AddItemBasePowerUp(context, gameConfiguration, item, Stats.MaxMovementSpeed, speed, AggregateType.Maximum); + this.AddItemBasePowerUp(context, gameConfiguration, item, Stats.MaxMovementSpeedUnderwater, speed, AggregateType.Maximum); + } + + private void AddItemBasePowerUp( + IContext context, + GameConfiguration gameConfiguration, + ItemDefinition item, + AttributeDefinition targetAttribute, + float value, + AggregateType aggregateType, + ItemLevelBonusTable? bonusPerLevelTable = null) + { + if (item.BasePowerUpAttributes.Any(p => p.TargetAttribute?.Id == targetAttribute.Id)) + { + return; + } + + var powerUp = context.CreateNew(); + powerUp.TargetAttribute = targetAttribute.GetPersistent(gameConfiguration); + powerUp.BaseValue = value; + powerUp.AggregateType = aggregateType; + powerUp.BonusPerLevelTable = bonusPerLevelTable; + item.BasePowerUpAttributes.Add(powerUp); + } + + private void AddFenrirMovementSpeedCombinationBonuses(IContext context, GameConfiguration gameConfiguration) + { + this.AddFenrirMovementSpeedCombinationBonus(context, gameConfiguration, ItemOptionTypes.BlackFenrir, BlackFenrirMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeed); + this.AddFenrirMovementSpeedCombinationBonus(context, gameConfiguration, ItemOptionTypes.BlackFenrir, BlackFenrirUnderwaterMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeedUnderwater); + this.AddFenrirMovementSpeedCombinationBonus(context, gameConfiguration, ItemOptionTypes.BlueFenrir, BlueFenrirMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeed); + this.AddFenrirMovementSpeedCombinationBonus(context, gameConfiguration, ItemOptionTypes.BlueFenrir, BlueFenrirUnderwaterMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeedUnderwater); + this.AddFenrirMovementSpeedCombinationBonus(context, gameConfiguration, ItemOptionTypes.GoldFenrir, GoldFenrirMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeed); + this.AddFenrirMovementSpeedCombinationBonus(context, gameConfiguration, ItemOptionTypes.GoldFenrir, GoldFenrirUnderwaterMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeedUnderwater); + } + + private void AddFenrirMovementSpeedCombinationBonus( + IContext context, + GameConfiguration gameConfiguration, + ItemOptionType optionType, + int number, + AttributeDefinition targetAttribute) + { + if (gameConfiguration.ItemOptionTypes.FirstOrDefault(t => t == optionType) is not { } persistentOptionType) + { + return; + } + + if (gameConfiguration.ItemOptionCombinationBonuses.Any(b => + b.Bonus?.TargetAttribute?.Id == targetAttribute.Id + && b.Requirements.Any(r => r.OptionType == persistentOptionType))) + { + return; + } + + var combinationBonus = context.CreateNew(); + combinationBonus.Number = number; + combinationBonus.Description = $"{persistentOptionType.Name}: {targetAttribute.Designation}"; + combinationBonus.AppliesMultipleTimes = false; + combinationBonus.Requirements.Add(this.CreateFenrirMovementSpeedRequirement(context, persistentOptionType)); + combinationBonus.Bonus = this.CreatePowerUpDefinition(context, gameConfiguration, targetAttribute, MovementSpeedConstants.UpgradedFenrirMovementSpeed, AggregateType.Maximum); + gameConfiguration.ItemOptionCombinationBonuses.Add(combinationBonus); + } + + private CombinationBonusRequirement CreateFenrirMovementSpeedRequirement(IContext context, ItemOptionType optionType) + { + var requirement = context.CreateNew(); + requirement.OptionType = optionType; + requirement.MinimumCount = 1; + return requirement; + } + + private void AddUnderwaterMapPowerUps(IContext context, GameConfiguration gameConfiguration) + { + foreach (var map in gameConfiguration.Maps.Where(m => UnderwaterMapNumbers.Contains(m.Number))) + { + if (map.CharacterPowerUpDefinitions.Any(p => p.TargetAttribute?.Id == Stats.IsUnderwater.Id)) + { + continue; + } + + map.CharacterPowerUpDefinitions.Add(this.CreatePowerUpDefinition(context, gameConfiguration, Stats.IsUnderwater, 1, AggregateType.AddRaw)); + } + } + + private PowerUpDefinition CreatePowerUpDefinition(IContext context, GameConfiguration gameConfiguration, AttributeDefinition targetAttribute, float value, AggregateType aggregateType) + { + var powerUp = context.CreateNew(); + powerUp.TargetAttribute = targetAttribute.GetPersistent(gameConfiguration); + powerUp.Boost = context.CreateNew(); + powerUp.Boost.ConstantValue.Value = value; + powerUp.Boost.ConstantValue.AggregateType = aggregateType; + return powerUp; + } +} diff --git a/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInSeason6.cs b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInSeason6.cs new file mode 100644 index 000000000..38dcfd7aa --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugInSeason6.cs @@ -0,0 +1,28 @@ +// +// 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.PlugIns; + +/// +/// Adds movement speed attributes to season 6 game configurations. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("1D4968DA-9C9C-42A7-AF80-D4811535EC63")] +public class AddMovementSpeedAttributesPlugInSeason6 : AddMovementSpeedAttributesPlugInBase +{ + private const int SeasonSixMaximumItemLevel = 15; + + /// + public override UpdateVersion Version => UpdateVersion.AddMovementSpeedAttributesSeason6; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + protected override int MaximumItemLevel => SeasonSixMaximumItemLevel; +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index a1e209a93..bfd9d6a95 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -422,4 +422,19 @@ public enum UpdateVersion /// The version of the . /// AddRandomExperienceConfigAttributesSeason6 = 83, + + /// + /// The version of the . + /// + AddMovementSpeedAttributes075 = 84, + + /// + /// The version of the . + /// + AddMovementSpeedAttributes095d = 85, + + /// + /// The version of the . + /// + AddMovementSpeedAttributesSeason6 = 86, } diff --git a/src/Persistence/Initialization/Version075/Items/Pets.cs b/src/Persistence/Initialization/Version075/Items/Pets.cs index 1d16abe57..c506d8801 100644 --- a/src/Persistence/Initialization/Version075/Items/Pets.cs +++ b/src/Persistence/Initialization/Version075/Items/Pets.cs @@ -33,7 +33,9 @@ public override void Initialize() this.AddItemToJewelItemDrop(angel); var imp = this.CreatePet(1, "Imp", 28, (Stats.AttackDamageIncrease, 1.3f, AggregateType.Multiplicate)); this.AddItemToJewelItemDrop(imp); - var uniria = this.CreatePet(2, "Horn of Uniria", 25); + var uniria = this.CreatePet(2, "Horn of Uniria", 25, + (Stats.MaxMovementSpeed, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum)); this.AddItemToJewelItemDrop(uniria); } @@ -69,4 +71,4 @@ private ItemDefinition CreatePet(byte number, string name, int dropLevelAndLevel pet.SetGuid(pet.Group, pet.Number); return pet; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/Version075/Items/Wings.cs b/src/Persistence/Initialization/Version075/Items/Wings.cs index 531874ea2..dcfd006ac 100644 --- a/src/Persistence/Initialization/Version075/Items/Wings.cs +++ b/src/Persistence/Initialization/Version075/Items/Wings.cs @@ -118,6 +118,9 @@ private ItemDefinition CreateWing(byte number, byte width, byte height, string n canFlyPowerUp.BaseValue = 1; wing.BasePowerUpAttributes.Add(canFlyPowerUp); + wing.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.MaxMovementSpeed, MovementSpeedConstants.DefaultWingMovementSpeed, AggregateType.Maximum)); + wing.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.DefaultWingMovementSpeed, AggregateType.Maximum)); + return wing; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/Version075/Maps/Atlans.cs b/src/Persistence/Initialization/Version075/Maps/Atlans.cs index 4a5c3e6bb..fc5cdd8f9 100644 --- a/src/Persistence/Initialization/Version075/Maps/Atlans.cs +++ b/src/Persistence/Initialization/Version075/Maps/Atlans.cs @@ -40,6 +40,13 @@ public Atlans(IContext context, GameConfiguration gameConfiguration) /// protected override string MapName => Name; + /// + protected override void AdditionalInitialization(GameMapDefinition mapDefinition) + { + base.AdditionalInitialization(mapDefinition); + this.AddUnderwaterMovementPowerUp(); + } + /// protected override IEnumerable CreateNpcSpawns() { @@ -651,4 +658,4 @@ protected override void CreateMonsters() monster.SetGuid(monster.Number); } } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/Version095d/Items/Pets.cs b/src/Persistence/Initialization/Version095d/Items/Pets.cs index d535ec284..74f20d5e9 100644 --- a/src/Persistence/Initialization/Version095d/Items/Pets.cs +++ b/src/Persistence/Initialization/Version095d/Items/Pets.cs @@ -34,10 +34,17 @@ public override void Initialize() this.AddItemToJewelItemDrop(angel); var imp = this.CreatePet(1, 0, "Imp", 28, true, (Stats.AttackDamageIncrease, 1.3f, AggregateType.Multiplicate)); this.AddItemToJewelItemDrop(imp); - var uniria = this.CreatePet(2, 0, "Horn of Uniria", 25, true); + var uniria = this.CreatePet(2, 0, "Horn of Uniria", 25, true, + (Stats.MaxMovementSpeed, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum)); this.AddItemToJewelItemDrop(uniria); - var dinorant = this.CreatePet(3, SkillNumber.FireBreath, "Horn of Dinorant", 110, false, (Stats.IsDinorantEquipped, 1, AggregateType.AddRaw), (Stats.DamageReceiveDecrement, 0.9f, AggregateType.Multiplicate), (Stats.AttackDamageIncrease, 1.15f, AggregateType.Multiplicate)); + var dinorant = this.CreatePet(3, SkillNumber.FireBreath, "Horn of Dinorant", 110, false, + (Stats.IsDinorantEquipped, 1, AggregateType.AddRaw), + (Stats.MaxMovementSpeed, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), + (Stats.DamageReceiveDecrement, 0.9f, AggregateType.Multiplicate), + (Stats.AttackDamageIncrease, 1.15f, AggregateType.Multiplicate)); this.AddDinorantOptions(dinorant); } @@ -71,6 +78,11 @@ private ItemDefinition CreatePet(byte number, SkillNumber skillNumber, string na var powerUpDefinition = this.Context.CreateNew(); powerUpDefinition.TargetAttribute = basePowerUp.Item1.GetPersistent(this.GameConfiguration); powerUpDefinition.BaseValue = basePowerUp.Item2; + if (basePowerUp.Item1 == Stats.MaxMovementSpeed || basePowerUp.Item1 == Stats.MaxMovementSpeedUnderwater) + { + powerUpDefinition.AggregateType = basePowerUp.Item3; + } + pet.BasePowerUpAttributes.Add(powerUpDefinition); } } @@ -104,4 +116,4 @@ private IncreasableItemOption CreateOption(ItemOptionType optionType, int number itemOption.PowerUpDefinition = this.CreatePowerUpDefinition(attributeDefinition, value, aggregateType); return itemOption; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/Version095d/Items/Wings.cs b/src/Persistence/Initialization/Version095d/Items/Wings.cs index c1517b2e4..603e48805 100644 --- a/src/Persistence/Initialization/Version095d/Items/Wings.cs +++ b/src/Persistence/Initialization/Version095d/Items/Wings.cs @@ -117,6 +117,9 @@ private ItemDefinition CreateWing(byte number, byte width, byte height, string n canFlyPowerUp.BaseValue = 1; wing.BasePowerUpAttributes.Add(canFlyPowerUp); + wing.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.MaxMovementSpeed, MovementSpeedConstants.DefaultWingMovementSpeed, AggregateType.Maximum)); + wing.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.DefaultWingMovementSpeed, AggregateType.Maximum)); + return wing; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs index 898571207..dfe9c6bfb 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs @@ -21,6 +21,13 @@ namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items; /// Pet system changed in Season 9. Reference: https://muonline.webzen.com/en/gameinfo/guide/detail/76 . public class Pets : InitializerBase { + private const int BlackFenrirMovementSpeedCombinationBonusNumber = 101; + private const int BlackFenrirUnderwaterMovementSpeedCombinationBonusNumber = 102; + private const int BlueFenrirMovementSpeedCombinationBonusNumber = 103; + private const int BlueFenrirUnderwaterMovementSpeedCombinationBonusNumber = 104; + private const int GoldFenrirMovementSpeedCombinationBonusNumber = 105; + private const int GoldFenrirUnderwaterMovementSpeedCombinationBonusNumber = 106; + private const string PetExperienceFormula = "level * level * level * 100 * (level + 10)"; /// @@ -44,17 +51,23 @@ public override void Initialize() var imp = this.CreatePet(1, 0, 1, 1, "Imp", 28, true, true, (Stats.AttackDamageIncrease, 1.3f, AggregateType.Multiplicate)); this.AddItemToJewelItemDrop(imp); - var uniria = this.CreatePet(2, 0, 1, 1, "Horn of Uniria", 25, true, true); + var uniria = this.CreatePet(2, 0, 1, 1, "Horn of Uniria", 25, true, true, + (Stats.MaxMovementSpeed, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum)); this.AddItemToJewelItemDrop(uniria); var dinorant = this.CreatePet(3, SkillNumber.FireBreath, 1, 1, "Horn of Dinorant", 110, false, true, (Stats.IsDinorantEquipped, 1, AggregateType.AddRaw), + (Stats.MaxMovementSpeed, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.BasicMountMovementSpeed, AggregateType.Maximum), (Stats.DamageReceiveDecrement, 0.9f, AggregateType.Multiplicate), (Stats.AttackDamageIncrease, 1.15f, AggregateType.Multiplicate)); this.AddDinorantOptions(dinorant); var darkHorse = this.CreatePet(4, SkillNumber.Earthshake, 1, 1, "Dark Horse", 218, false, false, - (Stats.IsHorseEquipped, 1, AggregateType.AddRaw)); + (Stats.IsHorseEquipped, 1, AggregateType.AddRaw), + (Stats.MaxMovementSpeed, MovementSpeedConstants.HorseOrFenrirMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.HorseOrFenrirMovementSpeed, AggregateType.Maximum)); this.AddDarkHorseOptions(darkHorse); this.GameConfiguration.DetermineCharacterClasses(CharacterClasses.AllLords).ForEach(darkHorse.QualifiedCharacters.Add); darkHorse.PetExperienceFormula = PetExperienceFormula; @@ -67,7 +80,9 @@ public override void Initialize() this.GameConfiguration.DetermineCharacterClasses(CharacterClasses.AllLords).ForEach(darkRaven.QualifiedCharacters.Add); var fenrir = this.CreatePet(37, SkillNumber.PlasmaStorm, 2, 2, "Horn of Fenrir", 300, false, true, - (Stats.CanFly, 1.0f, AggregateType.AddRaw)); + (Stats.CanFly, 1.0f, AggregateType.AddRaw), + (Stats.MaxMovementSpeed, MovementSpeedConstants.HorseOrFenrirMovementSpeed, AggregateType.Maximum), + (Stats.MaxMovementSpeedUnderwater, MovementSpeedConstants.HorseOrFenrirMovementSpeed, AggregateType.Maximum)); this.AddFenrirOptions(fenrir); this.CreatePet(64, 0, 1, 1, "Demon", 1, false, true, @@ -307,6 +322,36 @@ private void AddFenrirOptions(ItemDefinition fenrir) fenrirOptionDefinition.PossibleOptions.Add(this.CreateRelatedPetOption(ItemOptionTypes.GoldFenrir, 4, Stats.WizardryBaseDmg, AggregateType.AddRaw, ItemOptionDefinitionNumbers.Fenrir, 0, (Stats.TotalLevel, 1f / 25f))); fenrir.PossibleItemOptions.Add(fenrirOptionDefinition); + this.AddFenrirMovementSpeedCombinationBonuses(); + } + + private void AddFenrirMovementSpeedCombinationBonuses() + { + this.AddFenrirMovementSpeedCombinationBonus(ItemOptionTypes.BlackFenrir, BlackFenrirMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeed); + this.AddFenrirMovementSpeedCombinationBonus(ItemOptionTypes.BlackFenrir, BlackFenrirUnderwaterMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeedUnderwater); + this.AddFenrirMovementSpeedCombinationBonus(ItemOptionTypes.BlueFenrir, BlueFenrirMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeed); + this.AddFenrirMovementSpeedCombinationBonus(ItemOptionTypes.BlueFenrir, BlueFenrirUnderwaterMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeedUnderwater); + this.AddFenrirMovementSpeedCombinationBonus(ItemOptionTypes.GoldFenrir, GoldFenrirMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeed); + this.AddFenrirMovementSpeedCombinationBonus(ItemOptionTypes.GoldFenrir, GoldFenrirUnderwaterMovementSpeedCombinationBonusNumber, Stats.MaxMovementSpeedUnderwater); + } + + private void AddFenrirMovementSpeedCombinationBonus(ItemOptionType optionType, int number, AttributeDefinition targetAttribute) + { + var combinationBonus = this.Context.CreateNew(); + combinationBonus.Number = number; + combinationBonus.Description = $"{optionType.Name}: {targetAttribute.Designation}"; + combinationBonus.AppliesMultipleTimes = false; + combinationBonus.Requirements.Add(this.CreateFenrirMovementSpeedRequirement(optionType)); + combinationBonus.Bonus = this.CreatePowerUpDefinition(targetAttribute, MovementSpeedConstants.UpgradedFenrirMovementSpeed, AggregateType.Maximum); + this.GameConfiguration.ItemOptionCombinationBonuses.Add(combinationBonus); + } + + private CombinationBonusRequirement CreateFenrirMovementSpeedRequirement(ItemOptionType optionType) + { + var requirement = this.Context.CreateNew(); + requirement.OptionType = this.GameConfiguration.ItemOptionTypes.First(t => t == optionType); + requirement.MinimumCount = 1; + return requirement; } private IncreasableItemOption CreateOption(ItemOptionType optionType, int number, AttributeDefinition attributeDefinition, float value, AggregateType aggregateType, short optionNumber) @@ -343,4 +388,4 @@ private IncreasableItemOption CreateRelatedPetOption(ItemOptionType optionType, return itemOption; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/Wings.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/Wings.cs index bde53646b..0217b14f3 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/Wings.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/Wings.cs @@ -79,7 +79,7 @@ public override void Initialize() var secondWingOptions = this.CreateSecondClassWingOptions(); this.CreateWing(3, 5, 3, "Wings of Spirits", 150, 30, 200, 215, 0, 0, 2, 0, 0, 0, 0, this.BuildOptions((0b10, OptionType.HealthRecover), (0b00, OptionType.PhysDamage)), 32, 25, this._damageIncreaseByLevelTableSecond, secondWingOptions); this.CreateWing(4, 5, 3, "Wings of Soul", 150, 30, 200, 215, 2, 0, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b10, OptionType.WizDamage)), 32, 25, this._damageIncreaseByLevelTableSecond, secondWingOptions); - this.CreateWing(5, 3, 3, "Wings of Dragon", 150, 45, 200, 215, 0, 2, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b10, OptionType.PhysDamage)), 32, 25, this._damageIncreaseByLevelTableSecond, secondWingOptions); + this.CreateWing(5, 3, 3, "Wings of Dragon", 150, 45, 200, 215, 0, 2, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b10, OptionType.PhysDamage)), 32, 25, this._damageIncreaseByLevelTableSecond, secondWingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed); this.CreateWing(6, 4, 2, "Wings of Darkness", 150, 40, 200, 215, 0, 0, 0, 1, 0, 0, 0, this.BuildOptions((0b00, OptionType.WizDamage), (0b10, OptionType.PhysDamage)), 32, 25, this._damageIncreaseByLevelTableSecond, secondWingOptions); this.CreateWing(42, 4, 3, "Wings of Despair", 150, 30, 200, 215, 0, 0, 0, 0, 0, 2, 0, this.BuildOptions((0b00, OptionType.CurseDamage), (0b10, OptionType.WizDamage)), 32, 25, this._damageIncreaseByLevelTableSecond, secondWingOptions); @@ -95,7 +95,7 @@ public override void Initialize() // Third class wings: var thirdWingOptions = this.CreateThirdClassWingOptions(); - this.CreateWing(36, 4, 3, "Wing of Storm", 150, 60, 220, 400, 0, 3, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions); + this.CreateWing(36, 4, 3, "Wing of Storm", 150, 60, 220, 400, 0, 3, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed); this.CreateWing(37, 4, 3, "Wing of Eternal", 150, 45, 220, 400, 3, 0, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.WizDamage), (0b10, OptionType.Defense)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions); this.CreateWing(38, 4, 3, "Wing of Illusion", 150, 45, 220, 400, 0, 0, 3, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions); this.CreateWing(39, 4, 3, "Wing of Ruin", 150, 55, 220, 400, 0, 0, 0, 3, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.WizDamage)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions); @@ -149,9 +149,9 @@ private void CreateFlameOfCondor() this.GameConfiguration.Items.Add(feather); } - private ItemDefinition CreateWing(byte number, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int levelRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel, IEnumerable possibleOptions, int damageIncreaseInitial, int damageAbsorbInitial, ItemLevelBonusTable damageIncreasePerLevel, ItemOptionDefinition? wingOptionDefinition) + private ItemDefinition CreateWing(byte number, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int levelRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel, IEnumerable possibleOptions, int damageIncreaseInitial, int damageAbsorbInitial, ItemLevelBonusTable damageIncreasePerLevel, ItemOptionDefinition? wingOptionDefinition, float movementSpeed = MovementSpeedConstants.DefaultWingMovementSpeed) { - var wing = this.CreateWing(number, width, height, name, dropLevel, defense, durability, levelRequirement, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, magicGladiatorClassLevel, darkLordClassLevel, summonerClassLevel, ragefighterClassLevel); + var wing = this.CreateWing(number, width, height, name, dropLevel, defense, durability, levelRequirement, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, magicGladiatorClassLevel, darkLordClassLevel, summonerClassLevel, ragefighterClassLevel, movementSpeed); if (wingOptionDefinition != null) { wing.PossibleItemOptions.Add(wingOptionDefinition); @@ -192,7 +192,7 @@ private ItemDefinition CreateWing(byte number, byte width, byte height, string n return wing; } - private ItemDefinition CreateWing(byte number, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int levelRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel) + private ItemDefinition CreateWing(byte number, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int levelRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel, float movementSpeed = MovementSpeedConstants.DefaultWingMovementSpeed) { var wing = this.Context.CreateNew(); this.GameConfiguration.Items.Add(wing); @@ -230,6 +230,9 @@ private ItemDefinition CreateWing(byte number, byte width, byte height, string n canFlyPowerUp.BaseValue = 1; wing.BasePowerUpAttributes.Add(canFlyPowerUp); + wing.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.MaxMovementSpeed, movementSpeed, AggregateType.Maximum)); + wing.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.MaxMovementSpeedUnderwater, movementSpeed, AggregateType.Maximum)); + return wing; } @@ -307,4 +310,4 @@ private IncreasableItemOption CreateWingOption(byte number, AttributeDefinition return itemOption; } -} \ No newline at end of file +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Doppelgaenger3.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Doppelgaenger3.cs index a3769cf59..3d5e08080 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/Doppelgaenger3.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Doppelgaenger3.cs @@ -36,4 +36,11 @@ public Doppelgaenger3(IContext context, GameConfiguration gameConfiguration) /// protected override string MapName => Name; -} \ No newline at end of file + + /// + protected override void AdditionalInitialization(GameMapDefinition mapDefinition) + { + base.AdditionalInitialization(mapDefinition); + this.AddUnderwaterMovementPowerUp(); + } +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/KalimaBase.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/KalimaBase.cs index 215dfc48e..2c74b021d 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/KalimaBase.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/KalimaBase.cs @@ -21,9 +21,16 @@ protected KalimaBase(IContext context, GameConfiguration gameConfiguration) { } + /// + protected override void AdditionalInitialization(GameMapDefinition mapDefinition) + { + base.AdditionalInitialization(mapDefinition); + this.AddUnderwaterMovementPowerUp(); + } + /// protected override IEnumerable CreateNpcSpawns() { yield return this.CreateMonsterSpawn(1, this.NpcDictionary[259], 007, 019, Direction.South); // Oracle Layla } -} \ No newline at end of file +} From 9bb47d075f25a981ee7d011eddb34dd052016add Mon Sep 17 00:00:00 2001 From: itayalroy Date: Mon, 1 Jun 2026 00:53:52 +0300 Subject: [PATCH 5/5] Rename add movement plugin name to use CamelCase --- ...sPlugIn095d.cs => AddMovementSpeedAttributesPlugIn095D.cs} | 4 ++-- src/Persistence/Initialization/Updates/UpdateVersion.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Persistence/Initialization/Updates/{AddMovementSpeedAttributesPlugIn095d.cs => AddMovementSpeedAttributesPlugIn095D.cs} (86%) diff --git a/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095D.cs similarity index 86% rename from src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs rename to src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095D.cs index 83d980c2f..60303aa6e 100644 --- a/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095d.cs +++ b/src/Persistence/Initialization/Updates/AddMovementSpeedAttributesPlugIn095D.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -13,7 +13,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; [PlugIn] [Display(Name = PlugInName, Description = PlugInDescription)] [Guid("7C38C30F-163B-4625-A82D-5C3A0A9ED883")] -public class AddMovementSpeedAttributesPlugIn095d : AddMovementSpeedAttributesPlugInBase +public class AddMovementSpeedAttributesPlugIn095D : AddMovementSpeedAttributesPlugInBase { /// public override UpdateVersion Version => UpdateVersion.AddMovementSpeedAttributes095d; diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index bfd9d6a95..3778ac172 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -429,7 +429,7 @@ public enum UpdateVersion AddMovementSpeedAttributes075 = 84, /// - /// The version of the . + /// The version of the . /// AddMovementSpeedAttributes095d = 85,