feat(bt): Add wait and whilst, rename leaf to action
This commit is contained in:
+250
-131
@@ -10,15 +10,18 @@ import {
|
||||
ChildOf,
|
||||
TaskRunner,
|
||||
buildTree,
|
||||
leaf,
|
||||
Cancel,
|
||||
action,
|
||||
wait,
|
||||
cycle,
|
||||
whilst,
|
||||
sequential,
|
||||
} from "../src/bt/index";
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────
|
||||
function makeLeaf(world: World, parent?: Entity): Entity {
|
||||
function makeWait(world: World, parent?: Entity): Entity {
|
||||
const e = world.spawn();
|
||||
world.add(e, Task, { kind: "leaf" });
|
||||
world.add(e, Task, { kind: "wait" });
|
||||
if (parent) world.relate(e, ChildOf, parent);
|
||||
return e;
|
||||
}
|
||||
@@ -69,9 +72,9 @@ describe("Entity task factories", () => {
|
||||
world,
|
||||
sequential([
|
||||
entity(Label, { value: "sequence metadata" }),
|
||||
leaf(() => calls.push("a")),
|
||||
action(() => calls.push("a")),
|
||||
entity(Label, { value: "between leaves" }),
|
||||
leaf(() => calls.push("b")),
|
||||
action(() => calls.push("b")),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -90,6 +93,122 @@ describe("Entity task factories", () => {
|
||||
expect(world.has(root, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
it("action succeeds immediately when it returns", () => {
|
||||
const world = new World();
|
||||
let receivedDt = 0;
|
||||
let receivedEntity: Entity | undefined;
|
||||
|
||||
const runner = buildTree(
|
||||
world,
|
||||
action((_world, entity, dt) => {
|
||||
receivedEntity = entity;
|
||||
receivedDt = dt;
|
||||
}),
|
||||
);
|
||||
|
||||
runner.schedule(runner.root!);
|
||||
runner.tick(16);
|
||||
|
||||
expect(receivedEntity).toBe(runner.root);
|
||||
expect(receivedDt).toBe(16);
|
||||
expect(world.has(runner.root!, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
it("action fails or cancels when it throws", () => {
|
||||
const world = new World();
|
||||
const failed = buildTree(
|
||||
world,
|
||||
action(() => {
|
||||
throw new Error("bad");
|
||||
}),
|
||||
);
|
||||
const cancelled = buildTree(
|
||||
world,
|
||||
action(() => {
|
||||
throw Cancel;
|
||||
}),
|
||||
);
|
||||
|
||||
failed.schedule(failed.root!);
|
||||
failed.tick();
|
||||
cancelled.schedule(cancelled.root!);
|
||||
cancelled.tick();
|
||||
|
||||
expect(world.has(failed.root!, Failed)).toBe(true);
|
||||
expect(world.has(cancelled.root!, Cancelled)).toBe(true);
|
||||
});
|
||||
|
||||
it("wait can complete itself through task control", () => {
|
||||
const world = new World();
|
||||
const runner = buildTree(
|
||||
world,
|
||||
wait((_world, _entity, task) => {
|
||||
task.succeed();
|
||||
}),
|
||||
);
|
||||
|
||||
runner.schedule(runner.root!);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(runner.root!, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
it("wait without a starter remains running", () => {
|
||||
const world = new World();
|
||||
const runner = buildTree(world, wait());
|
||||
|
||||
runner.schedule(runner.root!);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(runner.root!, Running)).toBe(true);
|
||||
});
|
||||
|
||||
it("whilst loops at tick boundaries until its condition is false", () => {
|
||||
const world = new World();
|
||||
let count = 0;
|
||||
const runner = buildTree(
|
||||
world,
|
||||
whilst(
|
||||
() => count < 3,
|
||||
action(() => {
|
||||
count++;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
runner.schedule(runner.root!);
|
||||
runner.tick();
|
||||
expect(count).toBe(1);
|
||||
expect(world.has(runner.root!, Scheduled)).toBe(true);
|
||||
|
||||
runner.tick();
|
||||
expect(count).toBe(2);
|
||||
|
||||
runner.tick();
|
||||
expect(count).toBe(3);
|
||||
|
||||
runner.tick();
|
||||
expect(world.has(runner.root!, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
it("whilst propagates child failure", () => {
|
||||
const world = new World();
|
||||
const runner = buildTree(
|
||||
world,
|
||||
whilst(
|
||||
() => true,
|
||||
action(() => {
|
||||
throw new Error("bad");
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
runner.schedule(runner.root!);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(runner.root!, Failed)).toBe(true);
|
||||
});
|
||||
|
||||
it("cycle works with component child entities beside its task child", () => {
|
||||
const Label = defineComponent("cycleLabel", { value: "" });
|
||||
const world = new World();
|
||||
@@ -99,7 +218,7 @@ describe("Entity task factories", () => {
|
||||
world,
|
||||
cycle([
|
||||
entity(Label, { value: "cycle metadata" }),
|
||||
leaf(() => {
|
||||
action(() => {
|
||||
calls++;
|
||||
}),
|
||||
]),
|
||||
@@ -112,8 +231,8 @@ describe("Entity task factories", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Leaf tasks ──────────────────────────────────────
|
||||
describe("Leaf tasks", () => {
|
||||
// ── Wait tasks ──────────────────────────────────────
|
||||
describe("Wait tasks", () => {
|
||||
let world: World;
|
||||
let runner: TaskRunner;
|
||||
|
||||
@@ -122,79 +241,79 @@ describe("Leaf tasks", () => {
|
||||
runner = new TaskRunner(world);
|
||||
});
|
||||
|
||||
it("calls onLeaf when a leaf is scheduled and ticked", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
it("calls onWait when a wait task is scheduled and ticked", () => {
|
||||
const action = makeWait(world);
|
||||
const calls: Entity[] = [];
|
||||
|
||||
runner.onLeaf = (_w, e) => calls.push(e);
|
||||
runner.schedule(leaf);
|
||||
runner.onWait = (_w, e) => calls.push(e);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
|
||||
expect(calls).toEqual([leaf]);
|
||||
expect(calls).toEqual([action]);
|
||||
});
|
||||
|
||||
it("marks leaf as Running after tick", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
runner.schedule(leaf);
|
||||
it("marks wait task as Running after tick", () => {
|
||||
const action = makeWait(world);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(leaf, Running)).toBe(true);
|
||||
expect(world.has(leaf, Scheduled)).toBe(false);
|
||||
expect(world.has(action, Running)).toBe(true);
|
||||
expect(world.has(action, Scheduled)).toBe(false);
|
||||
});
|
||||
|
||||
it("succeed() marks leaf as Succeeded", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
runner.schedule(leaf);
|
||||
it("succeed() marks wait task as Succeeded", () => {
|
||||
const action = makeWait(world);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
runner.succeed(action);
|
||||
|
||||
expect(world.has(leaf, Succeeded)).toBe(true);
|
||||
expect(world.has(leaf, Running)).toBe(false);
|
||||
expect(world.has(action, Succeeded)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(false);
|
||||
});
|
||||
|
||||
it("fail() marks leaf as Failed", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
runner.schedule(leaf);
|
||||
it("fail() marks wait task as Failed", () => {
|
||||
const action = makeWait(world);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
runner.fail(leaf);
|
||||
runner.fail(action);
|
||||
|
||||
expect(world.has(leaf, Failed)).toBe(true);
|
||||
expect(world.has(leaf, Running)).toBe(false);
|
||||
expect(world.has(action, Failed)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(false);
|
||||
});
|
||||
|
||||
it("cancel() marks leaf as Cancelled", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
runner.schedule(leaf);
|
||||
it("cancel() marks wait task as Cancelled", () => {
|
||||
const action = makeWait(world);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
runner.cancel(leaf);
|
||||
runner.cancel(action);
|
||||
|
||||
expect(world.has(leaf, Cancelled)).toBe(true);
|
||||
expect(world.has(leaf, Running)).toBe(false);
|
||||
expect(world.has(action, Cancelled)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(false);
|
||||
});
|
||||
|
||||
it("onTerminal is called when leaf finishes", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
it("onTerminal is called when wait task finishes", () => {
|
||||
const action = makeWait(world);
|
||||
const terminals: { entity: Entity; status: string }[] = [];
|
||||
|
||||
runner.onTerminal = (_w, e, s) => terminals.push({ entity: e, status: s });
|
||||
runner.schedule(leaf);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
runner.succeed(action);
|
||||
|
||||
expect(terminals).toEqual([{ entity: leaf, status: "succeeded" }]);
|
||||
expect(terminals).toEqual([{ entity: action, status: "succeeded" }]);
|
||||
});
|
||||
|
||||
it("reset() clears all status tags", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
runner.schedule(leaf);
|
||||
const action = makeWait(world);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
runner.succeed(action);
|
||||
|
||||
runner.reset(leaf);
|
||||
runner.reset(action);
|
||||
|
||||
expect(world.has(leaf, Succeeded)).toBe(false);
|
||||
expect(world.has(leaf, Running)).toBe(false);
|
||||
expect(world.has(leaf, Scheduled)).toBe(false);
|
||||
expect(world.has(action, Succeeded)).toBe(false);
|
||||
expect(world.has(action, Running)).toBe(false);
|
||||
expect(world.has(action, Scheduled)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -210,12 +329,12 @@ describe("Sequential tasks", () => {
|
||||
|
||||
it("runs children one at a time in order", () => {
|
||||
const seq = makeSequential(world);
|
||||
const a = makeLeaf(world, seq);
|
||||
const b = makeLeaf(world, seq);
|
||||
const c = makeLeaf(world, seq);
|
||||
const a = makeWait(world, seq);
|
||||
const b = makeWait(world, seq);
|
||||
const c = makeWait(world, seq);
|
||||
|
||||
const leafCalls: Entity[] = [];
|
||||
runner.onLeaf = (_w, e) => leafCalls.push(e);
|
||||
runner.onWait = (_w, e) => leafCalls.push(e);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick();
|
||||
@@ -236,8 +355,8 @@ describe("Sequential tasks", () => {
|
||||
|
||||
it("fails immediately when a child fails", () => {
|
||||
const seq = makeSequential(world);
|
||||
const a = makeLeaf(world, seq);
|
||||
const b = makeLeaf(world, seq);
|
||||
const a = makeWait(world, seq);
|
||||
const b = makeWait(world, seq);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick(); // schedules a
|
||||
@@ -254,8 +373,8 @@ describe("Sequential tasks", () => {
|
||||
|
||||
it("succeeds when all children succeed", () => {
|
||||
const seq = makeSequential(world);
|
||||
const a = makeLeaf(world, seq);
|
||||
const b = makeLeaf(world, seq);
|
||||
const a = makeWait(world, seq);
|
||||
const b = makeWait(world, seq);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick(); // schedules a
|
||||
@@ -272,20 +391,20 @@ describe("Sequential tasks", () => {
|
||||
it("propagates terminal to grandparent", () => {
|
||||
const root = makeSequential(world);
|
||||
const child = makeSequential(world, root);
|
||||
const leaf = makeLeaf(world, child);
|
||||
const action = makeWait(world, child);
|
||||
|
||||
const terminals: Entity[] = [];
|
||||
runner.onTerminal = (_w, e) => terminals.push(e);
|
||||
|
||||
runner.schedule(root);
|
||||
runner.tick(); // schedules child
|
||||
runner.tick(); // schedules leaf
|
||||
runner.tick(); // runs leaf
|
||||
runner.succeed(leaf);
|
||||
runner.tick(); // schedules action
|
||||
runner.tick(); // runs action
|
||||
runner.succeed(action);
|
||||
runner.tick(); // child succeeds
|
||||
runner.tick(); // root succeeds
|
||||
|
||||
expect(terminals).toEqual([leaf, child, root]);
|
||||
expect(terminals).toEqual([action, child, root]);
|
||||
expect(world.has(root, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -311,9 +430,9 @@ describe("Parallel tasks", () => {
|
||||
|
||||
it("starts all children at once", () => {
|
||||
const par = makeParallel(world);
|
||||
const a = makeLeaf(world, par);
|
||||
const b = makeLeaf(world, par);
|
||||
const c = makeLeaf(world, par);
|
||||
const a = makeWait(world, par);
|
||||
const b = makeWait(world, par);
|
||||
const c = makeWait(world, par);
|
||||
|
||||
runner.schedule(par);
|
||||
runner.tick();
|
||||
@@ -325,8 +444,8 @@ describe("Parallel tasks", () => {
|
||||
|
||||
it("succeeds when all children succeed", () => {
|
||||
const par = makeParallel(world);
|
||||
const a = makeLeaf(world, par);
|
||||
const b = makeLeaf(world, par);
|
||||
const a = makeWait(world, par);
|
||||
const b = makeWait(world, par);
|
||||
|
||||
runner.schedule(par);
|
||||
runner.tick(); // schedules both
|
||||
@@ -355,8 +474,8 @@ describe("Parallel tasks", () => {
|
||||
|
||||
it("fails immediately when any child fails", () => {
|
||||
const par = makeParallel(world);
|
||||
const a = makeLeaf(world, par);
|
||||
const b = makeLeaf(world, par);
|
||||
const a = makeWait(world, par);
|
||||
const b = makeWait(world, par);
|
||||
|
||||
runner.schedule(par);
|
||||
runner.tick(); // schedules both
|
||||
@@ -391,8 +510,8 @@ describe("Random tasks", () => {
|
||||
|
||||
it("picks one child and succeeds/fails with it", () => {
|
||||
const rand = makeRandom(world);
|
||||
const a = makeLeaf(world, rand);
|
||||
const b = makeLeaf(world, rand);
|
||||
const a = makeWait(world, rand);
|
||||
const b = makeWait(world, rand);
|
||||
|
||||
runner.schedule(rand);
|
||||
runner.tick();
|
||||
@@ -409,7 +528,7 @@ describe("Random tasks", () => {
|
||||
|
||||
it("fails when picked child fails", () => {
|
||||
const rand = makeRandom(world);
|
||||
const a = makeLeaf(world, rand);
|
||||
const a = makeWait(world, rand);
|
||||
|
||||
runner.schedule(rand);
|
||||
runner.tick(); // schedules a (only child)
|
||||
@@ -444,55 +563,55 @@ describe("Cycle tasks", () => {
|
||||
|
||||
it("re-runs child after it succeeds", () => {
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
const action = makeWait(world, cyc);
|
||||
|
||||
let leafCount = 0;
|
||||
runner.onLeaf = () => leafCount++;
|
||||
let waitCount = 0;
|
||||
runner.onWait = () => waitCount++;
|
||||
|
||||
runner.schedule(cyc);
|
||||
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(1);
|
||||
runner.succeed(leaf);
|
||||
expect(waitCount).toBe(1);
|
||||
runner.succeed(action);
|
||||
|
||||
// Completion schedules the next cycle at a tick boundary.
|
||||
expect(world.has(leaf, Scheduled)).toBe(true);
|
||||
// Completion schedules the cycle node at a tick boundary.
|
||||
expect(world.has(cyc, Scheduled)).toBe(true);
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(2);
|
||||
runner.succeed(leaf);
|
||||
expect(waitCount).toBe(2);
|
||||
runner.succeed(action);
|
||||
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(3);
|
||||
expect(waitCount).toBe(3);
|
||||
});
|
||||
|
||||
it("re-runs child after it fails", () => {
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
const action = makeWait(world, cyc);
|
||||
|
||||
let leafCount = 0;
|
||||
runner.onLeaf = () => leafCount++;
|
||||
let waitCount = 0;
|
||||
runner.onWait = () => waitCount++;
|
||||
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
runner.fail(leaf);
|
||||
runner.fail(action);
|
||||
|
||||
expect(world.has(leaf, Scheduled)).toBe(true);
|
||||
expect(world.has(cyc, Scheduled)).toBe(true);
|
||||
runner.tick();
|
||||
expect(leafCount).toBe(2);
|
||||
expect(waitCount).toBe(2);
|
||||
});
|
||||
|
||||
it("never terminates on its own", () => {
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
const action = makeWait(world, cyc);
|
||||
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
runner.succeed(action);
|
||||
|
||||
// After many cycles, cycle is still not terminal
|
||||
for (let i = 0; i < 5; i++) {
|
||||
runner.tick();
|
||||
runner.succeed(leaf);
|
||||
runner.succeed(action);
|
||||
}
|
||||
|
||||
expect(world.has(cyc, Succeeded)).toBe(false);
|
||||
@@ -502,7 +621,7 @@ describe("Cycle tasks", () => {
|
||||
|
||||
it("can be cancelled", () => {
|
||||
const cyc = makeCycle(world);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
const action = makeWait(world, cyc);
|
||||
|
||||
runner.schedule(cyc);
|
||||
runner.tick();
|
||||
@@ -510,7 +629,7 @@ describe("Cycle tasks", () => {
|
||||
runner.cancel(cyc);
|
||||
|
||||
expect(world.has(cyc, Cancelled)).toBe(true);
|
||||
expect(world.has(leaf, Cancelled)).toBe(true);
|
||||
expect(world.has(action, Cancelled)).toBe(true);
|
||||
});
|
||||
|
||||
it("empty cycle does nothing", () => {
|
||||
@@ -527,8 +646,8 @@ describe("Cycle tasks", () => {
|
||||
it("cycle inside sequential advances parent when cancelled", () => {
|
||||
const seq = makeSequential(world);
|
||||
const cyc = makeCycle(world, seq);
|
||||
const leaf = makeLeaf(world, cyc);
|
||||
const after = makeLeaf(world, seq);
|
||||
const action = makeWait(world, cyc);
|
||||
const after = makeWait(world, seq);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick();
|
||||
@@ -553,8 +672,8 @@ describe("Selector tasks", () => {
|
||||
|
||||
it("succeeds on first child that succeeds", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
const a = makeWait(world, sel);
|
||||
const b = makeWait(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
@@ -571,9 +690,9 @@ describe("Selector tasks", () => {
|
||||
|
||||
it("tries next child when previous fails", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
const c = makeLeaf(world, sel);
|
||||
const a = makeWait(world, sel);
|
||||
const b = makeWait(world, sel);
|
||||
const c = makeWait(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
@@ -591,8 +710,8 @@ describe("Selector tasks", () => {
|
||||
|
||||
it("fails when all children fail", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
const a = makeWait(world, sel);
|
||||
const b = makeWait(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
@@ -617,8 +736,8 @@ describe("Selector tasks", () => {
|
||||
|
||||
it("skips cancelled children and continues", () => {
|
||||
const sel = makeSelector(world);
|
||||
const a = makeLeaf(world, sel);
|
||||
const b = makeLeaf(world, sel);
|
||||
const a = makeWait(world, sel);
|
||||
const b = makeWait(world, sel);
|
||||
|
||||
runner.schedule(sel);
|
||||
runner.tick(); // schedules a
|
||||
@@ -642,8 +761,8 @@ describe("Cancel", () => {
|
||||
it("cancels all descendants", () => {
|
||||
const root = makeSequential(world);
|
||||
const child = makeParallel(world, root);
|
||||
const a = makeLeaf(world, child);
|
||||
const b = makeLeaf(world, child);
|
||||
const a = makeWait(world, child);
|
||||
const b = makeWait(world, child);
|
||||
|
||||
runner.schedule(root);
|
||||
runner.tick(); // schedules child
|
||||
@@ -662,17 +781,17 @@ describe("Cancel", () => {
|
||||
it("cancel propagates to parent", () => {
|
||||
const root = makeSequential(world);
|
||||
const child = makeSequential(world, root);
|
||||
const leaf = makeLeaf(world, child);
|
||||
const action = makeWait(world, child);
|
||||
|
||||
runner.schedule(root);
|
||||
runner.tick(); // schedules child
|
||||
runner.tick(); // schedules leaf
|
||||
runner.tick(); // runs leaf
|
||||
runner.tick(); // schedules action
|
||||
runner.tick(); // runs action
|
||||
|
||||
runner.cancel(leaf);
|
||||
runner.cancel(action);
|
||||
|
||||
// leaf cancelled → child re-scheduled → child sees leaf cancelled → child cancelled
|
||||
runner.tick(); // child processes cancelled leaf
|
||||
// action cancelled → child re-scheduled → child sees action cancelled → child cancelled
|
||||
runner.tick(); // child processes cancelled action
|
||||
expect(world.has(child, Cancelled)).toBe(true);
|
||||
|
||||
// child cancelled → root re-scheduled → root sees child cancelled → root cancelled
|
||||
@@ -681,8 +800,8 @@ describe("Cancel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Multi-frame leaves ──────────────────────────────
|
||||
describe("Multi-frame leaves", () => {
|
||||
// ── Multi-frame wait tasks ──────────────────────────────
|
||||
describe("Multi-frame wait tasks", () => {
|
||||
let world: World;
|
||||
let runner: TaskRunner;
|
||||
|
||||
@@ -691,28 +810,28 @@ describe("Multi-frame leaves", () => {
|
||||
runner = new TaskRunner(world);
|
||||
});
|
||||
|
||||
it("leaf stays Running across ticks until explicitly finished", () => {
|
||||
const leaf = makeLeaf(world);
|
||||
it("wait task stays Running across ticks until explicitly finished", () => {
|
||||
const action = makeWait(world);
|
||||
|
||||
runner.schedule(leaf);
|
||||
runner.schedule(action);
|
||||
runner.tick();
|
||||
|
||||
expect(world.has(leaf, Running)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(true);
|
||||
|
||||
// Tick again — leaf is Running, not Scheduled, so nothing happens
|
||||
// Tick again — action is Running, not Scheduled, so nothing happens
|
||||
runner.tick();
|
||||
expect(world.has(leaf, Running)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(true);
|
||||
|
||||
// External system finishes it
|
||||
runner.succeed(leaf);
|
||||
expect(world.has(leaf, Succeeded)).toBe(true);
|
||||
expect(world.has(leaf, Running)).toBe(false);
|
||||
runner.succeed(action);
|
||||
expect(world.has(action, Succeeded)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(false);
|
||||
});
|
||||
|
||||
it("sequential waits for multi-frame leaf before advancing", () => {
|
||||
it("sequential waits for multi-frame wait before advancing", () => {
|
||||
const seq = makeSequential(world);
|
||||
const a = makeLeaf(world, seq);
|
||||
const b = makeLeaf(world, seq);
|
||||
const a = makeWait(world, seq);
|
||||
const b = makeWait(world, seq);
|
||||
|
||||
runner.schedule(seq);
|
||||
runner.tick(); // schedules a
|
||||
@@ -754,11 +873,11 @@ describe("Edge cases", () => {
|
||||
it("tick is a no-op when nothing is scheduled", () => {
|
||||
const world = new World();
|
||||
const runner = new TaskRunner(world);
|
||||
const leaf = makeLeaf(world);
|
||||
const action = makeWait(world);
|
||||
|
||||
// Leaf exists but is not Scheduled
|
||||
// Wait task exists but is not Scheduled
|
||||
expect(() => runner.tick()).not.toThrow();
|
||||
expect(world.has(leaf, Running)).toBe(false);
|
||||
expect(world.has(action, Running)).toBe(false);
|
||||
});
|
||||
|
||||
it("deeply nested tree works correctly", () => {
|
||||
@@ -767,14 +886,14 @@ describe("Edge cases", () => {
|
||||
|
||||
const root = makeSequential(world);
|
||||
const mid = makeSequential(world, root);
|
||||
const leaf = makeLeaf(world, mid);
|
||||
const action = makeWait(world, mid);
|
||||
|
||||
runner.schedule(root);
|
||||
|
||||
runner.tick();
|
||||
expect(world.has(leaf, Running)).toBe(true);
|
||||
expect(world.has(action, Running)).toBe(true);
|
||||
|
||||
runner.succeed(leaf);
|
||||
runner.succeed(action);
|
||||
expect(world.has(mid, Succeeded)).toBe(true);
|
||||
expect(world.has(root, Succeeded)).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user