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.csBehaviorVector2、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/Resources/Config/ActorBehavior/fighter_skill.json- 示例技能配置。
Client/Assets/Resources/Config/ActorBehavior/fighter_flyobj.json- 示例飞行物配置。
Client/Assets/Resources/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.csprojalready compilesClient/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.
-
TryStartBehaviorreturningBehaviorStartResult. -
Immediate firing of timeline entries at
time <= 0. -
Tickfiring timeline entries whenpreviousElapsed < entry.time <= currentElapsed. -
Auto-stop when elapsed reaches behavior duration.
-
Run-mode filtering: presentation events only in
ClientPresentation, logic application only inServerLogic. -
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, radius2, angle90, targetEnemy. -
sphere_test:Sphere, radius1.5, targetEnemy. -
ring_test:Ring, minRadius2, radius3.5, targetEnemy. -
line_test:Line, radius2, width0.5, targetEnemy. -
locked_test:Locked, targetHitActor. -
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 withinradius + actor.Radius. -
Ring: distance is betweenminRadiusandradius + actor.Radius. -
Sector: within radius and absolute angle from forward is withinangle / 2. -
Line: distance from actor center to segment[origin, origin + forward * radius]is withinwidth + 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, runOnCollisionlogic effect, increment hit count, expire athitLimit. -
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
BuffTickedwhen elapsed crosses each interval. -
Respect
repeatCount. -
Fire
BuffRemovedwhen duration expires or repeat count is reached. -
Logic operations of type
Buffshould callApplyBuff. -
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/Resources/Config/ActorBehavior/fighter_skill.json - Create:
Client/Assets/Resources/Config/ActorBehavior/fighter_flyobj.json - Create:
Client/Assets/Resources/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", "Resources", "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/Resources/Config/ActorBehavior
Expected: only actor behavior core, tests, and example config files changed.