feat(engine): advanced effect primitives (aura/triggers/conditional)
T3 Wave 2 batch C. Implements 5 advanced primitives — auras and event
triggers — that nest other primitives via childPrimitives() so the
T19 validator can walk the tree for depth/count enforcement:
- add-aura: leaf primitive seeding AuraSpec[] entries
({radius:1-7, targetAttr, delta}). T28 wires the per-move recompute.
- on-turn-start: nesting primitive seeding OnTurnStartHooks (an array of
primitive lists). childPrimitives() returns the inner list.
- on-capture: same pattern with OnCaptureHooks.
- on-damaged: same pattern with OnDamagedHooks.
- conditional: discriminated-union ConditionSpec (attr-lt/gt/eq, always,
never), then[], optional else[]. childPrimitives() returns then+else
combined so the validator can count both branches.
ChessAttrMap gains AuraSpec, OnTurnStartHooks, OnCaptureHooks,
OnDamagedHooks, ConditionalHooks; ConditionSpec is a public discriminated
union. Engine integration (trigger evaluation, aura recompute) deferred
to T22/T28.
The primitives/index.ts barrel now imports all 15 T3 primitives in
batch order (state → mechanic → advanced).
This commit is contained in:
parent
b93b4b7322
commit
ce49b55a60
9 changed files with 587 additions and 1 deletions
123
packages/chess/src/modifiers/primitives/conditional.test.ts
Normal file
123
packages/chess/src/modifiers/primitives/conditional.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { Session } from "@paratype/rete";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChessEngine } from "../../engine.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import { CONDITIONAL_PRIMITIVE } from "./conditional.js";
|
||||
import type { EffectPrimitiveNode, PrimitiveApplyContext } from "./types.js";
|
||||
import "./conditional.js";
|
||||
|
||||
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
|
||||
const session = new Session();
|
||||
const pieceId = session.nextId();
|
||||
const ctx: PrimitiveApplyContext = {
|
||||
engine: new ChessEngine(),
|
||||
session,
|
||||
pieceId,
|
||||
depth: 0,
|
||||
descriptor: {
|
||||
id: "custom:test-conditional",
|
||||
type: "data",
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
return { ctx, session };
|
||||
}
|
||||
|
||||
describe("conditional primitive — registry", () => {
|
||||
it("registers in PRIMITIVE_REGISTRY under key 'conditional'", () => {
|
||||
expect(PRIMITIVE_REGISTRY.has("conditional")).toBe(true);
|
||||
expect(PRIMITIVE_REGISTRY.get("conditional")).toBe(CONDITIONAL_PRIMITIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("conditional primitive — apply()", () => {
|
||||
it("seeds one conditional hook", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const thenBranch: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-to-attribute", params: { attr: "HpBonus", delta: 1 } },
|
||||
];
|
||||
const elseBranch: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-to-attribute", params: { attr: "HpBonus", delta: -1 } },
|
||||
];
|
||||
|
||||
CONDITIONAL_PRIMITIVE.apply(ctx, {
|
||||
condition: { type: "attr-lt", attr: "Hp", value: 3 },
|
||||
then: thenBranch,
|
||||
else: elseBranch,
|
||||
});
|
||||
|
||||
expect(session.get(ctx.pieceId, "ConditionalHooks")).toEqual([
|
||||
{
|
||||
condition: { type: "attr-lt", attr: "Hp", value: 3 },
|
||||
then: thenBranch,
|
||||
else: elseBranch,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("appends to existing ConditionalHooks", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
|
||||
CONDITIONAL_PRIMITIVE.apply(ctx, {
|
||||
condition: { type: "always" },
|
||||
then: [{ kind: "seed-attribute", params: { attr: "Hp", value: 5 } }],
|
||||
});
|
||||
CONDITIONAL_PRIMITIVE.apply(ctx, {
|
||||
condition: { type: "never" },
|
||||
then: [{ kind: "set-capture-flag", params: { flag: 2 } }],
|
||||
else: [{ kind: "set-capture-flag", params: { flag: 1 } }],
|
||||
});
|
||||
|
||||
expect(session.get(ctx.pieceId, "ConditionalHooks")).toEqual([
|
||||
{
|
||||
condition: { type: "always" },
|
||||
then: [{ kind: "seed-attribute", params: { attr: "Hp", value: 5 } }],
|
||||
else: undefined,
|
||||
},
|
||||
{
|
||||
condition: { type: "never" },
|
||||
then: [{ kind: "set-capture-flag", params: { flag: 2 } }],
|
||||
else: [{ kind: "set-capture-flag", params: { flag: 1 } }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("conditional primitive — childPrimitives()", () => {
|
||||
it("returns then + else primitives combined", () => {
|
||||
const thenBranch: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-direction", params: { direction: "forward" } },
|
||||
];
|
||||
const elseBranch: EffectPrimitiveNode[] = [
|
||||
{ kind: "block-move-type", params: { moveType: "capture" } },
|
||||
];
|
||||
|
||||
const children = CONDITIONAL_PRIMITIVE.childPrimitives?.({
|
||||
condition: { type: "always" },
|
||||
then: thenBranch,
|
||||
else: elseBranch,
|
||||
});
|
||||
|
||||
expect(children).toEqual([...thenBranch, ...elseBranch]);
|
||||
});
|
||||
|
||||
it("accepts conditional params with no else branch", () => {
|
||||
const parsed = CONDITIONAL_PRIMITIVE.paramsSchema.parse({
|
||||
condition: { type: "attr-gt", attr: "Hp", value: 2 },
|
||||
then: [{ kind: "seed-attribute", params: { attr: "HpBonus", value: 1 } }],
|
||||
});
|
||||
|
||||
expect(parsed.else).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("conditional primitive — paramsSchema", () => {
|
||||
it("rejects unknown condition.type values", () => {
|
||||
expect(() =>
|
||||
CONDITIONAL_PRIMITIVE.paramsSchema.parse({
|
||||
condition: { type: "attr-between", attr: "Hp", min: 1, max: 3 },
|
||||
then: [],
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
86
packages/chess/src/modifiers/primitives/conditional.ts
Normal file
86
packages/chess/src/modifiers/primitives/conditional.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { z } from "zod";
|
||||
import type { ChessAttrMap, ConditionSpec } from "../../schema.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import type {
|
||||
EffectPrimitive,
|
||||
EffectPrimitiveNode,
|
||||
PrimitiveApplyContext,
|
||||
PrimitiveKind,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Primitive kinds are runtime-validated later by the tree validator (T19).
|
||||
* Keeping this as `string` avoids hard-coding every primitive literal here.
|
||||
*/
|
||||
const NodeSchema: z.ZodType<EffectPrimitiveNode> = z.object({
|
||||
kind: z.string() as z.ZodType<PrimitiveKind>,
|
||||
params: z.unknown(),
|
||||
});
|
||||
|
||||
const ConditionSchema: z.ZodType<ConditionSpec> = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("attr-lt"),
|
||||
attr: z.string(),
|
||||
value: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("attr-gt"),
|
||||
attr: z.string(),
|
||||
value: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("attr-eq"),
|
||||
attr: z.string(),
|
||||
value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("always"),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("never"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const schema = z.object({
|
||||
condition: ConditionSchema,
|
||||
then: z.array(NodeSchema),
|
||||
else: z.array(NodeSchema).optional(),
|
||||
});
|
||||
type Params = z.infer<typeof schema>;
|
||||
type ConditionalHook = ChessAttrMap["ConditionalHooks"][number];
|
||||
|
||||
const descriptor: EffectPrimitive<Params> = {
|
||||
kind: "conditional",
|
||||
label: "Conditional",
|
||||
description:
|
||||
"Seeds ConditionalHooks entries consumed by the trigger evaluation pipeline.",
|
||||
paramsSchema: schema,
|
||||
apply(ctx: PrimitiveApplyContext, params: Params): void {
|
||||
const existing =
|
||||
(ctx.session.get(ctx.pieceId, "ConditionalHooks") as
|
||||
| ChessAttrMap["ConditionalHooks"]
|
||||
| undefined) ?? [];
|
||||
|
||||
let next: ConditionalHook;
|
||||
if (params.else !== undefined) {
|
||||
next = {
|
||||
condition: params.condition,
|
||||
then: [...params.then],
|
||||
else: [...params.else],
|
||||
};
|
||||
} else {
|
||||
next = {
|
||||
condition: params.condition,
|
||||
then: [...params.then],
|
||||
};
|
||||
}
|
||||
|
||||
ctx.session.insert(ctx.pieceId, "ConditionalHooks", [...existing, next]);
|
||||
},
|
||||
childPrimitives(params: Params): EffectPrimitiveNode[] {
|
||||
return [...params.then, ...(params.else ?? [])];
|
||||
},
|
||||
};
|
||||
|
||||
PRIMITIVE_REGISTRY.register(descriptor);
|
||||
export { descriptor as CONDITIONAL_PRIMITIVE };
|
||||
|
|
@ -8,4 +8,23 @@ export type {
|
|||
Session,
|
||||
} from "./types.js";
|
||||
|
||||
// Side-effect registration of individual primitives is added by Wave 2 (T4-T18).
|
||||
// State primitives (Wave 2 batch A — T4–T8):
|
||||
import "./seed-attribute.js";
|
||||
import "./add-to-attribute.js";
|
||||
import "./multiply-attribute.js";
|
||||
import "./add-direction.js";
|
||||
import "./set-capture-flag.js";
|
||||
|
||||
// Mechanic primitives (Wave 2 batch B — T9–T13):
|
||||
import "./absorb-damage-with-attribute.js";
|
||||
import "./reflect-damage.js";
|
||||
import "./modify-movement-range.js";
|
||||
import "./block-move-type.js";
|
||||
import "./override-promotion.js";
|
||||
|
||||
// Advanced primitives (Wave 2 batch C — T14–T18):
|
||||
import "./add-aura.js";
|
||||
import "./on-turn-start.js";
|
||||
import "./on-capture.js";
|
||||
import "./on-damaged.js";
|
||||
import "./conditional.js";
|
||||
|
|
|
|||
72
packages/chess/src/modifiers/primitives/on-capture.test.ts
Normal file
72
packages/chess/src/modifiers/primitives/on-capture.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Session } from "@paratype/rete";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChessEngine } from "../../engine.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import { ON_CAPTURE_PRIMITIVE } from "./on-capture.js";
|
||||
import type { EffectPrimitiveNode, PrimitiveApplyContext } from "./types.js";
|
||||
import "./on-capture.js";
|
||||
|
||||
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
|
||||
const session = new Session();
|
||||
const pieceId = session.nextId();
|
||||
const ctx: PrimitiveApplyContext = {
|
||||
engine: new ChessEngine(),
|
||||
session,
|
||||
pieceId,
|
||||
depth: 0,
|
||||
descriptor: {
|
||||
id: "custom:test-on-capture",
|
||||
type: "data",
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
return { ctx, session };
|
||||
}
|
||||
|
||||
describe("on-capture primitive — registry", () => {
|
||||
it("registers in PRIMITIVE_REGISTRY under key 'on-capture'", () => {
|
||||
expect(PRIMITIVE_REGISTRY.has("on-capture")).toBe(true);
|
||||
expect(PRIMITIVE_REGISTRY.get("on-capture")).toBe(ON_CAPTURE_PRIMITIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-capture primitive — apply()", () => {
|
||||
it("seeds one capture hook", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const primitives: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-to-attribute", params: { attr: "RangeBonus", delta: 1 } },
|
||||
];
|
||||
|
||||
ON_CAPTURE_PRIMITIVE.apply(ctx, { primitives });
|
||||
|
||||
expect(session.get(ctx.pieceId, "OnCaptureHooks")).toEqual([primitives]);
|
||||
});
|
||||
|
||||
it("appends additional hooks to existing OnCaptureHooks", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const first: EffectPrimitiveNode[] = [
|
||||
{ kind: "seed-attribute", params: { attr: "Hp", value: 2 } },
|
||||
];
|
||||
const second: EffectPrimitiveNode[] = [
|
||||
{ kind: "multiply-attribute", params: { attr: "HpBonus", factor: 2 } },
|
||||
];
|
||||
|
||||
ON_CAPTURE_PRIMITIVE.apply(ctx, { primitives: first });
|
||||
ON_CAPTURE_PRIMITIVE.apply(ctx, { primitives: second });
|
||||
|
||||
expect(session.get(ctx.pieceId, "OnCaptureHooks")).toEqual([first, second]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-capture primitive — childPrimitives()", () => {
|
||||
it("returns the inner primitive list for validator tree traversal", () => {
|
||||
const primitives: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-direction", params: { direction: "left" } },
|
||||
{ kind: "set-capture-flag", params: { flag: 2 } },
|
||||
];
|
||||
|
||||
const children = ON_CAPTURE_PRIMITIVE.childPrimitives?.({ primitives });
|
||||
|
||||
expect(children).toEqual(primitives);
|
||||
});
|
||||
});
|
||||
47
packages/chess/src/modifiers/primitives/on-capture.ts
Normal file
47
packages/chess/src/modifiers/primitives/on-capture.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { z } from "zod";
|
||||
import type { ChessAttrMap } from "../../schema.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import type {
|
||||
EffectPrimitive,
|
||||
EffectPrimitiveNode,
|
||||
PrimitiveApplyContext,
|
||||
PrimitiveKind,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Primitive kinds are runtime-validated later by the tree validator (T19).
|
||||
* Keeping this as `string` avoids hard-coding every primitive literal here.
|
||||
*/
|
||||
const NodeSchema: z.ZodType<EffectPrimitiveNode> = z.object({
|
||||
kind: z.string() as z.ZodType<PrimitiveKind>,
|
||||
params: z.unknown(),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
primitives: z.array(NodeSchema),
|
||||
});
|
||||
type Params = z.infer<typeof schema>;
|
||||
|
||||
const descriptor: EffectPrimitive<Params> = {
|
||||
kind: "on-capture",
|
||||
label: "On Capture",
|
||||
description: "Seeds OnCaptureHooks entries consumed during capture events.",
|
||||
paramsSchema: schema,
|
||||
apply(ctx: PrimitiveApplyContext, params: Params): void {
|
||||
const existing =
|
||||
(ctx.session.get(ctx.pieceId, "OnCaptureHooks") as
|
||||
| ChessAttrMap["OnCaptureHooks"]
|
||||
| undefined) ?? [];
|
||||
|
||||
ctx.session.insert(ctx.pieceId, "OnCaptureHooks", [
|
||||
...existing,
|
||||
[...params.primitives],
|
||||
]);
|
||||
},
|
||||
childPrimitives(params: Params): EffectPrimitiveNode[] {
|
||||
return [...params.primitives];
|
||||
},
|
||||
};
|
||||
|
||||
PRIMITIVE_REGISTRY.register(descriptor);
|
||||
export { descriptor as ON_CAPTURE_PRIMITIVE };
|
||||
72
packages/chess/src/modifiers/primitives/on-damaged.test.ts
Normal file
72
packages/chess/src/modifiers/primitives/on-damaged.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Session } from "@paratype/rete";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChessEngine } from "../../engine.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import { ON_DAMAGED_PRIMITIVE } from "./on-damaged.js";
|
||||
import type { EffectPrimitiveNode, PrimitiveApplyContext } from "./types.js";
|
||||
import "./on-damaged.js";
|
||||
|
||||
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
|
||||
const session = new Session();
|
||||
const pieceId = session.nextId();
|
||||
const ctx: PrimitiveApplyContext = {
|
||||
engine: new ChessEngine(),
|
||||
session,
|
||||
pieceId,
|
||||
depth: 0,
|
||||
descriptor: {
|
||||
id: "custom:test-on-damaged",
|
||||
type: "data",
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
return { ctx, session };
|
||||
}
|
||||
|
||||
describe("on-damaged primitive — registry", () => {
|
||||
it("registers in PRIMITIVE_REGISTRY under key 'on-damaged'", () => {
|
||||
expect(PRIMITIVE_REGISTRY.has("on-damaged")).toBe(true);
|
||||
expect(PRIMITIVE_REGISTRY.get("on-damaged")).toBe(ON_DAMAGED_PRIMITIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-damaged primitive — apply()", () => {
|
||||
it("seeds one damaged hook", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const primitives: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-to-attribute", params: { attr: "HpBonus", delta: -1 } },
|
||||
];
|
||||
|
||||
ON_DAMAGED_PRIMITIVE.apply(ctx, { primitives });
|
||||
|
||||
expect(session.get(ctx.pieceId, "OnDamagedHooks")).toEqual([primitives]);
|
||||
});
|
||||
|
||||
it("appends additional hooks to existing OnDamagedHooks", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const first: EffectPrimitiveNode[] = [
|
||||
{ kind: "seed-attribute", params: { attr: "Hp", value: 1 } },
|
||||
];
|
||||
const second: EffectPrimitiveNode[] = [
|
||||
{ kind: "reflect-damage", params: { percent: 0.5 } },
|
||||
];
|
||||
|
||||
ON_DAMAGED_PRIMITIVE.apply(ctx, { primitives: first });
|
||||
ON_DAMAGED_PRIMITIVE.apply(ctx, { primitives: second });
|
||||
|
||||
expect(session.get(ctx.pieceId, "OnDamagedHooks")).toEqual([first, second]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-damaged primitive — childPrimitives()", () => {
|
||||
it("returns the inner primitive list for validator tree traversal", () => {
|
||||
const primitives: EffectPrimitiveNode[] = [
|
||||
{ kind: "absorb-damage-with-attribute", params: { attr: "Hp", rate: 1 } },
|
||||
{ kind: "set-capture-flag", params: { flag: 4 } },
|
||||
];
|
||||
|
||||
const children = ON_DAMAGED_PRIMITIVE.childPrimitives?.({ primitives });
|
||||
|
||||
expect(children).toEqual(primitives);
|
||||
});
|
||||
});
|
||||
47
packages/chess/src/modifiers/primitives/on-damaged.ts
Normal file
47
packages/chess/src/modifiers/primitives/on-damaged.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { z } from "zod";
|
||||
import type { ChessAttrMap } from "../../schema.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import type {
|
||||
EffectPrimitive,
|
||||
EffectPrimitiveNode,
|
||||
PrimitiveApplyContext,
|
||||
PrimitiveKind,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Primitive kinds are runtime-validated later by the tree validator (T19).
|
||||
* Keeping this as `string` avoids hard-coding every primitive literal here.
|
||||
*/
|
||||
const NodeSchema: z.ZodType<EffectPrimitiveNode> = z.object({
|
||||
kind: z.string() as z.ZodType<PrimitiveKind>,
|
||||
params: z.unknown(),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
primitives: z.array(NodeSchema),
|
||||
});
|
||||
type Params = z.infer<typeof schema>;
|
||||
|
||||
const descriptor: EffectPrimitive<Params> = {
|
||||
kind: "on-damaged",
|
||||
label: "On Damaged",
|
||||
description: "Seeds OnDamagedHooks entries consumed during damage events.",
|
||||
paramsSchema: schema,
|
||||
apply(ctx: PrimitiveApplyContext, params: Params): void {
|
||||
const existing =
|
||||
(ctx.session.get(ctx.pieceId, "OnDamagedHooks") as
|
||||
| ChessAttrMap["OnDamagedHooks"]
|
||||
| undefined) ?? [];
|
||||
|
||||
ctx.session.insert(ctx.pieceId, "OnDamagedHooks", [
|
||||
...existing,
|
||||
[...params.primitives],
|
||||
]);
|
||||
},
|
||||
childPrimitives(params: Params): EffectPrimitiveNode[] {
|
||||
return [...params.primitives];
|
||||
},
|
||||
};
|
||||
|
||||
PRIMITIVE_REGISTRY.register(descriptor);
|
||||
export { descriptor as ON_DAMAGED_PRIMITIVE };
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { Session } from "@paratype/rete";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChessEngine } from "../../engine.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import { ON_TURN_START_PRIMITIVE } from "./on-turn-start.js";
|
||||
import type { EffectPrimitiveNode, PrimitiveApplyContext } from "./types.js";
|
||||
import "./on-turn-start.js";
|
||||
|
||||
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
|
||||
const session = new Session();
|
||||
const pieceId = session.nextId();
|
||||
const ctx: PrimitiveApplyContext = {
|
||||
engine: new ChessEngine(),
|
||||
session,
|
||||
pieceId,
|
||||
depth: 0,
|
||||
descriptor: {
|
||||
id: "custom:test-on-turn-start",
|
||||
type: "data",
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
return { ctx, session };
|
||||
}
|
||||
|
||||
describe("on-turn-start primitive — registry", () => {
|
||||
it("registers in PRIMITIVE_REGISTRY under key 'on-turn-start'", () => {
|
||||
expect(PRIMITIVE_REGISTRY.has("on-turn-start")).toBe(true);
|
||||
expect(PRIMITIVE_REGISTRY.get("on-turn-start")).toBe(ON_TURN_START_PRIMITIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-turn-start primitive — apply()", () => {
|
||||
it("seeds one turn-start hook", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const primitives: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-to-attribute", params: { attr: "HpBonus", delta: 1 } },
|
||||
];
|
||||
|
||||
ON_TURN_START_PRIMITIVE.apply(ctx, { primitives });
|
||||
|
||||
expect(session.get(ctx.pieceId, "OnTurnStartHooks")).toEqual([primitives]);
|
||||
});
|
||||
|
||||
it("appends additional hooks to existing OnTurnStartHooks", () => {
|
||||
const { ctx, session } = makeContext();
|
||||
const first: EffectPrimitiveNode[] = [
|
||||
{ kind: "seed-attribute", params: { attr: "Hp", value: 3 } },
|
||||
];
|
||||
const second: EffectPrimitiveNode[] = [
|
||||
{ kind: "set-capture-flag", params: { flag: 1 } },
|
||||
];
|
||||
|
||||
ON_TURN_START_PRIMITIVE.apply(ctx, { primitives: first });
|
||||
ON_TURN_START_PRIMITIVE.apply(ctx, { primitives: second });
|
||||
|
||||
expect(session.get(ctx.pieceId, "OnTurnStartHooks")).toEqual([first, second]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on-turn-start primitive — childPrimitives()", () => {
|
||||
it("returns the inner primitive list for validator tree traversal", () => {
|
||||
const primitives: EffectPrimitiveNode[] = [
|
||||
{ kind: "add-direction", params: { direction: "backward" } },
|
||||
{ kind: "block-move-type", params: { moveType: "slide" } },
|
||||
];
|
||||
|
||||
const children = ON_TURN_START_PRIMITIVE.childPrimitives?.({ primitives });
|
||||
|
||||
expect(children).toEqual(primitives);
|
||||
});
|
||||
});
|
||||
48
packages/chess/src/modifiers/primitives/on-turn-start.ts
Normal file
48
packages/chess/src/modifiers/primitives/on-turn-start.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { z } from "zod";
|
||||
import type { ChessAttrMap } from "../../schema.js";
|
||||
import { PRIMITIVE_REGISTRY } from "./registry.js";
|
||||
import type {
|
||||
EffectPrimitive,
|
||||
EffectPrimitiveNode,
|
||||
PrimitiveApplyContext,
|
||||
PrimitiveKind,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Primitive kinds are runtime-validated later by the tree validator (T19).
|
||||
* Keeping this as `string` avoids hard-coding every primitive literal here.
|
||||
*/
|
||||
const NodeSchema: z.ZodType<EffectPrimitiveNode> = z.object({
|
||||
kind: z.string() as z.ZodType<PrimitiveKind>,
|
||||
params: z.unknown(),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
primitives: z.array(NodeSchema),
|
||||
});
|
||||
type Params = z.infer<typeof schema>;
|
||||
|
||||
const descriptor: EffectPrimitive<Params> = {
|
||||
kind: "on-turn-start",
|
||||
label: "On Turn Start",
|
||||
description:
|
||||
"Seeds OnTurnStartHooks entries consumed during the engine's turn-start phase.",
|
||||
paramsSchema: schema,
|
||||
apply(ctx: PrimitiveApplyContext, params: Params): void {
|
||||
const existing =
|
||||
(ctx.session.get(ctx.pieceId, "OnTurnStartHooks") as
|
||||
| ChessAttrMap["OnTurnStartHooks"]
|
||||
| undefined) ?? [];
|
||||
|
||||
ctx.session.insert(ctx.pieceId, "OnTurnStartHooks", [
|
||||
...existing,
|
||||
[...params.primitives],
|
||||
]);
|
||||
},
|
||||
childPrimitives(params: Params): EffectPrimitiveNode[] {
|
||||
return [...params.primitives];
|
||||
},
|
||||
};
|
||||
|
||||
PRIMITIVE_REGISTRY.register(descriptor);
|
||||
export { descriptor as ON_TURN_START_PRIMITIVE };
|
||||
Loading…
Add table
Add a link
Reference in a new issue