diff --git a/Maple2.Database/Context/MetadataContext.cs b/Maple2.Database/Context/MetadataContext.cs index 23f90c7cb..9e5860ccd 100644 --- a/Maple2.Database/Context/MetadataContext.cs +++ b/Maple2.Database/Context/MetadataContext.cs @@ -73,6 +73,7 @@ private static void ConfigureAdditionalEffectMetadata(EntityTypeBuilder effect.Shield).HasJsonConversion(); builder.Property(effect => effect.InvokeEffect).HasJsonConversion(); builder.Property(effect => effect.Skills).HasJsonConversion(); + builder.Property(effect => effect.ModifyOverlapCount).HasJsonConversion(); } private static void ConfigureAnimationMetadata(EntityTypeBuilder builder) { diff --git a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs index 56633714f..315dbd857 100644 --- a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs +++ b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs @@ -41,6 +41,8 @@ protected override IEnumerable Map() { KeepOnEnterPvpZone: data.BasicProperty.doNotClearEffectFromEnterPVPZone, CasterIndividualBuff: data.BasicProperty.casterIndividualEffect, Exp: data.ExpProperty.value, + CooldownTime: (int) TimeSpan.FromSeconds(float.TryParse(data.BasicProperty.coolDownTime, out float cooldown) ? cooldown : 0).TotalMilliseconds, + RideId: data.RideeProperty == null! ? 0 : data.RideeProperty.rideeID, KeepCondition: (BuffKeepCondition) data.BasicProperty.keepCondition, ResetCondition: (BuffResetCondition) data.BasicProperty.resetCondition, DotCondition: (BuffDotCondition) data.BasicProperty.dotCondition), @@ -56,6 +58,12 @@ protected override IEnumerable Map() { Buff: Convert(data.DotBuffProperty)), Shield: Convert(data.ShieldProperty), InvokeEffect: Convert(data.InvokeEffectProperty), + ModifyOverlapCount: data.ModifyOverlapCountProperty == null! ? [] : data.ModifyOverlapCountProperty.effectCodes + .Zip(data.ModifyOverlapCountProperty.offsetCounts) + .Select(tuple => new AdditionalEffectMetadataModifyOverlapCount( + Id: tuple.First, + OffsetCount: tuple.Second)) + .ToArray(), Skills: data.conditionSkill.Concat(data.splashSkill).Select(skill => skill.Convert()).ToArray()); } } diff --git a/Maple2.File.Ingest/Mapper/FunctionCubeMapper.cs b/Maple2.File.Ingest/Mapper/FunctionCubeMapper.cs index 4db63d971..e2849e87c 100644 --- a/Maple2.File.Ingest/Mapper/FunctionCubeMapper.cs +++ b/Maple2.File.Ingest/Mapper/FunctionCubeMapper.cs @@ -19,9 +19,6 @@ protected override IEnumerable Map() { foreach ((int id, FunctionCubeRoot functionCubeRoot) in parser.Parse()) { FunctionCube functionCube = functionCubeRoot.FunctionCube; - if (!Enum.TryParse(functionCube.ControlType, out InteractCubeControlType controlTypet)) { - Console.WriteLine($"Unknown control type: {functionCube.ControlType}"); - } ConfigurableCube? configurableCube = functionCubeRoot.ConfigurableCube; yield return new FunctionCubeMetadata( Id: id, diff --git a/Maple2.File.Ingest/Mapper/RideMapper.cs b/Maple2.File.Ingest/Mapper/RideMapper.cs index b9b127e79..19d7c7073 100644 --- a/Maple2.File.Ingest/Mapper/RideMapper.cs +++ b/Maple2.File.Ingest/Mapper/RideMapper.cs @@ -1,6 +1,7 @@ using Maple2.File.IO; using Maple2.File.Parser; using Maple2.File.Parser.Xml.Riding; +using Maple2.Model.Enum; using Maple2.Model.Metadata; namespace Maple2.File.Ingest.Mapper; @@ -23,7 +24,7 @@ protected override IEnumerable Map() { Id: id, Model: data.basic.kfm, Basic: new RideMetadataBasic( - Type: (int) data.basic.type, // default=0,battle=1,object=2 + Type: Enum.TryParse(data.basic.type.ToString(), true, out RideOnType type) ? type : RideOnType.Default, SkillSetId: data.basic.skillSetID, SummonTime: data.basic.rideSummonCastTime, RunXStamina: data.basic.runXConsumeEp, diff --git a/Maple2.File.Ingest/Mapper/TableMapper.cs b/Maple2.File.Ingest/Mapper/TableMapper.cs index 1c7fe0aa9..c40b719bf 100644 --- a/Maple2.File.Ingest/Mapper/TableMapper.cs +++ b/Maple2.File.Ingest/Mapper/TableMapper.cs @@ -347,7 +347,7 @@ private Dictionary MergeInteractObjectTable(Diction InteractObjectMetadataEffect.ConditionEffect[] ParseConditional(InteractObject.ConditionAdditionalEffect additionalEffect) { if (additionalEffect.id.Length == 0 || additionalEffect.id[0] == 0) { - return Array.Empty(); + return []; } return additionalEffect.id.Zip(additionalEffect.level, (effectId, level) => @@ -356,7 +356,7 @@ InteractObjectMetadataEffect.ConditionEffect[] ParseConditional(InteractObject.C InteractObjectMetadataEffect.InvokeEffect[] ParseInvoke(InteractObject.AdditionalEffect additionalEffect) { if (additionalEffect.invoke.code.Length == 0 || additionalEffect.invoke.code[0] == 0) { - return Array.Empty(); + return []; } return additionalEffect.invoke.code diff --git a/Maple2.File.Ingest/MapperExtensions.cs b/Maple2.File.Ingest/MapperExtensions.cs index 16ba0989a..7a0bec4ed 100644 --- a/Maple2.File.Ingest/MapperExtensions.cs +++ b/Maple2.File.Ingest/MapperExtensions.cs @@ -243,7 +243,7 @@ public static SkillEffectMetadata Convert(this TriggerSkill trigger) { Independent: trigger.independent, Chain: trigger.chain ? new SkillEffectMetadataChain(trigger.chainDistance) : null); } else { - var owner = SkillEntity.Target; + var owner = SkillEntity.Owner; if (trigger.skillOwner > 0 && Enum.IsDefined((SkillEntity) trigger.skillOwner)) { owner = (SkillEntity) trigger.skillOwner; } @@ -352,7 +352,7 @@ public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCond } // We use this default to avoid writing useless checks - private static readonly BeginConditionTarget DefaultBeginConditionTarget = new([], null, [], [], [], new Dictionary(), [], []); + private static readonly BeginConditionTarget DefaultBeginConditionTarget = new([], new BeginConditionTarget.EventCondition(EventConditionType.Activate, false, [], []), [], [], [], new Dictionary(), [], []); private static BeginConditionTarget? Convert(SubConditionTarget? target) { if (target == null) { return null; @@ -413,11 +413,7 @@ BeginConditionTarget.HasBuff[] ParseBuffs(SubConditionTarget data) { // return data.hasSkillID > 0 ? new BeginConditionTarget.HasSkill(data.hasSkillID, data.hasSkillLevel) : null; // } - BeginConditionTarget.EventCondition? ParseEvent(SubConditionTarget data) { - if (data.eventCondition == 0) { - return null; - } - + BeginConditionTarget.EventCondition ParseEvent(SubConditionTarget data) { return new BeginConditionTarget.EventCondition( Type: (EventConditionType) data.eventCondition, IgnoreOwner: data.ignoreOwnerEvent != 0, diff --git a/Maple2.Model/Enum/Buff.cs b/Maple2.Model/Enum/Buff.cs index 5f72e3989..2bd65fcaa 100644 --- a/Maple2.Model/Enum/Buff.cs +++ b/Maple2.Model/Enum/Buff.cs @@ -55,10 +55,10 @@ public enum BuffKeepCondition { } public enum BuffResetCondition { - Reset = 0, + ResetEndTick = 0, PersistEndTick = 1, // end tick does not reset Reset2 = 2, // behaves the same as Reset ?? - Reset3 = 3, // also behaves like Reset ?? + Replace = 3, // Removes old buff and adds a new } public enum BuffDotCondition { @@ -73,3 +73,21 @@ public enum BuffFlag { UpdateBuff = 1, UpdateShield = 2, } + +public enum InvokeEffectType : byte { + ReduceCooldown = 1, + IncreaseSkillDamage = 2, + IncreaseEffectDuration = 3, + IncreaseDotDamage = 5, + // 20 (90050324) + // 21 (90050324) + IncreaseEvasionDebuff = 23, + IncreaseCritEvasionDebuff = 26, + // 34 (90050853) + // 35 (90050854) + // 38 (90050806) + // 40 (90050827) + ReduceSpiritCost = 56, // flame wave 0 spirit cost? value=0, rate=2 on 10300250. subtract rate from cost (10500201, rate=20%) + // 57 (90050351) + IncreaseHealing = 58, +} diff --git a/Maple2.Model/Enum/InvokeEffectType.cs b/Maple2.Model/Enum/InvokeEffectType.cs deleted file mode 100644 index f60646b25..000000000 --- a/Maple2.Model/Enum/InvokeEffectType.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Maple2.Model.Enum; - -public enum InvokeEffectType : byte { - ReduceCooldown = 1, - IncreaseSkillDamage = 2, - IncreaseEffectDuration = 3, - IncreaseDotDamage = 5, - // 20 (90050324) - // 21 (90050324) - IncreaseEvasionDebuff = 23, - IncreaseCritEvasionDebuff = 26, - // 34 (90050853) - // 35 (90050854) - // 38 (90050806) - // 40 (90050827) - ReduceSpiritCost = 56, // flame wave 0 spirit cost? value=0, rate=2 on 10300250. subtract rate from cost (10500201, rate=20%) - // 57 (90050351) - IncreaseHealing = 58, -} diff --git a/Maple2.Model/Enum/RideType.cs b/Maple2.Model/Enum/RideType.cs index 910ac4fd5..feba9f414 100644 --- a/Maple2.Model/Enum/RideType.cs +++ b/Maple2.Model/Enum/RideType.cs @@ -1,10 +1,10 @@ namespace Maple2.Model.Enum; public enum RideOnType : byte { - Default = 0, - UseItem = 1, - AdditionalEffect = 2, - HideAndSeek = 3, + None = 0, + Default = 1, + Battle = 2, + Object = 3, } public enum RideOffType : byte { diff --git a/Maple2.Model/Enum/Skill.cs b/Maple2.Model/Enum/Skill.cs index 003d1f376..498967cb7 100644 --- a/Maple2.Model/Enum/Skill.cs +++ b/Maple2.Model/Enum/Skill.cs @@ -79,8 +79,8 @@ public enum SkillRegion { public enum SkillEntity { None = 0, - Target = 1, - Owner = 2, + Owner = 1, + Target = 2, Caster = 3, PetOwner = 4, Attacker = 5, @@ -131,6 +131,11 @@ public enum SkillEntity { // 20: // 102: Frost Inflicted (10300271) // 103: Target Hit All 9x (11000266) +// ----- +// All: 1,2,4,5,6,7,10,11,13,14,16,17,18,19,20,102,103 +// IgnoreOwner => 1,2,4,5,6,7,10,16,17,18 +// SkillIds => 4,6,7,14,20 +// BuffIds => 16,17,102 public enum EventConditionType { Activate = 0, // always 0 in skills Tick = 0, diff --git a/Maple2.Model/Game/Ride/RideOnAction.cs b/Maple2.Model/Game/Ride/RideOnAction.cs index ad7d76be5..37b50d47c 100644 --- a/Maple2.Model/Game/Ride/RideOnAction.cs +++ b/Maple2.Model/Game/Ride/RideOnAction.cs @@ -25,12 +25,12 @@ public virtual void WriteTo(IByteWriter writer) { } } -public class RideOnActionUseItem : RideOnAction { +public class RideOnActionDefault : RideOnAction { private readonly Item item; public int ItemId => item.Id; public long ItemUid => item.Uid; - public RideOnActionUseItem(int rideId, int objectId, Item item) : base(RideOnType.UseItem, rideId, objectId) { + public RideOnActionDefault(int rideId, int objectId, Item item) : base(RideOnType.Default, rideId, objectId) { this.item = item; } @@ -42,11 +42,11 @@ public override void WriteTo(IByteWriter writer) { } } -public class RideOnActionAdditionalEffect : RideOnAction { +public class RideOnActionBattle : RideOnAction { public readonly int SkillId; public readonly short SkillLevel; - public RideOnActionAdditionalEffect(int rideId, int objectId, int skillId, short skillLevel) : base(RideOnType.AdditionalEffect, rideId, objectId) { + public RideOnActionBattle(int rideId, int objectId, int skillId, short skillLevel) : base(RideOnType.Battle, rideId, objectId) { SkillId = skillId; SkillLevel = skillLevel; } @@ -58,4 +58,4 @@ public override void WriteTo(IByteWriter writer) { } } -public class RideOnActionHideAndSeek(int rideId, int objectId) : RideOnAction(RideOnType.HideAndSeek, rideId, objectId); +public class RideOnActionObject(int rideId, int objectId) : RideOnAction(RideOnType.Object, rideId, objectId); diff --git a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs index 510194e16..6d3b7da73 100644 --- a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs +++ b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs @@ -16,6 +16,7 @@ public record AdditionalEffectMetadata( AdditionalEffectMetadataDot Dot, AdditionalEffectMetadataShield? Shield, AdditionalEffectMetadataInvokeEffect? InvokeEffect, + AdditionalEffectMetadataModifyOverlapCount[] ModifyOverlapCount, SkillEffectMetadata[] Skills); public record AdditionalEffectMetadataProperty( @@ -36,6 +37,8 @@ public record AdditionalEffectMetadataProperty( bool KeepOnEnterPvpZone, bool CasterIndividualBuff, int Exp, + int CooldownTime, + int RideId, BuffKeepCondition KeepCondition, BuffResetCondition ResetCondition, BuffDotCondition DotCondition); @@ -130,3 +133,7 @@ public record AdditionalEffectMetadataInvokeEffect( int EffectGroupId, int SkillId, int SkillGroupId); + +public record AdditionalEffectMetadataModifyOverlapCount( + int Id, + int OffsetCount); diff --git a/Maple2.Model/Metadata/BeginCondition.cs b/Maple2.Model/Metadata/BeginCondition.cs index e8ae2967f..15322133b 100644 --- a/Maple2.Model/Metadata/BeginCondition.cs +++ b/Maple2.Model/Metadata/BeginCondition.cs @@ -36,7 +36,7 @@ public record BeginConditionWeapon( public record BeginConditionTarget( BeginConditionTarget.HasBuff[] Buff, - BeginConditionTarget.EventCondition? Event, + BeginConditionTarget.EventCondition Event, BeginConditionTarget.BeginConditionStat[] Stat, ActorState[] States, ActorSubState[] SubStates, @@ -47,10 +47,6 @@ int[] HasNotBuffIds ) { public record HasBuff(int Id, short Level, bool Owned, int Count, CompareType Compare); - // All: 1,2,4,5,6,7,10,11,13,14,16,17,18,19,20,102,103 - // IgnoreOwner => 1,2,4,5,6,7,10,16,17,18 - // SkillIds => 4,6,7,14,20 - // BuffIds => 16,17,102 public record EventCondition(EventConditionType Type, bool IgnoreOwner, int[] SkillIds, int[] BuffIds); public record BeginConditionStat(BasicAttribute Attribute, float Value, CompareType Compare, CompareStatValueType ValueType); } diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 15d72f504..476fb9589 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -936,6 +936,11 @@ public static class Constant { public const float NpcLastSightRadius = 1800; public const float NpcLastSightHeightUp = 525; public const float NpcLastSightHeightDown = 225; + + public const int RecoveryHPWaitTick = 1000; + public const int RecoverySPWaitTick = 1000; + public const int RecoveryEPWaitTick = 1000; + public const float FallBoundingAddedDistance = 750f; #endregion } diff --git a/Maple2.Model/Metadata/RideMetadata.cs b/Maple2.Model/Metadata/RideMetadata.cs index 742c9da22..db69cd5d9 100644 --- a/Maple2.Model/Metadata/RideMetadata.cs +++ b/Maple2.Model/Metadata/RideMetadata.cs @@ -11,7 +11,7 @@ public record RideMetadata( IReadOnlyDictionary Stats); public record RideMetadataBasic( - int Type, + RideOnType Type, int SkillSetId, float SummonTime, long RunXStamina, diff --git a/Maple2.Server.Game/Commands/BuffCommand.cs b/Maple2.Server.Game/Commands/BuffCommand.cs index 04da4ea90..cf9330c3b 100644 --- a/Maple2.Server.Game/Commands/BuffCommand.cs +++ b/Maple2.Server.Game/Commands/BuffCommand.cs @@ -55,12 +55,17 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int if (all) { foreach (FieldPlayer player in session.Field.Players.Values) { if (remove) { - player.Buffs.Remove(buffId); + player.Buffs.Remove(buffId, session.Player.ObjectId); } else { player.Buffs.AddBuff(session.Player, player, buffId, (short) level, startTick, duration); if (stack > 1) { - player.Buffs.Buffs[buffId].Stack(player.Field.FieldTick, stack); - session.Field?.Broadcast(BuffPacket.Update(player.Buffs.Buffs[buffId])); + List buffs = player.Buffs.EnumerateBuffs(buffId); + foreach (Buff buff in buffs) { + if (buff.Stack(stack)) { + session.Field?.Broadcast(BuffPacket.Update(buff)); + } + } + } } @@ -74,23 +79,30 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int } if (remove) { - player.Buffs.Remove(buffId); + player.Buffs.Remove(buffId, session.Player.ObjectId); return; } player.Buffs.AddBuff(session.Player, player, buffId, (short) level, startTick, duration); if (stack > 1) { - player.Buffs.Buffs[buffId].Stack(player.Field.FieldTick, stack); - session.Field.Broadcast(BuffPacket.Update(player.Buffs.Buffs[buffId])); + List buffs = player.Buffs.EnumerateBuffs(buffId); + foreach (Buff buff in buffs) { + if (buff.Stack(stack)) { + session.Field?.Broadcast(BuffPacket.Update(buff)); + } + } } } else { if (remove) { - session.Player.Buffs.Remove(buffId); + session.Player.Buffs.Remove(buffId, session.Player.ObjectId); return; } session.Player.Buffs.AddBuff(session.Player, session.Player, buffId, (short) level, startTick, duration); if (stack > 1) { - session.Player.Buffs.Buffs[buffId].Stack(session.Field.FieldTick, stack); - session.Field?.Broadcast(BuffPacket.Update(session.Player.Buffs.Buffs[buffId])); + foreach (Buff buff in session.Player.Buffs.EnumerateBuffs(buffId)) { + if (buff.Stack(stack)) { + session.Field.Broadcast(BuffPacket.Update(buff)); + } + } } } diff --git a/Maple2.Server.Game/Commands/KillCommand.cs b/Maple2.Server.Game/Commands/KillCommand.cs index a43d6be68..473143935 100644 --- a/Maple2.Server.Game/Commands/KillCommand.cs +++ b/Maple2.Server.Game/Commands/KillCommand.cs @@ -153,8 +153,7 @@ private static void Kill(GameSession session, FieldNpc npc, SkillMetadata skill) Position = npc.Position, Direction = session.Player.Rotation, }; - var target = new DamageRecordTarget() { - ObjectId = npc.ObjectId, + var target = new DamageRecordTarget(npc) { Position = session.Player.Position, Direction = session.Player.Rotation, }; diff --git a/Maple2.Server.Game/Commands/PlayerCommand.cs b/Maple2.Server.Game/Commands/PlayerCommand.cs index eb459997f..558e72ab8 100644 --- a/Maple2.Server.Game/Commands/PlayerCommand.cs +++ b/Maple2.Server.Game/Commands/PlayerCommand.cs @@ -172,7 +172,7 @@ private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) { session.Player.Value.Character.Job = job; session.Config.Skill.SkillInfo.SetJob(job); - session.Player.Buffs.Buffs.Clear(); + session.Player.Buffs.Clear(); session.Player.Buffs.Initialize(); session.Player.Buffs.LoadFieldBuffs(); session.Stats.Refresh(); diff --git a/Maple2.Server.Game/Manager/Config/BuffManager.cs b/Maple2.Server.Game/Manager/Config/BuffManager.cs index 4eebe79fd..7e0b4bc93 100644 --- a/Maple2.Server.Game/Manager/Config/BuffManager.cs +++ b/Maple2.Server.Game/Manager/Config/BuffManager.cs @@ -1,12 +1,10 @@ using System.Collections.Concurrent; -using Maple2.Database.Extensions; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; using Maple2.Server.Game.Model; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Packets; -using Maple2.Server.Game.Session; using Maple2.Server.Game.Util; using Maple2.Server.World.Service; using Maple2.Tools.Extensions; @@ -25,11 +23,11 @@ public class BuffManager : IUpdatable { private int NextLocalId() => Interlocked.Increment(ref idCounter); #endregion public IActor Actor { get; private set; } - // TODO: Change this to support multiple buffs of the same id, different casters. Possibly also different levels? - public ConcurrentDictionary Buffs { get; } = new(); + private readonly ConcurrentDictionary> buffs = []; public IDictionary> Invokes { get; init; } public IDictionary> Compulsions { get; init; } private Dictionary Resistances { get; } = new(); + public ConcurrentDictionary CooldownTimes { get; } = new(); // TODO: Cache this public ReflectRecord? Reflect; private readonly ILogger logger = Log.ForContext(); @@ -49,8 +47,16 @@ public void Initialize() { public void ResetActor(IActor actor) { Actor = actor; - foreach ((int id, Buff buff) in Buffs) { - buff.ResetActor(actor); + foreach ((int id, List buffDict) in buffs) { + foreach (Buff buff in buffDict) { + buff.ResetActor(actor); + } + } + } + + public void Clear() { + foreach (Buff buff in EnumerateBuffs()) { + Remove(buff.Id, buff.Caster.ObjectId); } } @@ -58,7 +64,7 @@ public void LoadFieldBuffs() { // Lapenshards // Game Events // Prestige - AddMapBuffs(); + EnterField(); if (Actor is FieldPlayer player) { player.Session.Config.RefreshPremiumClubBuffs(); } @@ -70,6 +76,10 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, long start return; } + if (CooldownTimes.TryGetValue(id, out long cooldownTick) && startTick < cooldownTick) { + return; + } + if (durationMs < 0) { durationMs = additionalEffect.Property.DurationTick; } @@ -79,16 +89,30 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, long start return; } - // TODO: Implement AdditionalEffectMetadata.CasterIndividualBuff. - // If true, each caster will have their own buff added to the same Actor. - if (Buffs.TryGetValue(id, out Buff? existing)) { - if (level > existing.Level) { + CooldownTimes[id] = startTick + additionalEffect.Property.CooldownTime; + Buff? existing = GetBuff(id, additionalEffect.Property.CasterIndividualBuff ? caster.ObjectId : 0); + long endTick = startTick + durationMs; - } - if (!existing.Stack(startTick)) { - return; - } - if (notifyField) { + switch (additionalEffect.Property.ResetCondition) { + case BuffResetCondition.ResetEndTick: + case BuffResetCondition.Reset2: // This isn't correct, but it seems to behave VERY close to ResetEndTick + // endTick = startTick + durationMs; + break; + case BuffResetCondition.PersistEndTick: + if (existing != null) { + endTick = existing.EndTick; + } + break; + case BuffResetCondition.Replace: + if (existing != null) { + Remove(existing.Id, existing.Caster.ObjectId); + existing = null; + } + //endTick = startTick + durationMs; + break; + } + if (existing != null) { + if ((existing.UpdateEndTime(endTick) || existing.Stack()) && notifyField) { owner.Field.Broadcast(BuffPacket.Update(existing)); } return; @@ -96,17 +120,15 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, long start // Remove existing buff if it's in the same group if (additionalEffect.Property.Group > 0) { - existing = Buffs.Values.FirstOrDefault(buff => buff.Metadata.Property.Group == additionalEffect.Property.Group); - if (existing != null) { - existing.Disable(); - owner.Field.Broadcast(BuffPacket.Remove(existing)); + foreach (Buff existingBuff in EnumerateBuffs().Where(b => b.Metadata.Property.Group == additionalEffect.Property.Group)) { + Remove(existingBuff.Id, existingBuff.Caster.ObjectId); } } - var buff = new Buff(additionalEffect, NextLocalId(), caster, owner, startTick, durationMs); - if (!Buffs.TryAdd(id, buff)) { - Buffs[id].Stack(startTick); - owner.Field.Broadcast(BuffPacket.Update(buff)); + var buff = new Buff(additionalEffect, NextLocalId(), caster, owner, startTick, endTick); + + if (!TryAdd(buff)) { + logger.Error("Could not add buff {Id} to {Object}", buff.Id, Actor.ObjectId); return; } @@ -116,6 +138,7 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, long start SetCompulsionEvent(buff); SetShield(buff); SetUpdates(buff); + ModifyBuffStackCount(buff); // refresh stats if needed if (buff.Metadata.Status.Values.Any() || buff.Metadata.Status.Rates.Any() || buff.Metadata.Status.SpecialValues.Any() || buff.Metadata.Status.SpecialRates.Any()) { @@ -133,30 +156,71 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, long start } logger.Information("{Id} AddBuff to {ObjectId}: {SkillId},{Level} for {Tick}ms", buff.ObjectId, owner.ObjectId, id, level, buff.EndTick - buff.StartTick); - // Logger.Information("> {Data}", additionalEffect.Property); if (owner is FieldPlayer player) { + if (additionalEffect.Property.Exp > 0) { // Assuming this doesnt proc s + player.Session.Exp.AddStaticExp(additionalEffect.Property.Exp); + } + player.Session.ConditionUpdate(ConditionType.buff, codeLong: buff.Id); player.Session.Dungeon.UpdateDungeonEnterLimit(); } if (notifyField) { owner.Field.Broadcast(BuffPacket.Add(buff)); } + SetMount(buff); } - public bool HasBuff(int effectId, short effectLevel = 1, int overlapCount = 0) { - if (!Buffs.TryGetValue(effectId, out Buff? buff)) { - return false; + private Buff? GetBuff(int buffId, int casterObjectId = 0) { + if (casterObjectId > 0) { + if (this.buffs.TryGetValue(buffId, out List? buffs)) { + return buffs.FirstOrDefault(buff => buff.Caster.ObjectId == casterObjectId); + } + } else { + if (this.buffs.TryGetValue(buffId, out List? buffs)) { + return buffs.FirstOrDefault(); + } + } + return null; + } + + private bool TryAdd(Buff buff) { + if (this.buffs.TryGetValue(buff.Id, out List? buffs)) { + Buff? casterBuff = buffs.FirstOrDefault(b => b.Caster.ObjectId == buff.Caster.ObjectId); + if (casterBuff != null) { + return false; + } + buffs.Add(buff); + return true; } - if (overlapCount != 0 && buff.Stacks < overlapCount) { + return this.buffs.TryAdd(buff.Id, [buff]); + } + + public List EnumerateBuffs() => buffs.Values.SelectMany(list => list).ToList(); + public List EnumerateBuffs(int buffId) => this.buffs.TryGetValue(buffId, out List? buffs) ? buffs : []; + + public bool HasBuff(int effectId, short effectLevel = 1, int stacks = 0) { + List buffs = EnumerateBuffs(effectId); + if (buffs.Count == 0) { return false; } - return effectLevel == 0 || buff.Level >= effectLevel; + foreach (Buff buff in buffs) { + if (stacks != 0 && buff.Stacks < stacks) { + continue; + } + + if (effectLevel != 0 && buff.Level < effectLevel) { + continue; + } + + return true; + } + return false; } public bool HasBuff(BuffEventType eventType) { - return Buffs.Values.Any(buff => buff.Metadata.Property.EventType == eventType); + return EnumerateBuffs().Any(buff => buff.Metadata.Property.EventType == eventType); } private void SetReflect(Buff buff) { @@ -260,6 +324,28 @@ private void SetShield(Buff buff) { } } + private void ModifyBuffStackCount(Buff buff) { + if (buff.Metadata.ModifyOverlapCount.Length <= 0) { + return; + } + foreach (AdditionalEffectMetadataModifyOverlapCount modifyOverlapCount in buff.Metadata.ModifyOverlapCount) { + foreach (Buff buffResult in EnumerateBuffs(modifyOverlapCount.Id).Where(b => b.Stack(modifyOverlapCount.OffsetCount))) { + Actor.Field.Broadcast(BuffPacket.Update(buffResult)); + } + } + } + + private void SetMount(Buff buff) { + if (buff.Metadata.Property.RideId == 0 || Actor is not FieldPlayer player) { + return; + } + + if (!player.Session.Ride.Mount(buff.Metadata)) { + logger.Error("Failed to mount {Id} on {Object}", buff.Id, Actor.ObjectId); + Remove(buff.Id, Actor.ObjectId); + } + } + private void SetUpdates(Buff buff) { if (buff.Metadata.Update.Cancel != null) { CancelBuffs(buff, buff.Metadata.Update.Cancel); @@ -268,66 +354,84 @@ private void SetUpdates(Buff buff) { // Reset skill cooldowns if (buff.Metadata.Update.ResetCooldown.Length > 0 && Actor is FieldPlayer player) { foreach (int skillId in buff.Metadata.Update.ResetCooldown) { - // TODO: Uncomment this when skill cooldown PR is merged - //player.Session.Config.SetSkillCooldown(skillId); + player.Session.Config.SetSkillCooldown(skillId); + } + } + } + + public void TriggerEvent(IActor caster, IActor owner, IActor target, EventConditionType type, int effectSkillId = 0, int buffSkillId = 0) { + foreach (Buff buff in EnumerateBuffs()) { + if (!buff.Enabled) { + continue; } + buff.ApplySkills(caster, owner, target, type, effectSkillId, buffSkillId); } } private void CancelBuffs(Buff buff, AdditionalEffectMetadataUpdate.CancelEffect cancel) { + List<(int buffId, int casterId)> buffsToRemove = []; foreach (int cancelId in cancel.Ids) { - if (Buffs.TryGetValue(cancelId, out Buff? cancelBuff)) { + foreach (Buff buffResult in EnumerateBuffs(cancelId)) { if (cancel.CheckSameCaster) { - if (cancelBuff.Caster != buff.Caster) { + if (buffResult.Caster != buff.Caster) { continue; } } - - Remove(cancelId); + buffsToRemove.Add((buffResult.Id, Actor.ObjectId)); } } foreach (BuffCategory category in cancel.Categories) { - foreach (Buff cancelCategoryBuff in Buffs.Values) { - if (cancelCategoryBuff.Metadata.Property.Category == category) { - Remove(cancelCategoryBuff.Id); - } + foreach (Buff cancelCategoryBuff in EnumerateBuffs().Where(b => b.Metadata.Property.Category == category)) { + buffsToRemove.Add((cancelCategoryBuff.Id, Actor.ObjectId)); } } + Remove(buffsToRemove.ToArray()); + } + + public void ModifyDuration(int buffId, int modifyValue) { + foreach (Buff buff in EnumerateBuffs(buffId)) { + buff.UpdateEndTime(modifyValue); + } } public virtual void Update(long tickCount) { - foreach (Buff buff in Buffs.Values) { + foreach (Buff buff in EnumerateBuffs()) { buff.Update(tickCount); } } public void LeaveField() { + if (Actor.Field == null) { + return; + } + List<(int id, int casterId)> buffsToRemove = []; foreach (MapEntranceBuff buff in Actor.Field.Metadata.EntranceBuffs) { - Remove(buff.Id); + buffsToRemove.Add((buff.Id, Actor.ObjectId)); } - foreach (Buff buff in Buffs.Values) { - if (buff.Metadata.Property.RemoveOnLeaveField) { - Remove(buff.Id); - } + foreach (Buff buff in EnumerateBuffs().Where(b => b.Metadata.Property.RemoveOnLeaveField)) { + buffsToRemove.Add((buff.Id, Actor.ObjectId)); } + Remove(buffsToRemove.ToArray()); } - private void AddMapBuffs() { + private void EnterField() { foreach (MapEntranceBuff buff in Actor.Field.Metadata.EntranceBuffs) { AddBuff(Actor, Actor, buff.Id, buff.Level, Actor.Field.FieldTick); } if (Actor.Field.Metadata.Property.Type == MapType.Pvp) { - foreach (Buff buff in Buffs.Values) { + List<(int id, int casterId)> buffsToRemove = []; + foreach (Buff buff in EnumerateBuffs()) { if (buff.Metadata.Property.RemoveOnPvpZone) { - Remove(buff.Id); + buffsToRemove.Add((buff.Id, Actor.ObjectId)); } if (!buff.Metadata.Property.KeepOnEnterPvpZone) { - Remove(buff.Id); + buffsToRemove.Add((buff.Id, Actor.ObjectId)); } } + Remove(buffsToRemove.ToArray()); } if (Actor.Field.Metadata.Property.Region == MapRegion.ShadowWorld) { @@ -353,19 +457,22 @@ public void AddItemBuffs(Item item) { } public void RemoveItemBuffs(Item item) { + List<(int id, int casterId)> buffsToRemove = []; foreach (ItemMetadataAdditionalEffect buff in item.Metadata.AdditionalEffects) { - Remove(buff.Id); + buffsToRemove.Add((buff.Id, Actor.ObjectId)); } if (item.Socket != null) { foreach (ItemGemstone? gem in item.Socket.Sockets) { if (gem != null && Actor.Field.ItemMetadata.TryGet(gem.ItemId, out ItemMetadata? metadata)) { foreach (ItemMetadataAdditionalEffect buff in metadata.AdditionalEffects) { - Remove(buff.Id); + buffsToRemove.Add((buff.Id, Actor.ObjectId)); } } } } + + Remove(buffsToRemove.ToArray()); } public void SetCacheBuffs(IList buffs, long currentTick) { @@ -385,37 +492,69 @@ public void SetCacheBuffs(IList buffs, long currentTick) { } } - public bool Remove(int id) { + public void Remove(params (int id, int casterId)[] buffIds) { + foreach ((int id, int casterId) in buffIds) { + Remove(id, casterId); + } + } + + public bool Remove(int id, int casterId) { //TODO: Check if buff is removable/should be removed - if (!Buffs.Remove(id, out Buff? buff)) { - return false; + bool refreshStats = false; + List buffsToRemove = []; + foreach (Buff buff in EnumerateBuffs(id)) { + if (buff.Metadata.Property.CasterIndividualBuff && buff.Caster.ObjectId != casterId) { + continue; + } + + buffsToRemove.Add(buff); + foreach ((BasicAttribute attribute, float value) in buff.Metadata.Status.Resistances) { + Resistances[attribute] = Math.Max(0, Resistances[attribute] - value); + } + + if (buff.Metadata.Status.Values.Count > 0 || buff.Metadata.Status.Rates.Count > 0 || buff.Metadata.Status.SpecialValues.Count > 0 || buff.Metadata.Status.SpecialRates.Count > 0) { + if (Actor is FieldPlayer player) { + refreshStats = true; + } + } } if (Reflect?.SourceBuffId == id) { Reflect = null; } - foreach ((BasicAttribute attribute, float value) in buff.Metadata.Status.Resistances) { - Resistances[attribute] = Math.Min(0, Resistances[attribute] - value); - } Invokes.RemoveAll(id); Compulsions.RemoveAll(id); - - if (buff.Metadata.Status.Values.Count > 0 || buff.Metadata.Status.Rates.Count > 0 || buff.Metadata.Status.SpecialValues.Count > 0 || buff.Metadata.Status.SpecialRates.Count > 0) { - if (Actor is FieldPlayer player) { - player.Session.Stats.Refresh(); + foreach (Buff buff in buffsToRemove) { + RemoveInternal(buff); + Actor.Field.Broadcast(BuffPacket.Remove(buff)); + if (buff.Metadata.Property.RideId > 0) { + refreshStats = true; + if (Actor is FieldPlayer player && player.Session.Ride.Ride?.Action is RideOnActionBattle battle && battle.SkillId == buff.Id) { + player.Session.Ride.Dismount(RideOffType.AdditionalEffect); + } } } + if (refreshStats) { + Actor.Stats.Refresh(); + } - Actor.Field.Broadcast(BuffPacket.Remove(buff)); return true; + + void RemoveInternal(Buff buffToRemove) { + if (buffs.TryGetValue(buffToRemove.Id, out List? buffList)) { + if (!buffList.Remove(buffToRemove)) { + logger.Error("Failed to remove buff {Id} from {Object}", buffToRemove.Id, Actor.ObjectId); + } + } + } } public void OnDeath() { - foreach (Buff buff in Buffs.Values) { + foreach (Buff buff in EnumerateBuffs()) { if (!buff.Metadata.Property.KeepOnDeath) { - Remove(buff.Id); + Remove(buff.Id, Actor.ObjectId); continue; } buff.UpdateEnabled(); @@ -423,7 +562,7 @@ public void OnDeath() { } private bool CheckImmunity(int newBuffId, BuffCategory category) { - foreach ((int id, Buff buff) in Buffs) { + foreach (Buff buff in EnumerateBuffs()) { if (buff.Metadata.Update.ImmuneIds.Contains(newBuffId) || buff.Metadata.Update.ImmuneCategories.Contains(category)) { return false; } @@ -433,11 +572,13 @@ private bool CheckImmunity(int newBuffId, BuffCategory category) { } public List GetSaveCacheBuffs() { - return Buffs.Values.Where(buff => !buff.Metadata.Property.RemoveOnLogout).ToList(); + return EnumerateBuffs() + .Where(buff => !buff.Metadata.Property.RemoveOnLogout) + .ToList(); } public void UpdateEnabled() { - foreach (Buff buff in Buffs.Values) { + foreach (Buff buff in EnumerateBuffs()) { buff.UpdateEnabled(); } } diff --git a/Maple2.Server.Game/Manager/Config/ConfigManager.cs b/Maple2.Server.Game/Manager/Config/ConfigManager.cs index 15586552b..2cc858d81 100644 --- a/Maple2.Server.Game/Manager/Config/ConfigManager.cs +++ b/Maple2.Server.Game/Manager/Config/ConfigManager.cs @@ -182,7 +182,7 @@ public void SaveSkillCooldown(SkillMetadata skill, long startTick) { } } - public void SetSkillCooldown(int skillId, short level, int endTick = 0) { + public void SetSkillCooldown(int skillId, short level = 1, int endTick = 0) { var cooldown = new SkillCooldown(skillId, level) { EndTick = endTick, }; diff --git a/Maple2.Server.Game/Manager/Config/SkillManager.cs b/Maple2.Server.Game/Manager/Config/SkillManager.cs index e5e222d6e..7f4331488 100644 --- a/Maple2.Server.Game/Manager/Config/SkillManager.cs +++ b/Maple2.Server.Game/Manager/Config/SkillManager.cs @@ -51,12 +51,12 @@ public void UpdatePassiveBuffs(bool notifyField = true) { logger.Information("Applying passive skill {Name}: {SkillId},{Level}", metadata.Name, metadata.Id, metadata.Level); foreach (SkillEffectMetadata effect in metadata.Data.Skills) { - if (effect.Condition is not { Target: SkillEntity.Target }) { + if (effect.Condition is not { Target: SkillEntity.Owner }) { Console.WriteLine($"Skipping effect {effect}"); continue; } - session.Player.ApplyEffect(session.Player, session.Player, effect, notifyField); + session.Player.ApplyEffect(session.Player, session.Player, effect, session.Player.Field.FieldTick, EventConditionType.Activate, skillId: metadata.Id, notifyField: notifyField); } } } diff --git a/Maple2.Server.Game/Manager/ExperienceManager.cs b/Maple2.Server.Game/Manager/ExperienceManager.cs index d5983513e..ff478e2aa 100644 --- a/Maple2.Server.Game/Manager/ExperienceManager.cs +++ b/Maple2.Server.Game/Manager/ExperienceManager.cs @@ -150,6 +150,16 @@ public long AddExp(ExpType expType, float modifier = 1f, long additionalExp = 0) return AddExp((long) ((expValue * modifier) * entry.Factor) + additionalExp, expType.Message()); } + public void AddStaticExp(long amount) { + if (amount <= 0) { + return; + } + Exp += amount; + session.Send(ExperienceUpPacket.Add(amount, Exp, RestExp, ExpMessageCode.s_msg_take_exp)); + session.ConditionUpdate(ConditionType.exp, counter: amount); + LevelUp(); + } + public void AddMobExp(int moblevel, float modifier = 1f, long additionalExp = 0) { if (!session.TableMetadata.ExpTable.ExpBase.TryGetValue(2, out IReadOnlyDictionary? expBase)) { return; @@ -179,7 +189,7 @@ public bool LevelUp() { session.Field?.Broadcast(LevelUpPacket.LevelUp(session.Player)); session.ConditionUpdate(ConditionType.level_up, codeLong: (int) session.Player.Value.Character.Job.Code(), targetLong: Level); session.ConditionUpdate(ConditionType.level, targetLong: Level); - + session.Config.UpdateDeathPenalty(0); session.Stats.Refresh(); session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index fb289a519..af7a3cd79 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -476,12 +476,12 @@ public void AddSkill(SkillRecord record) { public IEnumerable GetTargets(Prism[] prisms, SkillEntity entity, int limit, ICollection? ignore = null) { switch (entity) { - case SkillEntity.Owner: + case SkillEntity.Target: case SkillEntity.Attacker: case SkillEntity.RegionBuff: case SkillEntity.RegionDebuff: return prisms.Filter(Players.Values, limit, ignore); - case SkillEntity.Target: + case SkillEntity.Owner: return prisms.Filter(Mobs.Values, limit, ignore); case SkillEntity.RegionPet: return prisms.Filter(Pets.Values.Where(pet => pet.OwnerId == 0), limit, ignore); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 60be4b827..92736bfa0 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -47,6 +47,7 @@ public partial class FieldManager : IField { public TableMetadataStorage TableMetadata { get; init; } = null!; public FunctionCubeMetadataStorage FunctionCubeMetadata { get; init; } = null!; public ServerTableMetadataStorage ServerTableMetadata { get; init; } = null!; + public RideMetadataStorage RideMetadata { get; init; } = null!; public ItemStatsCalculator ItemStatsCalc { get; init; } = null!; public Lua.Lua Lua { get; init; } = null!; public Factory FieldFactory { get; init; } = null!; @@ -293,6 +294,7 @@ public void EnsurePlayerPosition(FieldPlayer player) { return; } + player.FallDamage(Constant.FallBoundingAddedDistance); player.MoveToPosition(player.LastGroundPosition.Align() + new Vector3(0, 0, 150f), default); } diff --git a/Maple2.Server.Game/Manager/Items/EquipManager.cs b/Maple2.Server.Game/Manager/Items/EquipManager.cs index 46c548d2e..89b2c2bb5 100644 --- a/Maple2.Server.Game/Manager/Items/EquipManager.cs +++ b/Maple2.Server.Game/Manager/Items/EquipManager.cs @@ -305,6 +305,7 @@ private bool UnequipInternal(EquipSlot slot, bool isSkin, short inventorySlot = } session.Field?.Broadcast(EquipPacket.UnequipItem(session.Player, unequipItem)); + session.Buffs.RemoveItemBuffs(unequipItem); return true; } diff --git a/Maple2.Server.Game/Manager/RideManager.cs b/Maple2.Server.Game/Manager/RideManager.cs new file mode 100644 index 000000000..c8c60fda4 --- /dev/null +++ b/Maple2.Server.Game/Manager/RideManager.cs @@ -0,0 +1,129 @@ +using Maple2.Database.Extensions; +using Maple2.Database.Storage; +using Maple2.Model; +using Maple2.Model.Enum; +using Maple2.Model.Error; +using Maple2.Model.Game; +using Maple2.Model.Metadata; +using Maple2.Server.Core.Packets; +using Maple2.Server.Core.Sync; +using Maple2.Server.Game.Manager.Field; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; +using Maple2.Server.Game.Util.Sync; +using Serilog; + +namespace Maple2.Server.Game.Manager; + +public sealed class RideManager { + private readonly GameSession session; + public Ride? Ride { get; set; } + public RideOnType RideType => Ride?.Action.Type ?? RideOnType.None; + + + private readonly ILogger logger = Log.Logger.ForContext(); + + public RideManager(GameSession session) { + this.session = session; + } + + public bool Mount(Item item) { + if (Ride != null) { + return false; + } + + if (!session.RideMetadata.TryGet(item.Metadata.Property.Ride, out RideMetadata? metadata)) { + session.Send(NoticePacket.MessageBox(StringCode.s_item_invalid_function_item)); + return false; + } + + if (item.Metadata.Limit.TransferType == TransferType.BindOnUse) { + session.Item.Bind(item); + } + + int objectId = FieldManager.NextGlobalId(); + var action = new RideOnActionDefault(item.Metadata.Property.Ride, objectId, item); + Ride = new Ride(session.Player.ObjectId, metadata, action); + session.Field.Broadcast(RidePacket.Start(Ride)); + return true; + } + + public bool Mount(AdditionalEffectMetadata additionalEffectMetadata) { + if (Ride != null) { + return false; + } + + if (!session.Field.RideMetadata.TryGet(additionalEffectMetadata.Property.RideId, out RideMetadata? metadata)) { + return false; + } + + var action = new RideOnActionBattle(metadata.Id, FieldManager.NextGlobalId(), additionalEffectMetadata.Id, additionalEffectMetadata.Level); + + Ride = new Ride(session.Player.ObjectId, metadata, action); + session.Field.Broadcast(RidePacket.Start(Ride)); + if (Ride.Metadata.Stats.Count > 0) { + session.Stats.SetBattleMountStats(Ride.Metadata.Stats); + session.Field.Broadcast(StatsPacket.Init(session.Player)); + } + return true; + + } + + public void Dismount(RideOffType type, bool forced = false) { + if (Ride == null) { + return; + } + + RideOffAction rideOffAction = GetRideOffAction(type, forced); + session.Field.Broadcast(RidePacket.Stop(Ride.OwnerId, rideOffAction)); + + Ride ride = Ride; + Ride = null; + + if (ride.Action is RideOnActionBattle battle) { + session.Buffs.Remove(battle.SkillId, session.Player.ObjectId); + } + if (ride.Metadata.Stats.Count > 0) { + session.Stats.Refresh(); + } + session.Config.LoadKeyTable(); + } + + private RideOffAction GetRideOffAction(RideOffType type, bool forced = false) { + if (forced) { + return new RideOffAction(forced); + } + + return type switch { + RideOffType.Default => new RideOffAction(forced), + RideOffType.UseSkill => new RideOffActionUseSkill(), + RideOffType.Interact => new RideOffActionInteract(), + RideOffType.Taxi => new RideOffActionTaxi(), + RideOffType.CashCall => new RideOffActionCashCall(), + RideOffType.BeautyShop => new RideOffActionBeautyShop(), + RideOffType.TakeLr => new RideOffActionTakeLr(), + RideOffType.Hold => new RideOffActionHold(), + RideOffType.Recall => new RideOffActionRecall(), + RideOffType.SummonPetOn => new RideOffActionSummonPetOn(), + RideOffType.SummonPetTransfer => new RideOffActionSummonPetTransfer(), + RideOffType.HomeConvenient => new RideOffActionHomeConvenient(), + RideOffType.DisableField => new RideOffActionDisableField(), + RideOffType.Dead => new RideOffActionDead(), + RideOffType.AdditionalEffect => new RideOffActionAdditionalEffect(), + RideOffType.RidingUi => new RideOffActionRidingUi(), + RideOffType.Homemade => new RideOffActionHomemade(), + RideOffType.AutoInteraction => new RideOffActionAutoInteraction(), + RideOffType.AutoClimb => new RideOffActionAutoClimb(), + RideOffType.CoupleEmotion => new RideOffActionCoupleEmotion(), + //RideOffType.React => new RideOffActionReact(), + RideOffType.UseFunctionItem => new RideOffActionUseFunctionItem(), + RideOffType.Nurturing => new RideOffActionNurturing(), + RideOffType.Groggy => new RideOffActionGroggy(), + RideOffType.UnRideSkill => new RideOffActionUnRideSkill(), + RideOffType.UseGlideItem => new RideOffActionUseGlideItem(), + RideOffType.HideAndSeek => new RideOffActionHideAndSeek(), + _ => new RideOffAction(forced), + }; + } +} diff --git a/Maple2.Server.Game/Manager/StatsManager.cs b/Maple2.Server.Game/Manager/StatsManager.cs index 355052f36..bbaf53382 100644 --- a/Maple2.Server.Game/Manager/StatsManager.cs +++ b/Maple2.Server.Game/Manager/StatsManager.cs @@ -35,6 +35,10 @@ public StatsManager(IActor actor, UserStatTable userStatsMetadata) { Values = new Stats(metadata, player.Value.Character.Job.Code()); } + public void SetBattleMountStats(IReadOnlyDictionary stats) { + Values.SetStaticStats(stats); + } + public StatsManager(IActor actor) { this.Actor = actor; levelStats = new Dictionary>(); @@ -108,6 +112,11 @@ public void Refresh() { return; } + // Do not refresh stats if player is mounted + if (player.Session.Ride.RideType is RideOnType.Battle or RideOnType.Object) { + return; + } + Character character = player.Value.Character; if (levelStats.TryGetValue(character.Level, out IReadOnlyDictionary? metadata)) { Values.Reset(metadata, character.Job.Code()); @@ -163,7 +172,7 @@ private void AddEquips(FieldPlayer player) { } private void AddBuffs(FieldPlayer player) { - foreach (Buff buff in player.Buffs.Buffs.Values) { + foreach (Buff buff in player.Buffs.EnumerateBuffs()) { foreach ((BasicAttribute valueBasicAttribute, long value) in buff.Metadata.Status.Values) { Values[valueBasicAttribute].AddTotal(value); } @@ -180,7 +189,7 @@ private void AddBuffs(FieldPlayer player) { } private void StatConversion(FieldPlayer player) { - foreach (Buff buff in player.Buffs.Buffs.Values) { + foreach (Buff buff in player.Buffs.EnumerateBuffs()) { if (buff.Metadata.Status.Conversion != null) { float sum = Values[buff.Metadata.Status.Conversion.BaseAttribute].Total; sum *= buff.Metadata.Status.Conversion.Rate; diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 1e7e37a24..10736f245 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -73,11 +73,10 @@ public void Dispose() { protected virtual void Dispose(bool disposing) { } - public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata effect, bool notifyField = true) { + public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata effect, long startTick, EventConditionType type = EventConditionType.Activate, int skillId = 0, int buffId = 0, bool notifyField = true) { Debug.Assert(effect.Condition != null); - foreach (SkillEffectMetadata.Skill skill in effect.Skills) { - Buffs.AddBuff(caster, owner, skill.Id, skill.Level, Field.FieldTick, notifyField: notifyField); + Buffs.AddBuff(caster, owner, skill.Id, skill.Level, startTick, notifyField: notifyField); } } @@ -86,8 +85,7 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat return; } - var targetRecord = new DamageRecordTarget { - ObjectId = ObjectId, + var targetRecord = new DamageRecordTarget(this) { Position = caster.Position, Direction = caster.Rotation, // Idk why this is wrong }; @@ -108,7 +106,7 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat if (damageAmount != 0) { long positiveDamage = damageAmount * -1; if (!DamageDealers.TryGetValue(caster.ObjectId, out DamageRecordTarget? record)) { - record = new DamageRecordTarget(); + record = new DamageRecordTarget(this); DamageDealers.TryAdd(caster.ObjectId, record); } record.AddDamage(DamageType.Normal, positiveDamage); @@ -116,6 +114,23 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat Field.Broadcast(StatsPacket.Update(this, BasicAttribute.Health)); } + foreach ((DamageType damageType, long amount) in targetRecord.Damage) { + switch (damageType) { + case DamageType.Critical: + caster.Buffs.TriggerEvent(caster, caster, this, EventConditionType.OnOwnerAttackHit, effectSkillId: damage.SkillId); + caster.Buffs.TriggerEvent(caster, caster, this, EventConditionType.OnOwnerAttackCrit, effectSkillId: damage.SkillId); + break; + case DamageType.Normal: + caster.Buffs.TriggerEvent(caster, caster, this, EventConditionType.OnOwnerAttackHit, effectSkillId: damage.SkillId); + break; + case DamageType.Block: + break; + case DamageType.Miss: + caster.Buffs.TriggerEvent(caster, caster, this, EventConditionType.OnAttackMiss, effectSkillId: damage.SkillId); + break; + } + } + damage.Targets.Add(targetRecord); } @@ -131,7 +146,7 @@ public virtual void Reflect(IActor target) { record.Counter++; if (record.Counter >= record.Metadata.Count) { - Buffs.Remove(record.SourceBuffId); + Buffs.Remove(record.SourceBuffId, ObjectId); } target.Buffs.AddBuff(this, target, record.Metadata.EffectId, record.Metadata.EffectLevel, Field.FieldTick); @@ -178,12 +193,6 @@ public virtual void TargetAttack(SkillRecord record) { } } - public virtual void ApplyEffect(IActor caster, IActor target, SkillEffectMetadata effect, long startTick) { - foreach (SkillEffectMetadata.Skill skill in effect.Skills) { - Buffs.AddBuff(caster, target, skill.Id, skill.Level, startTick); - } - } - private IActor GetTarget(SkillEntity entity, IActor caster, IActor target) { return entity switch { SkillEntity.Target => target, @@ -234,5 +243,7 @@ public virtual void KeyframeEvent(string keyName) { } return record; } - protected abstract void OnDeath(); + protected virtual void OnDeath() { + Buffs.TriggerEvent(this, this, this, EventConditionType.OnDeath); + } } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs index cf3c5ca35..7be3d2bd6 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs @@ -520,7 +520,7 @@ private void ProcessNode(BuffNode node) { } if (node.Type == NodeBuffType.Remove) { - target.Buffs.Remove(node.Id); + target.Buffs.Remove(node.Id, actor.ObjectId); return; } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs index 88011b06f..54823c9d1 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs @@ -66,9 +66,7 @@ public override void ApplyDamage(IActor caster, DamageRecord damage, SkillMetada return; } - var targetRecord = new DamageRecordTarget { - ObjectId = ObjectId, - }; + var targetRecord = new DamageRecordTarget(this); int damageAmount = TamingPoint - Math.Min(TamingPoint + attack.Pet.TamingPoint, Constant.TamingPetMaxPoint); TamingPoint -= damageAmount; targetRecord.AddDamage(damageAmount == 0 ? DamageType.Miss : DamageType.Normal, damageAmount); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index d76fb7a78..b71483f14 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -160,32 +160,41 @@ public override void Update(long tickCount) { InBattle = false; } - // Loops through each registered regen stat and applies regen - var statsToRemove = new List(); - foreach (BasicAttribute attribute in regenStats.Keys) { - Stat stat = Stats.Values[attribute]; - Stat regen = Stats.Values[regenStats[attribute].Item1]; - Stat interval = Stats.Values[regenStats[attribute].Item2]; - - if (stat.Current >= stat.Total) { - // Removes stat from regen stats so it won't be listened for - statsToRemove.Add(attribute); - continue; - } - - lastRegenTime.TryGetValue(attribute, out long regenTime); - - if (tickCount - regenTime > interval.Total) { - lastRegenTime[attribute] = tickCount; - if (attribute == BasicAttribute.Health) { - RecoverHp((int) regen.Total); + if (!IsDead) { + // Loops through each registered regen stat and applies regen + var statsToRemove = new List(); + foreach (BasicAttribute attribute in regenStats.Keys) { + Stat stat = Stats.Values[attribute]; + Stat regen = Stats.Values[regenStats[attribute].Item1]; + Stat interval = Stats.Values[regenStats[attribute].Item2]; + + if (stat.Current >= stat.Total) { + // Removes stat from regen stats so it won't be listened for + statsToRemove.Add(attribute); continue; } - Session.Send(StatsPacket.Update(this, attribute)); + + lastRegenTime.TryGetValue(attribute, out long regenTime); + + if (tickCount - regenTime > interval.Base) { + lastRegenTime[attribute] = tickCount; + switch (attribute) { + case BasicAttribute.Health: + RecoverHp((int) regen.Total); + continue; + case BasicAttribute.Spirit: + RecoverSp((int) regen.Total); + continue; + case BasicAttribute.Stamina: + RecoverStamina((int) regen.Total); + continue; + } + Session.Send(StatsPacket.Update(this, attribute)); + } + } + foreach (BasicAttribute attribute in statsToRemove) { + regenStats.Remove(attribute); } - } - foreach (BasicAttribute attribute in statsToRemove) { - regenStats.Remove(attribute); } Session.GameEvent.Update(tickCount); @@ -365,6 +374,7 @@ public bool Revive(bool instant = false) { } Buffs.UpdateEnabled(); + CheckRegen(); return true; } @@ -407,8 +417,11 @@ public void ConsumeHp(int amount) { stat.Add(-amount); Session.Send(StatsPacket.Update(this, BasicAttribute.Health)); - if (stat.Current > 0 && !regenStats.ContainsKey(BasicAttribute.Health)) { - regenStats.Add(BasicAttribute.Health, new Tuple(BasicAttribute.HpRegen, BasicAttribute.HpRegenInterval)); + if (!IsDead) { + if (!regenStats.ContainsKey(BasicAttribute.Health)) { + regenStats.Add(BasicAttribute.Health, new Tuple(BasicAttribute.HpRegen, BasicAttribute.HpRegenInterval)); + } + lastRegenTime[BasicAttribute.Health] = Field.FieldTick + Constant.RecoveryHPWaitTick; } Session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { @@ -449,8 +462,11 @@ public void ConsumeSp(int amount) { Stats.Values[BasicAttribute.Spirit].Add(-amount); - if (!regenStats.ContainsKey(BasicAttribute.Spirit)) { - regenStats.Add(BasicAttribute.Spirit, new Tuple(BasicAttribute.SpRegen, BasicAttribute.SpRegenInterval)); + if (!IsDead) { + if (!regenStats.ContainsKey(BasicAttribute.Spirit)) { + regenStats.Add(BasicAttribute.Spirit, new Tuple(BasicAttribute.SpRegen, BasicAttribute.SpRegenInterval)); + } + // lastRegenTime[BasicAttribute.Spirit] = Field.FieldTick + Constant.RecoverySPWaitTick; - Not applicable for SP? } } @@ -464,7 +480,7 @@ public void RecoverStamina(int amount) { } Stat stat = Stats.Values[BasicAttribute.Stamina]; - if (stat.Total < stat.Base) { + if (stat.Current < stat.Total) { Stats.Values[BasicAttribute.Stamina].Add(amount); Session.Send(StatsPacket.Update(this, BasicAttribute.Stamina)); } @@ -482,8 +498,11 @@ public void ConsumeStamina(int amount, bool noRegen = false) { Stats.Values[BasicAttribute.Stamina].Add(-amount); - if (!regenStats.ContainsKey(BasicAttribute.Stamina) && !noRegen) { - regenStats.Add(BasicAttribute.Stamina, new Tuple(BasicAttribute.StaminaRegen, BasicAttribute.StaminaRegenInterval)); + if (!IsDead) { + if (!regenStats.ContainsKey(BasicAttribute.Stamina) && !noRegen) { + regenStats.Add(BasicAttribute.Stamina, new Tuple(BasicAttribute.StaminaRegen, BasicAttribute.StaminaRegenInterval)); + } + lastRegenTime[BasicAttribute.Stamina] = Field.FieldTick + Constant.RecoveryEPWaitTick; } } @@ -526,5 +545,30 @@ public void MoveToPortal(FieldPortal portal) { Session.Send(PortalPacket.MoveByPortal(this, portal.Position, portal.Rotation)); } + + public void FallDamage(float distance) { + double distanceScalingFactor = 0.04813; // base distance scaling factor + double hpRatioExponent = 1.087; // HP ratio exponent for diminishing returns + double currentHp = Stats.Values[BasicAttribute.Health].Current; + double maxHp = Stats.Values[BasicAttribute.Health].Total; + double distanceFactor = distanceScalingFactor * Math.Exp(0.0046 * distance); + double hpRatio = currentHp / maxHp; + double hpScaling = Math.Pow(hpRatio, hpRatioExponent); + + double damageD = currentHp * distanceFactor * hpScaling; + damageD = Math.Min(currentHp * 0.25, damageD); + int damage = (int) damageD; + if (damage > 0) { + ConsumeHp(damage); + Field.Broadcast(StatsPacket.Update(this, BasicAttribute.Health)); + Session.Send(FallDamagePacket.FallDamage(ObjectId, damage)); + Session.ConditionUpdate(ConditionType.fall_damage, targetLong: damage); + if (!IsDead) { + Session.ConditionUpdate(ConditionType.fall_survive); + } else { + Session.ConditionUpdate(ConditionType.fall_die); + } + } + } } diff --git a/Maple2.Server.Game/Model/Field/Actor/IActor.cs b/Maple2.Server.Game/Model/Field/Actor/IActor.cs index ea1a4c085..1e7d4c2da 100644 --- a/Maple2.Server.Game/Model/Field/Actor/IActor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/IActor.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using Maple2.Database.Storage; +using Maple2.Model.Enum; using Maple2.Model.Metadata; using Maple2.Server.Game.Manager; using Maple2.Server.Game.Manager.Config; @@ -21,7 +22,7 @@ public interface IActor : IFieldEntity { public bool IsDead { get; } public IPrism Shape { get; } - public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata effect, long startTick) { } + public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata effect, long startTick, EventConditionType type = EventConditionType.Activate, int skillId = 0, int buffId = 0, bool notifyField = true) { } public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadataAttack attack) { } public virtual void TargetAttack(SkillRecord record) { } diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 44e8dae92..a22fffb78 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -38,17 +38,20 @@ public class Buff : IUpdatable, IByteSerializable { private readonly ILogger logger = Log.ForContext(); - public Buff(AdditionalEffectMetadata metadata, int objectId, IActor caster, IActor owner, long startTick, int durationMs) { + public Buff(AdditionalEffectMetadata metadata, int objectId, IActor caster, IActor owner, long startTick, long endTick) { Metadata = metadata; ObjectId = objectId; Caster = caster; Owner = owner; + StartTick = startTick; + EndTick = endTick; + // Buffs with IntervalTick=0 will just proc a single time IntervalTick = metadata.Property.IntervalTick > 0 ? metadata.Property.IntervalTick : metadata.Property.DurationTick + 1000; - Stack(startTick, durationMs: durationMs); + Stack(); NextProcTick = startTick + Metadata.Property.DelayTick + Metadata.Property.IntervalTick; UpdateEnabled(false); canProc = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration; @@ -64,20 +67,39 @@ public void ResetActor(IActor actor) { } } - public bool Stack(long startTick, int amount = 1, int durationMs = 0) { - Stacks = Math.Min(Stacks + amount, Metadata.Property.MaxCount); - StartTick = startTick; + public bool UpdateEndTime(long endTick) { + if (endTick == EndTick) { + return false; + } + EndTick = endTick; + return true; + } + + public bool Stack(int amount = 1) { + int currentStacks = Stacks; + + // Ensure we don't go below 0 + int adjustedAmount = Math.Max(0, Stacks - amount); + if (adjustedAmount == 0) { + return false; + } - if (Stacks == 1 || Metadata.Property.ResetCondition != BuffResetCondition.PersistEndTick) { - EndTick = StartTick + durationMs; + Stacks = Math.Min(Stacks + adjustedAmount, Metadata.Property.MaxCount); + if (Stacks == currentStacks) { + return false; } + + if (Stacks >= Metadata.Property.MaxCount) { + Owner.Buffs.TriggerEvent(Owner, Owner, Owner, EventConditionType.OnBuffStacksReached, buffSkillId: Id); + } + return true; } public void RemoveStack(int amount = 1) { Stacks = Math.Max(0, Stacks - amount); if (Stacks == 0) { - Owner.Buffs.Remove(Id); + Owner.Buffs.Remove(Id, Caster.ObjectId); } } @@ -89,7 +111,7 @@ public virtual void Update(long tickCount) { continue; } - Owner.Buffs.Remove(id); + Owner.Buffs.Remove(id, Caster.ObjectId); } } @@ -97,7 +119,7 @@ public virtual void Update(long tickCount) { } if (canExpire && !canProc && tickCount > EndTick) { - Owner.Buffs.Remove(Id); + Owner.Buffs.Remove(Id, Caster.ObjectId); return; } @@ -139,15 +161,28 @@ private void Proc() { ApplyCancel(); ModifyDuration(); + NextProcTick += IntervalTick; + if (NextProcTick > EndTick) { + canProc = false; + } + } + + public void ApplySkills(IActor caster, IActor owner, IActor target, EventConditionType type = EventConditionType.Activate, int skillId = 0, int buffId = 0) { foreach (SkillEffectMetadata effect in Metadata.Skills) { if (effect.Condition != null) { // logger.Error("Buff Condition-Effect unimplemented from {Id} on {Owner}", Id, Owner.ObjectId); + if (effect.Condition.Condition.Check(caster, owner, target, type, skillId, buffId)) { + continue; + } switch (effect.Condition.Target) { - case SkillEntity.Target: - Owner.ApplyEffect(Caster, Owner, effect, Field.FieldTick); + case SkillEntity.Owner: + owner.ApplyEffect(caster, owner, effect, Field.FieldTick, type, skillId, buffId); break; case SkillEntity.Caster: - Caster.ApplyEffect(Caster, Owner, effect, Field.FieldTick); + caster.ApplyEffect(caster, caster, effect, Field.FieldTick, type, skillId, buffId); + break; + case SkillEntity.Target: + target.ApplyEffect(caster, target, effect, Field.FieldTick, type, skillId, buffId); break; default: logger.Error("Invalid Buff Target: {Target}", effect.Condition.Target); @@ -157,11 +192,6 @@ private void Proc() { Caster.Field.AddSkill(Caster, effect, [Owner.Position], Owner.Rotation); } } - - NextProcTick += IntervalTick; - if (NextProcTick > EndTick) { - canProc = false; - } } private void ApplyRecovery() { @@ -230,7 +260,7 @@ private void ApplyDotBuff() { } AdditionalEffectMetadataDot.DotBuff dotBuff = Metadata.Dot.Buff; - if (dotBuff.Target == SkillEntity.Target) { + if (dotBuff.Target == SkillEntity.Owner) { Owner.Buffs.AddBuff(Caster, Owner, dotBuff.Id, dotBuff.Level, Field.FieldTick); } else { Caster.Buffs.AddBuff(Caster, Owner, dotBuff.Id, dotBuff.Level, Field.FieldTick); @@ -244,41 +274,50 @@ private void ApplyCancel() { if (Metadata.Update.Cancel.Ids.Length > 0) { foreach (int id in Metadata.Update.Cancel.Ids) { - if (Owner.Buffs.Buffs.TryGetValue(id, out Buff? buff) - && (!Metadata.Update.Cancel.CheckSameCaster || buff.Caster.ObjectId == Caster.ObjectId)) { - Owner.Buffs.Remove(id); + foreach (Buff buff in Owner.Buffs.EnumerateBuffs(id)) { + if (!Metadata.Update.Cancel.CheckSameCaster || buff.Caster.ObjectId == Caster.ObjectId) { + Owner.Buffs.Remove(id, Caster.ObjectId); + } } } - List buffsToRemove = Owner.Buffs.Buffs.Values + List buffsToRemove = Owner.Buffs.EnumerateBuffs() .Where(buff => Metadata.Update.Cancel.Categories.Contains(buff.Metadata.Property.Category) && (!Metadata.Update.Cancel.CheckSameCaster || buff.Caster.ObjectId == Caster.ObjectId) ).ToList(); - buffsToRemove.ForEach(buff => Owner.Buffs.Remove(buff.Id)); + buffsToRemove.ForEach(buff => Owner.Buffs.Remove(buff.Id, Caster.ObjectId)); } } public void ModifyDuration() { foreach (AdditionalEffectMetadataUpdate.ModifyDuration modifyDuration in Metadata.Update.Duration) { - if (!Owner.Buffs.Buffs.TryGetValue(modifyDuration.Id, out Buff? buff)) { + List buffs = Owner.Buffs.EnumerateBuffs(modifyDuration.Id); + if (buffs.Count == 0) { continue; } - buff.EndTick += (long) modifyDuration.Value; - if (modifyDuration.Rate > 0) { - long remainingDuration = (long) (modifyDuration.Rate * (buff.EndTick - Environment.TickCount64)); - buff.EndTick += (modifyDuration.Rate >= 1) ? remainingDuration : -remainingDuration; - } + foreach (Buff buff in buffs) { + buff.EndTick += (long) modifyDuration.Value; + if (modifyDuration.Rate > 0) { + long remainingDuration = (long) (modifyDuration.Rate * (buff.EndTick - Environment.TickCount64)); + buff.EndTick += (modifyDuration.Rate >= 1) ? remainingDuration : -remainingDuration; + } - // restart proc if possible - if (NextProcTick < EndTick) { - canProc = true; + // restart proc if possible + if (NextProcTick < EndTick) { + canProc = true; + } + Field.Broadcast(BuffPacket.Update(buff)); } - Field.Broadcast(BuffPacket.Update(buff)); } } + public void UpdateEndTime(int modifyValue) { + EndTick += modifyValue; + Field.Broadcast(BuffPacket.Update(this)); + } + public void WriteTo(IByteWriter writer) { WriteAdditionalEffect(writer); WriteShieldHealth(writer); diff --git a/Maple2.Server.Game/Model/Skill/DamageRecord.cs b/Maple2.Server.Game/Model/Skill/DamageRecord.cs index 38f65763e..022ea86bb 100644 --- a/Maple2.Server.Game/Model/Skill/DamageRecord.cs +++ b/Maple2.Server.Game/Model/Skill/DamageRecord.cs @@ -41,14 +41,16 @@ public DamageRecord(SkillMetadata skillMetadata, SkillMetadataAttack attackMetad } public class DamageRecordTarget { - public int ObjectId { get; init; } + public readonly IActor Target; + public int ObjectId => Target.ObjectId; public Vector3 Position; public Vector3 Direction; private readonly List<(DamageType, long)> damage; public IReadOnlyList<(DamageType Type, long Amount)> Damage => damage; - public DamageRecordTarget() { + public DamageRecordTarget(IActor target) { + Target = target; damage = new List<(DamageType Type, long Amount)>(); } diff --git a/Maple2.Server.Game/Model/Stats.cs b/Maple2.Server.Game/Model/Stats.cs index 5d3e99720..dd4df362d 100644 --- a/Maple2.Server.Game/Model/Stats.cs +++ b/Maple2.Server.Game/Model/Stats.cs @@ -14,15 +14,15 @@ public class Stats { public int GearScore = 0; - public Stats(IReadOnlyDictionary metadata, JobCode jobCode) { + public Stats(IReadOnlyDictionary statsDictionary, JobCode jobCode) { basicValues = new Dictionary(); specialValues = new Dictionary(); - foreach (BasicAttribute attribute in metadata.Keys) { + foreach (BasicAttribute attribute in statsDictionary.Keys) { if (attribute is BasicAttribute.PhysicalAtk or BasicAttribute.MagicalAtk) { continue; } - this[attribute].AddBase(metadata[attribute]); + this[attribute].AddBase(statsDictionary[attribute]); } // TODO: Remove when UserStat has rmsp @@ -40,6 +40,18 @@ public Stats(NpcMetadataStat npcStats) { } } + /// + /// Clears and sets static stats. + /// + public void SetStaticStats(IReadOnlyDictionary statsDictionary) { + basicValues.Clear(); + specialValues.Clear(); + foreach (BasicAttribute attribute in statsDictionary.Keys) { + this[attribute].AddBase(statsDictionary[attribute]); + } + // Does not add PhysicalAtk or MagicalAtk due to it not using jobCode + } + public void Reset(IReadOnlyDictionary metadata, JobCode jobCode) { basicValues.Clear(); specialValues.Clear(); diff --git a/Maple2.Server.Game/PacketHandlers/FallDamageHandler.cs b/Maple2.Server.Game/PacketHandlers/FallDamageHandler.cs index 5306c02a6..72c9292d4 100644 --- a/Maple2.Server.Game/PacketHandlers/FallDamageHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/FallDamageHandler.cs @@ -1,9 +1,7 @@ -using Maple2.Model.Enum; -using Maple2.Model.Metadata; +using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; -using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; namespace Maple2.Server.Game.PacketHandlers; @@ -15,23 +13,11 @@ public class FallDamageHandler : PacketHandler { public override void Handle(GameSession session, IByteReader packet) { float distance = packet.ReadFloat(); - int damage = CalcFallDamage(session.Player.Stats.Values[BasicAttribute.Health].Total, distance); - - if (damage > 0) { - session.Player.ConsumeHp(damage); - session.Player.Field.Broadcast(StatsPacket.Update(session.Player, BasicAttribute.Health)); - session.Send(FallDamagePacket.FallDamage(session.Player.ObjectId, damage)); - session.ConditionUpdate(ConditionType.fall_damage, targetLong: damage); - if (!session.Player.IsDead) { - session.ConditionUpdate(ConditionType.fall_survive); - } else { - session.ConditionUpdate(ConditionType.fall_die); - } + distance -= 1000f; + if (distance <= 0) { + return; } - } - private static int CalcFallDamage(long maxHp, float distance) { - float damage = maxHp * 7 * ((distance - BASE_FALL_DISTANCE) / Constant.BlockSize) / 100; - return Math.Max((int) damage, 0); + session.Player.FallDamage(distance); } } diff --git a/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs b/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs index 88799b5bd..ff66959fb 100644 --- a/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs @@ -26,7 +26,7 @@ public override void Handle(GameSession session, IByteReader packet) { // Check and remove any existing insignia buffs if (TableMetadata.InsigniaTable.Entries.TryGetValue(session.Player.Value.Character.Insignia, out InsigniaTable.Entry? oldInsigniaMetadata) && oldInsigniaMetadata.BuffId > 0) { - session.Player.Buffs.Remove(oldInsigniaMetadata.BuffId); + session.Player.Buffs.Remove(oldInsigniaMetadata.BuffId, session.Player.ObjectId); } short insigniaId = packet.ReadShort(); diff --git a/Maple2.Server.Game/PacketHandlers/InteractObjectHandler.cs b/Maple2.Server.Game/PacketHandlers/InteractObjectHandler.cs index 95038f35c..fd2a69106 100644 --- a/Maple2.Server.Game/PacketHandlers/InteractObjectHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/InteractObjectHandler.cs @@ -1,5 +1,6 @@ using Maple2.Model.Enum; using Maple2.Model.Game; +using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; @@ -103,6 +104,18 @@ private void HandleEnd(GameSession session, IByteReader packet) { }, interact.Rotation, item, session.CharacterId); session.Field.Broadcast(FieldPacket.DropItem(fieldItem)); } + + foreach (InteractObjectMetadataEffect.InvokeEffect invokeEffect in interact.Value.AdditionalEffect.Invoke) { + if (invokeEffect.Probability < 10000 && invokeEffect.Probability / 100 < Random.Shared.Next(100)) { + continue; + } + + session.Player.Buffs.AddBuff(session.Player, session.Player, invokeEffect.Id, invokeEffect.Level, session.Field.FieldTick, notifyField: true); + } + + if (interact.Value.AdditionalEffect.ModifyCode > 0) { + session.ConditionUpdate(ConditionType.interact_object, codeLong: interact.Value.Id); + } } } } diff --git a/Maple2.Server.Game/PacketHandlers/JobHandler.cs b/Maple2.Server.Game/PacketHandlers/JobHandler.cs index 5485d204f..9cebd4e3e 100644 --- a/Maple2.Server.Game/PacketHandlers/JobHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/JobHandler.cs @@ -90,7 +90,7 @@ private void HandleAdvance(GameSession session, IByteReader packet) { session.Player.Value.Character.Job = job; session.Config.Skill.SkillInfo.SetJob(job); - session.Player.Buffs.Buffs.Clear(); + session.Player.Buffs.Clear(); session.Player.Buffs.Initialize(); session.Player.Buffs.LoadFieldBuffs(); session.Stats.Refresh(); diff --git a/Maple2.Server.Game/PacketHandlers/RideHandler.cs b/Maple2.Server.Game/PacketHandlers/RideHandler.cs index 252322ab2..e673693ce 100644 --- a/Maple2.Server.Game/PacketHandlers/RideHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RideHandler.cs @@ -53,7 +53,7 @@ public override void Handle(GameSession session, IByteReader packet) { } private void HandleStart(GameSession session, IByteReader packet) { - if (session.Field == null || !session.Field.Metadata.Limit.Ride || session.Ride != null) { + if (session.Field == null || !session.Field.Metadata.Limit.Ride || session.Ride.Ride != null) { session.Send(NoticePacket.MessageBox(StringCode.s_action_cant_ride)); return; } @@ -66,7 +66,7 @@ private void HandleStart(GameSession session, IByteReader packet) { // client doesn't set this data? //var ugc = packet.ReadClass(); - if (type != RideOnType.UseItem) { + if (type != RideOnType.Default) { return; } @@ -75,41 +75,29 @@ private void HandleStart(GameSession session, IByteReader packet) { session.Send(NoticePacket.MessageBox(StringCode.s_item_invalid_do_not_have)); return; } - if (item.IsExpired() || item.Metadata.Property.Ride != rideId || !RideMetadata.TryGet(rideId, out RideMetadata? metadata)) { + if (item.IsExpired() || item.Metadata.Property.Ride != rideId) { session.Send(NoticePacket.MessageBox(StringCode.s_item_invalid_function_item)); return; } - if (item.Metadata.Limit.TransferType == TransferType.BindOnUse) { - session.Item.Bind(item); - } - - int objectId = FieldManager.NextGlobalId(); - var action = new RideOnActionUseItem(rideId, objectId, item); - session.Ride = new Ride(session.Player.ObjectId, metadata, action); - session.Field.Broadcast(RidePacket.Start(session.Ride)); + session.Ride.Mount(item); } private void HandleStop(GameSession session, IByteReader packet) { - if (session.Field == null || session.Ride == null) { + if (session.Field == null || session.Ride.Ride == null) { return; } var type = packet.Read(); - if (type != RideOffType.Default) { - return; - } - int ownerId = session.Ride.OwnerId; + int ownerId = session.Ride.Ride.OwnerId; bool forced = packet.ReadBool(); - var action = new RideOffAction(forced); - session.Ride = null; - session.Field.Broadcast(RidePacket.Stop(ownerId, action)); + session.Ride.Dismount(type, forced); } private void HandleChange(GameSession session, IByteReader packet) { - if (session.Field == null || session.Ride == null) { + if (session.Field == null || session.Ride.Ride == null) { return; } @@ -126,12 +114,12 @@ private void HandleChange(GameSession session, IByteReader packet) { return; } - session.Ride = null; + session.Ride.Ride = null; session.Field.Broadcast(RidePacket.Change(session.Player.ObjectId, rideId, itemUid)); } private void HandleJoin(GameSession session, IByteReader packet) { - if (session.Field == null || session.Ride != null) { + if (session.Field == null || session.Ride.Ride != null) { return; } @@ -139,40 +127,45 @@ private void HandleJoin(GameSession session, IByteReader packet) { if (!session.Field.TryGetPlayer(otherId, out FieldPlayer? other)) { return; } - if (other.Session.Ride == null || other.Session.Ride.Passengers.Length == 0) { + if (other.Session.Ride.Ride == null || other.Session.Ride.Ride.Passengers.Length == 0) { return; } // TODO: must be friend, guild, or party? // s_multi_riding_item_desc - sbyte index = (sbyte) Array.FindIndex(other.Session.Ride.Passengers, objectId => objectId == 0); - other.Session.Ride.Passengers[index] = session.Player.ObjectId; + sbyte index = (sbyte) Array.FindIndex(other.Session.Ride.Ride.Passengers, objectId => objectId == 0); + if (index < 0) { + // Ride is full + return; + } + + other.Session.Ride.Ride.Passengers[index] = session.Player.ObjectId; session.Ride = other.Session.Ride; - session.Field.Broadcast(RidePacket.Join(session.Ride.OwnerId, session.Player.ObjectId, index)); + session.Field.Broadcast(RidePacket.Join(session.Ride.Ride.OwnerId, session.Player.ObjectId, index)); } private void HandleLeave(GameSession session) { - if (session.Field == null || session.Ride == null) { + if (session.Field == null || session.Ride.Ride == null) { return; } - if (session.Ride.Passengers.Length == 0) { + if (session.Ride.Ride.Passengers.Length == 0) { return; } - sbyte index = (sbyte) Array.FindIndex(session.Ride.Passengers, objectId => objectId == session.Player.ObjectId); + sbyte index = (sbyte) Array.FindIndex(session.Ride.Ride.Passengers, objectId => objectId == session.Player.ObjectId); if (index < 0) { return; } - int ownerId = session.Ride.OwnerId; + int ownerId = session.Ride.Ride.OwnerId; if (session.Field.TryGetPlayerById(ownerId, out FieldPlayer? owner) && owner.Session.Ride == session.Ride) { session.Player.MoveToPosition(session.Player.Position, session.Player.Rotation); } - session.Ride.Passengers[index] = 0; - session.Ride = null; + session.Ride.Ride.Passengers[index] = 0; + session.Ride.Ride = null; session.Field.Broadcast(RidePacket.Leave(ownerId, session.Player.ObjectId)); } diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index 01cc16faf..84f9914b6 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -261,12 +261,12 @@ private void HandleTarget(GameSession session, IByteReader packet) { packet.ReadByte(); switch (record.Attack.Range.ApplyTarget) { - case SkillEntity.Target: + case SkillEntity.Owner: if (session.Field.Mobs.TryGetValue(targetId, out FieldNpc? npc)) { record.Targets.Add(npc); } continue; - case SkillEntity.Owner: + case SkillEntity.Target: if (session.Field.TryGetPlayer(targetId, out FieldPlayer? player)) { record.Targets.Add(player); } diff --git a/Maple2.Server.Game/Packets/FieldPacket.cs b/Maple2.Server.Game/Packets/FieldPacket.cs index 5d252b70a..98c32ff40 100644 --- a/Maple2.Server.Game/Packets/FieldPacket.cs +++ b/Maple2.Server.Game/Packets/FieldPacket.cs @@ -4,6 +4,7 @@ using Maple2.Model.Common; using Maple2.Model.Enum; using Maple2.Model.Game; +using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.Packets; @@ -37,9 +38,9 @@ public static ByteWriter AddPlayer(GameSession session) { pWriter.Write(player.Character.SkinColor); pWriter.WriteUnicodeString(player.Character.Picture); - pWriter.WriteBool(session.Ride != null); - if (session.Ride != null) { - pWriter.WriteClass(session.Ride.Action); + pWriter.WriteBool(session.Ride.Ride != null); + if (session.Ride.Ride != null) { + pWriter.WriteClass(session.Ride.Ride.Action); pWriter.WriteByte(); // Unknown Count for Loop } @@ -83,8 +84,9 @@ public static ByteWriter AddPlayer(GameSession session) { pWriter.WriteDeflated(buffer.Buffer, 0, buffer.Length); } - pWriter.WriteShort((short) session.Player.Buffs.Buffs.Count); - foreach (Buff buff in session.Player.Buffs.Buffs.Values) { + List buffs = session.Player.Buffs.EnumerateBuffs(); + pWriter.WriteShort((short) buffs.Count); + foreach (Buff buff in buffs) { pWriter.WriteInt(buff.Owner.ObjectId); pWriter.WriteInt(buff.ObjectId); pWriter.WriteInt(buff.Caster.ObjectId); @@ -148,8 +150,9 @@ public static ByteWriter AddNpc(FieldNpc npc) { pWriter.WriteNpcStats(npc.Stats.Values); pWriter.WriteBool(npc.IsDead); - pWriter.WriteShort((short) npc.Buffs.Buffs.Count); - foreach (Buff buff in npc.Buffs.Buffs.Values) { + List buffs = npc.Buffs.EnumerateBuffs(); + pWriter.WriteShort((short) buffs.Count); + foreach (Buff buff in buffs) { pWriter.WriteInt(buff.Owner.ObjectId); pWriter.WriteInt(buff.ObjectId); pWriter.WriteInt(buff.Caster.ObjectId); @@ -163,10 +166,10 @@ public static ByteWriter AddNpc(FieldNpc npc) { if (npc.Value.IsBoss) { pWriter.WriteUnicodeString(); // EffectStr - pWriter.WriteInt(npc.Buffs.Buffs.Count); - foreach (Buff buff in npc.Buffs.Buffs.Values) { - pWriter.WriteInt(buff.Id); - pWriter.WriteShort(buff.Level); + pWriter.WriteInt(npc.Value.Metadata.Skill.Entries.Length); + foreach (NpcMetadataSkill.Entry skill in npc.Value.Metadata.Skill.Entries) { + pWriter.WriteInt(skill.Id); + pWriter.WriteShort(skill.Level); } pWriter.WriteInt(); diff --git a/Maple2.Server.Game/Session/GameSession.State.cs b/Maple2.Server.Game/Session/GameSession.State.cs index 143ec3919..513e33e92 100644 --- a/Maple2.Server.Game/Session/GameSession.State.cs +++ b/Maple2.Server.Game/Session/GameSession.State.cs @@ -30,7 +30,6 @@ public partial class GameSession { public StorageManager? Storage; public PetManager? Pet; - public Ride? Ride; public FieldInstrument? Instrument; public FieldGuideObject? GuideObject; @@ -54,7 +53,7 @@ public partial class GameSession { public bool CanHold() { return GuideObject == null - && Ride == null + && Ride.Ride == null && HeldCube == null; } } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index a9bd3d9d6..f16ad6b67 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -63,6 +63,7 @@ public sealed partial class GameSession : Core.Network.Session { public required QuestMetadataStorage QuestMetadata { get; init; } public required ScriptMetadataStorage ScriptMetadata { get; init; } public required FunctionCubeMetadataStorage FunctionCubeMetadata { get; init; } + public required RideMetadataStorage RideMetadata { get; init; } public required FieldManager.Factory FieldFactory { get; init; } public required Lua.Lua Lua { private get; init; } public required ItemStatsCalculator ItemStatsCalc { private get; init; } @@ -101,6 +102,7 @@ public sealed partial class GameSession : Core.Network.Session { public FishingManager Fishing { get; set; } = null!; public DungeonManager Dungeon { get; set; } = null!; public AnimationManager Animation { get; set; } = null!; + public RideManager Ride { get; set; } = null!; public GameSession(TcpClient tcpClient, GameServer server, IComponentContext context) : base(tcpClient) { @@ -174,6 +176,7 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat Marriage = new MarriageManager(this); Fishing = new FishingManager(this, TableMetadata, ServerTableMetadata); Dungeon = new DungeonManager(this); + Ride = new RideManager(this); CommandHandler.RegisterCommands(); // Refresh commands with proper permissions GroupChatInfoResponse groupChatInfoRequest = World.GroupChatInfo(new GroupChatInfoRequest { CharacterId = CharacterId, @@ -348,6 +351,8 @@ private void LeaveField() { NpcScript = null; MiniGameRecord = null; + Buffs.LeaveField(); + if (Field != null) { Scheduler.Stop(); Field.RemovePlayer(Player.ObjectId, out _); diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs index dde1400c4..cbdbf9942 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs @@ -388,11 +388,11 @@ public void RemoveBuff(int boxId, int buffId, bool ignorePlayer) { DebugLog("[RemoveBuff] boxId:{Id}, buffId:{BuffId}, ignorePlayer:{ignorePlayer}", boxId, buffId, ignorePlayer); if (!ignorePlayer) { foreach (IActor player in PlayersInBox(boxId)) { - player.Buffs.Remove(buffId); + player.Buffs.Remove(buffId, Field.FieldActor.ObjectId); } } else { foreach (IActor monster in MonstersInBox(boxId)) { - monster.Buffs.Remove(buffId); + monster.Buffs.Remove(buffId, Field.FieldActor.ObjectId); } } } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs index 6f610680f..5249e3172 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs @@ -90,7 +90,7 @@ public void NpcRemoveAdditionalEffect(int spawnId, int additionalEffectId) { WarnLog("[NpcRemoveAdditionalEffect] spawnId:{SpawnId}", spawnId); foreach (FieldNpc npc in Field.EnumerateNpcs()) { if (npc.SpawnPointId == spawnId) { - npc.Buffs.Remove(additionalEffectId); + npc.Buffs.Remove(additionalEffectId, npc.ObjectId); } } } @@ -184,7 +184,15 @@ public void SpawnNpcRange(int[] spawnIds, bool isAutoTargeting, int randomPickCo #region Conditions public bool CheckNpcAdditionalEffect(int spawnId, int additionalEffectId, int level) { - ErrorLog("[CheckNpcAdditionalEffect] spawnId:{SpawnId}, additionalEffectId:{EffectId}, level:{Level}", spawnId, additionalEffectId, level); + DebugLog("[CheckNpcAdditionalEffect] spawnId:{SpawnId}, additionalEffectId:{EffectId}, level:{Level}", spawnId, additionalEffectId, level); + IEnumerable actors = Field.GetActorsBySpawnId(spawnId); + foreach (IActor actor in actors) { + if (actor is not FieldNpc) { + continue; + } + return actor.Buffs.HasBuff(additionalEffectId, (short) level); + } + return false; } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Player.cs b/Maple2.Server.Game/Trigger/TriggerContext.Player.cs index 5f71b57b8..e7ca6cd96 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Player.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Player.cs @@ -43,6 +43,9 @@ public void MoveRandomUser(int mapId, int portalId, int boxId, int count) { DebugLog("[MoveRandomUser] mapId:{MapId}, portalId:{PortalId}, boxId:{BoxId}, count:{Count}", mapId, portalId, boxId, count); FieldPlayer[] players = PlayersInBox(boxId).ToArray(); Random.Shared.Shuffle(players); + if (count > players.Length) { + count = players.Length; + } for (int i = 0; i < count; i++) { FieldPlayer player = players[i]; @@ -214,7 +217,7 @@ public void SetState(int triggerId, object[] states, bool randomize) { public bool CheckAnyUserAdditionalEffect(int boxId, int additionalEffectId, int level) { DebugLog("[CheckAnyUserAdditionalEffect] boxId:{BoxId}, additionalEffectId:{EffectId}, level:{Level}", boxId, additionalEffectId, level); foreach (FieldPlayer player in PlayersInBox(boxId)) { - if (player.Buffs.Buffs.TryGetValue(additionalEffectId, out Buff? buff) && buff.Level == level) { + if (player.Buffs.HasBuff(additionalEffectId, (short) level)) { return true; } } diff --git a/Maple2.Server.Game/Util/DamageCalculator.cs b/Maple2.Server.Game/Util/DamageCalculator.cs index 98ab2e905..b45667ed0 100644 --- a/Maple2.Server.Game/Util/DamageCalculator.cs +++ b/Maple2.Server.Game/Util/DamageCalculator.cs @@ -131,7 +131,7 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D attackDamage *= damageMultiplier * Constant.AttackDamageFactor + property.Value; // Apply any shields - foreach (Buff buff in target.Buffs.Buffs.Values.Where(buff => buff.Metadata.Shield != null)) { + foreach (Buff buff in target.Buffs.EnumerateBuffs().Where(buff => buff.Metadata.Shield != null)) { if (buff.ShieldHealth >= attackDamage) { buff.ShieldHealth -= (long) attackDamage; attackDamage = 0; @@ -140,7 +140,7 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D } attackDamage -= buff.ShieldHealth; - target.Buffs.Remove(buff.Id); + target.Buffs.Remove(buff.Id, target.ObjectId); } return (damageType, (long) Math.Max(1, attackDamage)); diff --git a/Maple2.Server.Game/Util/NpcTalkUtil.cs b/Maple2.Server.Game/Util/NpcTalkUtil.cs index 403c4a0ac..79e57343e 100644 --- a/Maple2.Server.Game/Util/NpcTalkUtil.cs +++ b/Maple2.Server.Game/Util/NpcTalkUtil.cs @@ -103,7 +103,7 @@ private static bool MeetsJobCondition(GameSession session, JobConditionMetadata // TODO: Maid checks - if (jobCondition.BuffId > 0 && !session.Player.Buffs.Buffs.ContainsKey(jobCondition.BuffId)) { + if (jobCondition.BuffId > 0 && !session.Player.Buffs.HasBuff(jobCondition.BuffId)) { return false; } @@ -178,7 +178,7 @@ public static bool ConditionCheck(this ScriptConditionMetadata scriptCondition, } if (scriptCondition.Buff.Key > 0) { - if (scriptCondition.Buff.Value && !session.Player.Buffs.Buffs.ContainsKey(scriptCondition.Buff.Key)) { + if (scriptCondition.Buff.Value && !session.Player.Buffs.HasBuff(scriptCondition.Buff.Key)) { return false; } } diff --git a/Maple2.Server.Game/Util/SkillUtils.cs b/Maple2.Server.Game/Util/SkillUtils.cs index 535069240..c58a3eb5f 100644 --- a/Maple2.Server.Game/Util/SkillUtils.cs +++ b/Maple2.Server.Game/Util/SkillUtils.cs @@ -68,7 +68,7 @@ public static IEnumerable Filter(this Prism[] prisms, IEnumerable entit } } - public static bool Check(this BeginCondition condition, IActor caster, IActor owner, IActor target) { + public static bool Check(this BeginCondition condition, IActor caster, IActor owner, IActor target, EventConditionType eventType = EventConditionType.Activate, int eventSkillId = 0, int eventBuffId = 0) { if (caster is FieldPlayer player) { if (condition is not { Probability: 1 } && condition.Probability < Random.Shared.NextDouble()) { return false; @@ -129,36 +129,48 @@ public static bool Check(this BeginCondition condition, IActor caster, IActor ow if (condition.ActiveSkill.Length > 0 && condition.ActiveSkill.All(id => owner.Animation.Current?.Skill?.Id != id)) { return false; } + if (condition.OnlyOnBattleMount && player.Session.Ride.Ride?.Metadata.Basic.Type != RideOnType.Battle) { + return false; + } + if (!condition.AllowOnBattleMount && player.Session.Ride.Ride?.Metadata.Basic.Type == RideOnType.Battle) { + return false; + } } - return condition.Caster.Check(caster) && condition.Owner.Check(owner) && condition.Target.Check(target); + return condition.Caster.Check(caster, eventType, eventSkillId, eventBuffId) && condition.Owner.Check(owner, eventType, eventSkillId, eventBuffId) && condition.Target.Check(target, eventType, eventSkillId, eventBuffId); } - private static bool Check(this BeginConditionTarget? condition, IActor target) { + private static bool Check(this BeginConditionTarget? condition, IActor target, EventConditionType eventType = EventConditionType.Activate, int eventSkillId = 0, int eventBuffId = 0) { if (condition == null) { return true; } foreach ((int id, short level, bool owned, int count, CompareType compare) in condition.Buff) { - if (!target.Buffs.Buffs.TryGetValue(id, out Buff? buff)) { - return false; - } - if (buff.Level < level) { - return false; - } - if (owned && buff.Owner.ObjectId == 0) { - return false; - } + List buffs = target.Buffs.EnumerateBuffs(id); + bool validBuff = false; + foreach (Buff buff in buffs) { + if (buff.Level < level) { + continue; + } + if (owned && buff.Owner.ObjectId == 0) { + continue; + } - bool compareResult = compare switch { - CompareType.Equals => buff.Stacks == count, - CompareType.Less => buff.Stacks < count, - CompareType.LessEquals => buff.Stacks <= count, - CompareType.Greater => buff.Stacks > count, - CompareType.GreaterEquals => buff.Stacks >= count, - _ => true, - }; - if (!compareResult) { + bool compareResult = compare switch { + CompareType.Equals => buff.Stacks == count, + CompareType.Less => buff.Stacks < count, + CompareType.LessEquals => buff.Stacks <= count, + CompareType.Greater => buff.Stacks > count, + CompareType.GreaterEquals => buff.Stacks >= count, + _ => true, + }; + if (!compareResult) { + continue; + } + validBuff = true; + break; + } + if (!validBuff) { return false; } } @@ -207,6 +219,17 @@ private static bool Check(this BeginConditionTarget? condition, IActor target) { } } + if (condition.Event.Type != eventType) { + return false; + } + + if (condition.Event.BuffIds.Length > 0 && !condition.Event.BuffIds.Contains(eventBuffId)) { + return false; + } + if (condition.Event.SkillIds.Length > 0 && !condition.Event.SkillIds.Contains(eventSkillId)) { + return false; + } + return true; }