# 角色行为(技能)系统实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现共享层角色行为核心,可从 JSON 加载技能、飞行物、Buff 和逻辑效果配置,并用 xUnit 覆盖第一版权威逻辑。 **Architecture:** 在 `Client/Assets/Framework/Shared/ActorBehavior/` 新增纯 C# 共享模块,由 `Server/Framework.Shared` 同源编译。运行时通过 `BehaviorWorld` 管理角色、主动行为、飞行物和 Buff,并输出 `BehaviorEvent` 给服务端逻辑或客户端表现层消费。 **Tech Stack:** C# 9.0, netstandard2.1 shared library, xUnit tests in `Server/Framework.Shared.Tests`, no UnityEngine dependency, no external JSON package. ## Global Constraints - 第一版只实现共享核心、示例配置和自动化测试。 - 不直接接入大厅角色控制器。 - 不直接接入某个小游戏 UI。 - 共享核心不得引用 Unity。 - 客户端和服务端加载同一套 JSON。 - 服务端只运行权威逻辑;客户端表现模式不执行权威属性结算。 - JSON 解析不得引入新的 NuGet 依赖,避免破坏 Unity 和服务端同源编译。 --- ## File Structure Create: - `Client/Assets/Framework/Shared/ActorBehavior/BehaviorMath.cs` - `BehaviorVector2`、`BehaviorVector3` 和基础几何方法。 - `Client/Assets/Framework/Shared/ActorBehavior/BehaviorTypes.cs` - 枚举、输入、事件、角色状态、运行选项、结果类型。 - `Client/Assets/Framework/Shared/ActorBehavior/BehaviorConfigModels.cs` - JSON 对应的配置模型。 - `Client/Assets/Framework/Shared/ActorBehavior/BehaviorJson.cs` - 轻量 JSON parser 和模型转换,不依赖外部包。 - `Client/Assets/Framework/Shared/ActorBehavior/BehaviorConfigCatalog.cs` - 配置加载、索引和校验。 - `Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs` - 行为、飞行物、Buff、逻辑效果运行时。 - `Client/Assets/Game/Config/ActorBehavior/fighter_skill.json` - 示例技能配置。 - `Client/Assets/Game/Config/ActorBehavior/fighter_flyobj.json` - 示例飞行物配置。 - `Client/Assets/Game/Config/ActorBehavior/fighter_buff.json` - 示例 Buff 配置。 - `Server/Framework.Shared.Tests/ActorBehavior/BehaviorConfigCatalogTests.cs` - 配置解析和校验测试。 - `Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs` - 行为时间线、模式过滤、飞行物和 Buff 测试。 - `Server/Framework.Shared.Tests/ActorBehavior/BehaviorTargetingTests.cs` - 扇形、球形、环形、直线、锁定目标测试。 Modify: - No project file changes expected. `Server/Framework.Shared/Framework.Shared.csproj` already compiles `Client/Assets/Framework/Shared/**/*.cs`. ## Shared Interfaces The implementation must expose these public APIs: ```csharp namespace XWorld.Framework.ActorBehavior { public sealed class BehaviorConfigCatalog { public string Type { get; } public int Version { get; } public IReadOnlyDictionary Behaviors { get; } public IReadOnlyDictionary FlyObjects { get; } public IReadOnlyDictionary Buffs { get; } public static BehaviorConfigCatalog LoadFromJson(string type, string skillJson, string flyObjectJson, string buffJson); } public sealed class BehaviorWorld { public BehaviorWorld(BehaviorWorldOptions options); public void RegisterCatalog(BehaviorConfigCatalog catalog); public void AddActor(BehaviorActorState actor); public bool SwitchActorCatalog(int actorId, string catalogType); public BehaviorStartResult TryStartBehavior(int actorId, string behaviorId, BehaviorInput input); public bool StopBehavior(int actorId, BehaviorStopReason reason); public void Tick(float deltaTime); public IReadOnlyList DrainEvents(); public bool HasActiveBehavior(int actorId); public int ActiveProjectileCount { get; } public int ActiveBuffCount(int actorId); } } ``` ## Task 1: 配置模型和 JSON 加载 **Files:** - Create: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorMath.cs` - Create: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorTypes.cs` - Create: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorConfigModels.cs` - Create: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorJson.cs` - Create: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorConfigCatalog.cs` - Test: `Server/Framework.Shared.Tests/ActorBehavior/BehaviorConfigCatalogTests.cs` **Interfaces:** - Produces: `BehaviorConfigCatalog.LoadFromJson(string type, string skillJson, string flyObjectJson, string buffJson)` - Produces: model classes `BehaviorSpec`, `FlyObjectSpec`, `BuffSpec`, `LogicEffectSpec`, `TimelineEntrySpec` - Produces: enums `TimelineEntryKind`, `LogicShapeKind`, `LogicEffectOpKind`, `FlyMovementKind`, `BuffStackingKind` - [ ] **Step 1: Write failing config loading tests** ```csharp using System; using Xunit; using XWorld.Framework.ActorBehavior; namespace XWorld.Framework.Tests.ActorBehavior { public class BehaviorConfigCatalogTests { [Fact] public void LoadFromJson_LoadsCompleteFighterCatalog() { BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson( "fighter", TestBehaviorJson.Skill, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff); Assert.Equal("fighter", catalog.Type); Assert.Equal(1, catalog.Version); Assert.True(catalog.Behaviors.ContainsKey("slash")); Assert.True(catalog.FlyObjects.ContainsKey("blade_wave")); Assert.True(catalog.Buffs.ContainsKey("slow_01")); Assert.Equal(4, catalog.Behaviors["slash"].Timeline.Count); } [Fact] public void LoadFromJson_RejectsMismatchedType() { string mismatchedBuff = TestBehaviorJson.Buff.Replace("\"type\": \"fighter\"", "\"type\": \"mage\""); InvalidOperationException ex = Assert.Throws(() => BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, TestBehaviorJson.FlyObject, mismatchedBuff)); Assert.Contains("type", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void LoadFromJson_RejectsMissingReferences() { string badSkill = TestBehaviorJson.Skill.Replace("\"flyObjectId\": \"blade_wave\"", "\"flyObjectId\": \"missing_wave\""); InvalidOperationException ex = Assert.Throws(() => BehaviorConfigCatalog.LoadFromJson("fighter", badSkill, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff)); Assert.Contains("missing_wave", ex.Message); } } } ``` - [ ] **Step 2: Add test fixture JSON** Add a `TestBehaviorJson` helper in the same test file: ```csharp internal static class TestBehaviorJson { public const string Skill = @"{ ""type"": ""fighter"", ""version"": 1, ""behaviors"": [{ ""id"": ""slash"", ""duration"": 0.8, ""canBeInterrupted"": true, ""input"": ""DirectionOrTarget"", ""timeline"": [ { ""time"": 0.0, ""kind"": ""Animation"", ""animation"": ""slash_01"", ""duration"": 0.8 }, { ""time"": 0.12, ""kind"": ""Effect"", ""prefab"": ""Assets/Game/Art/Effect/slash.prefab"", ""duration"": 0.5 }, { ""time"": 0.25, ""kind"": ""LogicEffect"", ""effectId"": ""slash_hit"" }, { ""time"": 0.32, ""kind"": ""Projectile"", ""flyObjectId"": ""blade_wave"" } ], ""logicEffects"": [{ ""id"": ""slash_hit"", ""shape"": ""Sector"", ""radius"": 2.4, ""angle"": 90, ""target"": ""Enemy"", ""effects"": [{ ""type"": ""Attribute"", ""attribute"": ""hp"", ""value"": -30 }] }] }] }"; public const string FlyObject = @"{ ""type"": ""fighter"", ""version"": 1, ""flyObjects"": [{ ""id"": ""blade_wave"", ""duration"": 1.5, ""movement"": { ""kind"": ""Line"", ""speed"": 6.0 }, ""collision"": { ""shape"": ""Sphere"", ""radius"": 0.4, ""hitLimit"": 1 }, ""timeline"": [ { ""time"": 0.0, ""kind"": ""Effect"", ""prefab"": ""Assets/Game/Art/Effect/blade_wave.prefab"", ""duration"": 1.5 }, { ""time"": 0.0, ""kind"": ""LogicEffect"", ""effectId"": ""blade_wave_hit"", ""trigger"": ""OnCollision"" } ], ""logicEffects"": [{ ""id"": ""blade_wave_hit"", ""shape"": ""Locked"", ""target"": ""HitActor"", ""effects"": [ { ""type"": ""Attribute"", ""attribute"": ""hp"", ""value"": -20 }, { ""type"": ""Buff"", ""buffId"": ""slow_01"" } ] }] }] }"; public const string Buff = @"{ ""type"": ""fighter"", ""version"": 1, ""buffs"": [{ ""id"": ""slow_01"", ""duration"": 3.0, ""tickInterval"": 1.0, ""repeatCount"": 3, ""stacking"": ""RefreshDuration"", ""logicEffects"": [{ ""id"": ""slow_tick"", ""shape"": ""Locked"", ""target"": ""Owner"", ""effects"": [{ ""type"": ""Attribute"", ""attribute"": ""moveSpeed"", ""value"": -0.2 }] }] }] }"; } ``` - [ ] **Step 3: Run tests to verify red** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter BehaviorConfigCatalogTests ``` Expected: FAIL because `XWorld.Framework.ActorBehavior` types do not exist. - [ ] **Step 4: Implement minimal config models and parser** Create the files listed above. Implement: - A recursive JSON parser supporting object, array, string, number, boolean, and null. - Typed conversion helpers for required strings and optional numbers. - Public config model classes with public fields/properties. - Catalog validation for type mismatch, missing references, duplicates, invalid duration, and invalid shapes. - [ ] **Step 5: Run tests to verify green** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter BehaviorConfigCatalogTests ``` Expected: PASS. ## Task 2: 行为状态机和时间线事件 **Files:** - Modify: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs` - Test: `Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs` **Interfaces:** - Consumes: `BehaviorConfigCatalog` - Produces: `BehaviorWorld.TryStartBehavior`, `StopBehavior`, `Tick`, `DrainEvents`, `HasActiveBehavior` - Produces: `BehaviorEventKind.BehaviorStarted`, `BehaviorStopped`, `TimelineAnimation`, `TimelineEffect`, `LogicEffectApplied`, `AttributeEffectRequested` - [ ] **Step 1: Write failing behavior runtime tests** ```csharp using System.Linq; using Xunit; using XWorld.Framework.ActorBehavior; namespace XWorld.Framework.Tests.ActorBehavior { public class BehaviorWorldTests { [Fact] public void TryStartBehavior_AllowsOnlyOneActiveBehavior() { BehaviorWorld world = TestWorld.CreateServerWorld(); Assert.True(world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)).Success); BehaviorStartResult second = world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)); Assert.False(second.Success); Assert.True(world.HasActiveBehavior(1)); } [Fact] public void StopBehavior_AllowsInterruptibleBehaviorToSwitch() { BehaviorWorld world = TestWorld.CreateServerWorld(); world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)); Assert.True(world.StopBehavior(1, BehaviorStopReason.Stunned)); Assert.True(world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)).Success); Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.BehaviorStopped); } [Fact] public void Tick_TriggersTimelineEventsAtConfiguredTimes() { BehaviorWorld world = TestWorld.CreatePresentationWorld(); world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)); world.Tick(0.26f); var events = world.DrainEvents(); Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineAnimation && e.Animation == "slash_01"); Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineEffect && e.Prefab == "Assets/Game/Art/Effect/slash.prefab"); } [Fact] public void ServerLogic_SkipsPresentationEventsAndAppliesLogic() { BehaviorWorld world = TestWorld.CreateServerWorld(); world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)); world.Tick(0.26f); var events = world.DrainEvents(); Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.TimelineAnimation); Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.TimelineEffect); Assert.Contains(events, e => e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 2 && e.Attribute == "hp"); } } } ``` - [ ] **Step 2: Add shared test world helper** Add to `BehaviorWorldTests.cs`: ```csharp internal static class TestWorld { public static BehaviorWorld CreateServerWorld() { return Create(BehaviorRunMode.ServerLogic); } public static BehaviorWorld CreatePresentationWorld() { return Create(BehaviorRunMode.ClientPresentation); } private static BehaviorWorld Create(BehaviorRunMode mode) { BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson( "fighter", TestBehaviorJson.Skill, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff); var world = new BehaviorWorld(new BehaviorWorldOptions { Mode = mode }); world.RegisterCatalog(catalog); world.AddActor(new BehaviorActorState { ActorId = 1, CatalogType = "fighter", TeamId = 1, Position = new BehaviorVector3(0, 0, 0), Forward = new BehaviorVector3(1, 0, 0), Radius = 0.3f, Alive = true }); world.AddActor(new BehaviorActorState { ActorId = 2, CatalogType = "fighter", TeamId = 2, Position = new BehaviorVector3(1, 0, 0), Forward = new BehaviorVector3(-1, 0, 0), Radius = 0.3f, Alive = true }); return world; } } ``` - [ ] **Step 3: Run tests to verify red** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter BehaviorWorldTests ``` Expected: FAIL because runtime behavior is not implemented. - [ ] **Step 4: Implement behavior runtime** Implement: - Actor dictionary. - Catalog registration. - Active behavior state with elapsed time and fired timeline indices. - `TryStartBehavior` returning `BehaviorStartResult`. - Immediate firing of timeline entries at `time <= 0`. - `Tick` firing timeline entries when `previousElapsed < entry.time <= currentElapsed`. - Auto-stop when elapsed reaches behavior duration. - Run-mode filtering: presentation events only in `ClientPresentation`, logic application only in `ServerLogic`. - [ ] **Step 5: Run tests to verify green** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter BehaviorWorldTests ``` Expected: PASS for behavior state and timeline tests. ## Task 3: 目标选择和逻辑效果 **Files:** - Modify: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs` - Modify: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorMath.cs` - Test: `Server/Framework.Shared.Tests/ActorBehavior/BehaviorTargetingTests.cs` **Interfaces:** - Consumes: `LogicEffectSpec` - Produces: target selection for `Sector`, `Sphere`, `Ring`, `Line`, `Locked` - [ ] **Step 1: Write failing targeting tests** ```csharp using System.Linq; using Xunit; using XWorld.Framework.ActorBehavior; namespace XWorld.Framework.Tests.ActorBehavior { public class BehaviorTargetingTests { [Theory] [InlineData("sector_test", 2)] [InlineData("sphere_test", 2)] [InlineData("ring_test", 3)] [InlineData("line_test", 2)] public void LogicEffectShapes_SelectExpectedTargets(string behaviorId, int expectedTarget) { BehaviorWorld world = TestTargetWorld.Create(); world.TryStartBehavior(1, behaviorId, BehaviorInput.FromDirection(1, 0)); world.Tick(0.01f); Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == expectedTarget); } [Fact] public void LockedEffect_UsesTargetActorInput() { BehaviorWorld world = TestTargetWorld.Create(); world.TryStartBehavior(1, "locked_test", BehaviorInput.FromTargetActor(3)); world.Tick(0.01f); Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 3); } } } ``` - [ ] **Step 2: Add targeting fixture JSON helper** Create `TestTargetWorld` in the same file. It should build a catalog with five behaviors, each firing one `LogicEffect` at `time: 0`, and actors at: ```csharp actor 1: position (0,0,0), forward (1,0,0), team 1 actor 2: position (1,0,0), team 2 actor 3: position (3,0,0), team 2 actor 4: position (0,0,2), team 2 ``` Use shape parameters: - `sector_test`: `Sector`, radius `2`, angle `90`, target `Enemy`. - `sphere_test`: `Sphere`, radius `1.5`, target `Enemy`. - `ring_test`: `Ring`, minRadius `2`, radius `3.5`, target `Enemy`. - `line_test`: `Line`, radius `2`, width `0.5`, target `Enemy`. - `locked_test`: `Locked`, target `HitActor`. - [ ] **Step 3: Run tests to verify red** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter BehaviorTargetingTests ``` Expected: FAIL until shape selection is implemented. - [ ] **Step 4: Implement shape selection** Implement geometry in XZ plane: - `Sphere`: distance from origin to actor center is within `radius + actor.Radius`. - `Ring`: distance is between `minRadius` and `radius + actor.Radius`. - `Sector`: within radius and absolute angle from forward is within `angle / 2`. - `Line`: distance from actor center to segment `[origin, origin + forward * radius]` is within `width + actor.Radius`. - `Locked`: choose input target actor, owner, or hit actor according to target filter. - [ ] **Step 5: Run tests to verify green** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter BehaviorTargetingTests ``` Expected: PASS. ## Task 4: 飞行物运行时和碰撞 **Files:** - Modify: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs` - Test: `Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs` **Interfaces:** - Produces: `ActiveProjectileCount` - Produces: `BehaviorEventKind.ProjectileSpawned`, `ProjectileMoved`, `ProjectileHit` - [ ] **Step 1: Write failing projectile tests** Add tests to `BehaviorWorldTests.cs`: ```csharp [Fact] public void Projectile_SpawnsMovesHitsAndExpiresAtHitLimit() { BehaviorWorld world = TestWorld.CreateServerWorld(); world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)); world.Tick(0.33f); Assert.True(world.ActiveProjectileCount > 0); world.Tick(0.1f); var events = world.DrainEvents(); Assert.Contains(events, e => e.Kind == BehaviorEventKind.ProjectileSpawned); Assert.Contains(events, e => e.Kind == BehaviorEventKind.ProjectileMoved); Assert.Contains(events, e => e.Kind == BehaviorEventKind.ProjectileHit && e.TargetActorId == 2); Assert.Equal(0, world.ActiveProjectileCount); } [Theory] [InlineData("Line")] [InlineData("Parabola")] [InlineData("Homing")] public void ProjectileMovementModes_UpdatePosition(string movementKind) { string fly = TestBehaviorJson.FlyObject.Replace(@"""kind"": ""Line""", @"""kind"": """ + movementKind + @""""); BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, fly, TestBehaviorJson.Buff); var world = new BehaviorWorld(new BehaviorWorldOptions { Mode = BehaviorRunMode.ClientPresentation }); world.RegisterCatalog(catalog); world.AddActor(new BehaviorActorState { ActorId = 1, CatalogType = "fighter", TeamId = 1, Position = new BehaviorVector3(0, 0, 0), Forward = new BehaviorVector3(1, 0, 0), Radius = 0.3f, Alive = true }); world.AddActor(new BehaviorActorState { ActorId = 2, CatalogType = "fighter", TeamId = 2, Position = new BehaviorVector3(4, 0, 0), Forward = new BehaviorVector3(-1, 0, 0), Radius = 0.3f, Alive = true }); world.TryStartBehavior(1, "slash", BehaviorInput.FromTargetActor(2)); world.Tick(0.33f); world.DrainEvents(); world.Tick(0.1f); Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.ProjectileMoved && e.Position.X > 0); } ``` - [ ] **Step 2: Run projectile tests to verify red** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter "Projectile" ``` Expected: FAIL until projectile runtime is implemented. - [ ] **Step 3: Implement projectile runtime** Implement: - Projectile spawn from timeline entries with `kind: Projectile`. - Position starts at owner position. - Direction comes from `BehaviorInput.Direction`, target actor direction, or actor forward. - `Line`: `position += direction * speed * dt`. - `Parabola`: horizontal direction with speed, vertical velocity affected by gravity; keep XZ deterministic. - `Homing`: direction recalculated toward target actor each tick. - Sphere collision against alive enemy actors. - On collision emit `ProjectileHit`, run `OnCollision` logic effect, increment hit count, expire at `hitLimit`. - [ ] **Step 4: Run projectile tests to verify green** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter "Projectile" ``` Expected: PASS. ## Task 5: Buff 运行时 **Files:** - Modify: `Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs` - Test: `Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs` **Interfaces:** - Produces: `ActiveBuffCount(int actorId)` - Produces: `BehaviorEventKind.BuffAdded`, `BuffTicked`, `BuffRemoved` - [ ] **Step 1: Write failing buff test** Add to `BehaviorWorldTests.cs`: ```csharp [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)); } ``` Use the public `ApplyBuff(int targetActorId, string catalogType, string buffId, int sourceActorId)` API. Do not add test-only production methods. - [ ] **Step 2: Run buff test to verify red** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter "Buff" ``` Expected: FAIL until buff runtime is implemented. - [ ] **Step 3: Implement buff runtime** Implement: - Public `ApplyBuff(int targetActorId, string catalogType, string buffId, int sourceActorId)`. - Stacking modes `RefreshDuration`, `IgnoreIfExists`, `StackIndependent`. - Tick elapsed duration. - Fire `BuffTicked` when elapsed crosses each interval. - Respect `repeatCount`. - Fire `BuffRemoved` when duration expires or repeat count is reached. - Logic operations of type `Buff` should call `ApplyBuff`. - [ ] **Step 4: Run buff tests to verify green** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter "Buff" ``` Expected: PASS. ## Task 6: 示例配置、全量验证和 cleanup **Files:** - Create: `Client/Assets/Game/Config/ActorBehavior/fighter_skill.json` - Create: `Client/Assets/Game/Config/ActorBehavior/fighter_flyobj.json` - Create: `Client/Assets/Game/Config/ActorBehavior/fighter_buff.json` - Modify: all new actor behavior files as needed after refactor - Test: `Server/Framework.Shared.Tests/ActorBehavior/BehaviorConfigCatalogTests.cs` **Interfaces:** - Consumes: all previous public APIs. - Produces: repository example JSON matching the spec. - [ ] **Step 1: Write failing example config test** Add to `BehaviorConfigCatalogTests.cs`: ```csharp [Fact] public void ExampleConfigFiles_LoadFromRepository() { string root = FindRepoRoot(); string dir = System.IO.Path.Combine(root, "Client", "Assets", "Game", "Config", "ActorBehavior"); BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson( "fighter", System.IO.File.ReadAllText(System.IO.Path.Combine(dir, "fighter_skill.json")), System.IO.File.ReadAllText(System.IO.Path.Combine(dir, "fighter_flyobj.json")), System.IO.File.ReadAllText(System.IO.Path.Combine(dir, "fighter_buff.json"))); Assert.True(catalog.Behaviors.ContainsKey("slash")); } private static string FindRepoRoot() { string dir = AppContext.BaseDirectory; while (dir != null) { if (System.IO.Directory.Exists(System.IO.Path.Combine(dir, ".git"))) { return dir; } dir = System.IO.Directory.GetParent(dir)?.FullName; } throw new InvalidOperationException("repo root not found"); } ``` - [ ] **Step 2: Run example test to verify red** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj --filter ExampleConfigFiles_LoadFromRepository ``` Expected: FAIL until example files exist. - [ ] **Step 3: Add example JSON files** Create the three example files using the JSON from `TestBehaviorJson`. - [ ] **Step 4: Run full shared tests** Run: ```powershell dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj ``` Expected: PASS. - [ ] **Step 5: Run server solution build** Run: ```powershell dotnet build Server\Server.sln ``` Expected: PASS. - [ ] **Step 6: Review git diff** Run: ```powershell git diff -- Client/Assets/Framework/Shared/ActorBehavior Server/Framework.Shared.Tests/ActorBehavior Client/Assets/Game/Config/ActorBehavior ``` Expected: only actor behavior core, tests, and example config files changed.