From c12e9ea9d77775c68462547a25cb990bdf226962 Mon Sep 17 00:00:00 2001 From: ud18010 Date: Thu, 30 Jul 2026 13:17:41 +0800 Subject: [PATCH] feat: add actor behavior buffs --- .../Shared/ActorBehavior/BehaviorWorld.cs | 130 +++++++++++++++++- .../ActorBehavior/BehaviorWorldTests.cs | 21 +++ 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs b/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs index b28c288c..15694be1 100644 --- a/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs +++ b/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs @@ -10,6 +10,7 @@ namespace XWorld.Framework.ActorBehavior private readonly Dictionary _actors = new Dictionary(); private readonly Dictionary _activeBehaviors = new Dictionary(); private readonly List _projectiles = new List(); + private readonly Dictionary> _buffs = new Dictionary>(); private readonly List _events = new List(); private int _nextProjectileId = 1; @@ -84,6 +85,7 @@ namespace XWorld.Framework.ActorBehavior { if (deltaTime <= 0f) return; TickProjectiles(deltaTime); + TickBuffs(deltaTime); var completed = new List(); foreach (ActiveBehavior active in new List(_activeBehaviors.Values)) @@ -117,12 +119,68 @@ namespace XWorld.Framework.ActorBehavior public int ActiveBuffCount(int actorId) { - return 0; + return _buffs.TryGetValue(actorId, out List buffs) ? buffs.Count : 0; } public bool ApplyBuff(int targetActorId, string catalogType, string buffId, int sourceActorId) { - return false; + if (!_actors.ContainsKey(targetActorId)) return false; + if (!_catalogs.TryGetValue(catalogType, out BehaviorConfigCatalog catalog)) return false; + if (!catalog.Buffs.TryGetValue(buffId, out BuffSpec spec)) return false; + + if (!_buffs.TryGetValue(targetActorId, out List activeBuffs)) + { + activeBuffs = new List(); + _buffs[targetActorId] = activeBuffs; + } + + ActiveBuff existing = null; + for (int i = 0; i < activeBuffs.Count; i++) + { + if (activeBuffs[i].Spec.Id == buffId) + { + existing = activeBuffs[i]; + break; + } + } + + if (existing != null && spec.Stacking == BuffStackingKind.IgnoreIfExists) + return true; + if (existing != null && spec.Stacking == BuffStackingKind.RefreshDuration) + { + existing.Elapsed = 0f; + existing.NextTickTime = spec.TickInterval > 0f ? spec.TickInterval : spec.Duration; + existing.TickCount = 0; + _events.Add(new BehaviorEvent + { + Kind = BehaviorEventKind.BuffAdded, + ActorId = sourceActorId, + SourceActorId = sourceActorId, + TargetActorId = targetActorId, + BuffId = spec.Id + }); + return true; + } + + activeBuffs.Add(new ActiveBuff + { + SourceActorId = sourceActorId, + TargetActorId = targetActorId, + Catalog = catalog, + Spec = spec, + Elapsed = 0f, + NextTickTime = spec.TickInterval > 0f ? spec.TickInterval : spec.Duration, + TickCount = 0 + }); + _events.Add(new BehaviorEvent + { + Kind = BehaviorEventKind.BuffAdded, + ActorId = sourceActorId, + SourceActorId = sourceActorId, + TargetActorId = targetActorId, + BuffId = spec.Id + }); + return true; } private void FireTimeline(ActiveBehavior active, float previousElapsed, float currentElapsed) @@ -217,10 +275,67 @@ namespace XWorld.Framework.ActorBehavior { SpawnProjectile(sourceActorId, catalog, op.FlyObjectId, input); } + else if (op.Type == LogicEffectOpKind.Buff) + { + ApplyBuff(targetActorId, catalog.Type, op.BuffId, sourceActorId); + } } } } + private void TickBuffs(float deltaTime) + { + var actorIds = new List(_buffs.Keys); + for (int actorIndex = 0; actorIndex < actorIds.Count; actorIndex++) + { + int actorId = actorIds[actorIndex]; + List activeBuffs = _buffs[actorId]; + var expired = new List(); + for (int i = 0; i < activeBuffs.Count; i++) + { + ActiveBuff buff = activeBuffs[i]; + buff.Elapsed += deltaTime; + while (buff.Spec.TickInterval > 0f + && buff.Elapsed + 0.00001f >= buff.NextTickTime + && (buff.Spec.RepeatCount <= 0 || buff.TickCount < buff.Spec.RepeatCount)) + { + buff.TickCount++; + _events.Add(new BehaviorEvent + { + Kind = BehaviorEventKind.BuffTicked, + ActorId = buff.SourceActorId, + SourceActorId = buff.SourceActorId, + TargetActorId = buff.TargetActorId, + BuffId = buff.Spec.Id + }); + for (int e = 0; e < buff.Spec.LogicEffects.Count; e++) + ApplyLogicEffect(buff.TargetActorId, buff.Catalog, buff.Spec.LogicEffects[e], BehaviorInput.FromTargetActor(buff.TargetActorId), buff.TargetActorId); + buff.NextTickTime += buff.Spec.TickInterval; + } + + bool repeatReached = buff.Spec.RepeatCount > 0 && buff.TickCount >= buff.Spec.RepeatCount; + bool durationReached = buff.Spec.Duration > 0f && buff.Elapsed >= buff.Spec.Duration; + if (repeatReached || durationReached) + { + expired.Add(buff); + _events.Add(new BehaviorEvent + { + Kind = BehaviorEventKind.BuffRemoved, + ActorId = buff.SourceActorId, + SourceActorId = buff.SourceActorId, + TargetActorId = buff.TargetActorId, + BuffId = buff.Spec.Id + }); + } + } + + for (int i = 0; i < expired.Count; i++) + activeBuffs.Remove(expired[i]); + if (activeBuffs.Count == 0) + _buffs.Remove(actorId); + } + } + private void SpawnProjectile(int sourceActorId, BehaviorConfigCatalog catalog, string flyObjectId, BehaviorInput input) { if (!_actors.TryGetValue(sourceActorId, out BehaviorActorState source)) return; @@ -461,5 +576,16 @@ namespace XWorld.Framework.ActorBehavior public float VerticalVelocity; public int HitCount; } + + private sealed class ActiveBuff + { + public int SourceActorId; + public int TargetActorId; + public BehaviorConfigCatalog Catalog; + public BuffSpec Spec; + public float Elapsed; + public float NextTickTime; + public int TickCount; + } } } diff --git a/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs b/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs index 62c67530..dc9677e2 100644 --- a/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs +++ b/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using Xunit; using XWorld.Framework.ActorBehavior; @@ -94,6 +95,26 @@ namespace XWorld.Framework.Tests.ActorBehavior Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.ProjectileMoved && e.Position.X > 0); } + + [Fact] + public void Buff_TicksByIntervalRepeatCountAndExpires() + { + BehaviorWorld world = TestWorld.CreateServerWorld(); + + Assert.True(world.ApplyBuff(2, "fighter", "slow_01", 1)); + Assert.Equal(1, world.ActiveBuffCount(2)); + + world.Tick(1.0f); + world.Tick(1.0f); + world.Tick(1.0f); + world.Tick(0.1f); + var events = world.DrainEvents(); + + Assert.Contains(events, e => e.Kind == BehaviorEventKind.BuffAdded && e.TargetActorId == 2); + Assert.Equal(3, events.Count(e => e.Kind == BehaviorEventKind.BuffTicked && e.TargetActorId == 2)); + Assert.Contains(events, e => e.Kind == BehaviorEventKind.BuffRemoved && e.TargetActorId == 2); + Assert.Equal(0, world.ActiveBuffCount(2)); + } } internal static class TestWorld