feat(bt): Add entity-based task factories

Add `leaf`, `sequential`, `parallel`, `selector`, `random`, and `repeat`
functions that return `EntityDef` trees. Legacy object definitions are
still
accepted for backward compatibility. Non-task child entities are ignored
by
the runner, and `buildTree` now stores the root entity on
`TaskRunner.root`.
This commit is contained in:
hyper
2026-06-28 10:15:38 +08:00
parent 968672da06
commit ddde3f7597
6 changed files with 312 additions and 46 deletions
+59 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from "vitest";
import { World, type Entity } from "../src/index";
import { World, defineComponent, entity, type Entity } from "../src/index";
import {
Task,
Scheduled,
@@ -9,6 +9,10 @@ import {
Cancelled,
ChildOf,
TaskRunner,
buildTree,
leaf,
repeat,
sequential,
} from "../src/bt/index";
// ── Helpers ─────────────────────────────────────────
@@ -54,6 +58,60 @@ function makeSelector(world: World, parent?: Entity): Entity {
return e;
}
// ── Entity task factories ───────────────────────────
describe("Entity task factories", () => {
it("materializes non-task child entities and ignores them during execution", () => {
const Label = defineComponent("testLabel", { value: "" });
const world = new World();
const calls: string[] = [];
const runner = buildTree(
world,
sequential([
entity(Label, { value: "sequence metadata" }),
leaf(() => calls.push("a")),
entity(Label, { value: "between leaves" }),
leaf(() => calls.push("b")),
]),
);
const root = runner.root!;
const children = [...world.getRelatedTo(root, ChildOf)];
const labels = children
.filter((child) => world.has(child, Label))
.map((child) => world.get(child, Label).value);
expect(labels).toEqual(["sequence metadata", "between leaves"]);
runner.schedule(root);
for (let i = 0; i < 5; i++) runner.tick();
expect(calls).toEqual(["a", "b"]);
expect(world.has(root, Succeeded)).toBe(true);
});
it("repeat works with component child entities beside its task child", () => {
const Label = defineComponent("repeatLabel", { value: "" });
const world = new World();
let calls = 0;
const runner = buildTree(
world,
repeat([
entity(Label, { value: "repeat metadata" }),
leaf(() => {
calls++;
}),
]),
);
runner.schedule(runner.root!);
for (let i = 0; i < 5; i++) runner.tick();
expect(calls).toBeGreaterThan(1);
});
});
// ── Leaf tasks ──────────────────────────────────────
describe("Leaf tasks", () => {
let world: World;