Files
AIC-Project/docs/superpowers/plans/2026-07-30-actor-behavior-system.md

27 KiB

角色行为(技能)系统实现计划

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
    • BehaviorVector2BehaviorVector3 和基础几何方法。
  • 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:

namespace XWorld.Framework.ActorBehavior
{
    public sealed class BehaviorConfigCatalog
    {
        public string Type { get; }
        public int Version { get; }
        public IReadOnlyDictionary<string, BehaviorSpec> Behaviors { get; }
        public IReadOnlyDictionary<string, FlyObjectSpec> FlyObjects { get; }
        public IReadOnlyDictionary<string, BuffSpec> 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<BehaviorEvent> 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

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<InvalidOperationException>(() =>
                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<InvalidOperationException>(() =>
                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:

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:

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:

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

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:

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:

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:

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

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:

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:

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:

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:

[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:

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:

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:

[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:

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:

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:

[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:

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:

dotnet test Server\Framework.Shared.Tests\Framework.Shared.Tests.csproj

Expected: PASS.

  • Step 5: Run server solution build

Run:

dotnet build Server\Server.sln

Expected: PASS.

  • Step 6: Review git diff

Run:

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.