feat(engine): hp-bonus modifier descriptor
This commit is contained in:
parent
f566c3a488
commit
7d27620507
4 changed files with 96 additions and 3 deletions
58
packages/chess/src/modifiers/descriptors/hp-bonus.test.ts
Normal file
58
packages/chess/src/modifiers/descriptors/hp-bonus.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { Session } from "@paratype/rete";
|
||||
import { MODIFIER_REGISTRY } from "../registry.js";
|
||||
import "./hp-bonus.js";
|
||||
import { HP_BONUS_DESCRIPTOR } from "./hp-bonus.js";
|
||||
|
||||
describe("hp-bonus descriptor — registry", () => {
|
||||
it("registers in MODIFIER_REGISTRY under key 'hp-bonus'", () => {
|
||||
expect(MODIFIER_REGISTRY.has("hp-bonus")).toBe(true);
|
||||
expect(MODIFIER_REGISTRY.get("hp-bonus")).toBe(HP_BONUS_DESCRIPTOR);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hp-bonus descriptor — describe()", () => {
|
||||
it("formats positive values with a leading '+'", () => {
|
||||
expect(HP_BONUS_DESCRIPTOR.describe(3)).toBe("HP +3");
|
||||
});
|
||||
|
||||
it("formats negative values without an extra sign", () => {
|
||||
expect(HP_BONUS_DESCRIPTOR.describe(-1)).toBe("HP -1");
|
||||
});
|
||||
|
||||
it("formats zero as '+'", () => {
|
||||
expect(HP_BONUS_DESCRIPTOR.describe(0)).toBe("HP +0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hp-bonus descriptor — apply()", () => {
|
||||
it("seeds HpBonus fact on piece entity in session", () => {
|
||||
const session = new Session();
|
||||
const pieceId = session.nextId();
|
||||
// Seed a PieceType fact so it's a "real" piece entity.
|
||||
session.insert(pieceId, "PieceType", "pawn");
|
||||
|
||||
HP_BONUS_DESCRIPTOR.apply(session, pieceId, 3);
|
||||
|
||||
expect(session.contains(pieceId, "HpBonus")).toBe(true);
|
||||
expect(session.get(pieceId, "HpBonus")).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hp-bonus descriptor — valueSchema", () => {
|
||||
const schema = HP_BONUS_DESCRIPTOR.valueSchema;
|
||||
|
||||
it("accepts values within range [-10, 10]", () => {
|
||||
expect(() => schema.parse(10)).not.toThrow();
|
||||
expect(() => schema.parse(-10)).not.toThrow();
|
||||
expect(() => schema.parse(0)).not.toThrow();
|
||||
});
|
||||
|
||||
it("rejects values above 10", () => {
|
||||
expect(() => schema.parse(11)).toThrow();
|
||||
});
|
||||
|
||||
it("rejects non-integer values", () => {
|
||||
expect(() => schema.parse(1.5)).toThrow();
|
||||
});
|
||||
});
|
||||
25
packages/chess/src/modifiers/descriptors/hp-bonus.ts
Normal file
25
packages/chess/src/modifiers/descriptors/hp-bonus.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { z } from "zod";
|
||||
import type { Session, EntityId } from "@paratype/rete";
|
||||
import { MODIFIER_REGISTRY } from "../registry.js";
|
||||
import type { ModifierDescriptor } from "../types.js";
|
||||
|
||||
const schema = z.number().int().min(-10).max(10);
|
||||
type Value = z.infer<typeof schema>;
|
||||
|
||||
const descriptor: ModifierDescriptor<Value> = {
|
||||
id: "hp-bonus",
|
||||
attrName: "HpBonus",
|
||||
label: "HP Bonus",
|
||||
valueSchema: schema,
|
||||
stackingRule: "additive",
|
||||
uiForm: "number",
|
||||
apply(session: Session, pieceId: EntityId, effectiveValue: Value): void {
|
||||
session.insert(pieceId, "HpBonus", effectiveValue);
|
||||
},
|
||||
describe(value: Value): string {
|
||||
return `HP ${value >= 0 ? "+" : ""}${value}`;
|
||||
},
|
||||
};
|
||||
|
||||
MODIFIER_REGISTRY.register(descriptor);
|
||||
export { descriptor as HP_BONUS_DESCRIPTOR };
|
||||
|
|
@ -28,15 +28,25 @@ class ModifierRegistryClass {
|
|||
* taken — defensive check because silently overwriting would create
|
||||
* confusing debugging ("which of my two hp-bonus descriptors won?"
|
||||
* races on module import order).
|
||||
*
|
||||
* Generic over V so descriptors with concrete value types
|
||||
* (e.g. `ModifierDescriptor<number>`) can be passed without a
|
||||
* type assertion at the call site. The registry stores them as
|
||||
* `ModifierDescriptor<unknown>` — a heterogeneous container that
|
||||
* loses V intentionally; callers that need the typed value should
|
||||
* import the descriptor directly from its own module.
|
||||
*/
|
||||
register(descriptor: ModifierDescriptor): void {
|
||||
register<V>(descriptor: ModifierDescriptor<V>): void {
|
||||
if (this.#byId.has(descriptor.id)) {
|
||||
throw new Error(
|
||||
`ModifierRegistry: duplicate modifier id "${descriptor.id}". ` +
|
||||
`Each modifier descriptor must have a unique id.`,
|
||||
);
|
||||
}
|
||||
this.#byId.set(descriptor.id, descriptor);
|
||||
// Widen to the internal unknown-typed representation. The double-cast
|
||||
// through `unknown` is intentional: the registry is a heterogeneous
|
||||
// store and V is intentionally erased at storage time.
|
||||
this.#byId.set(descriptor.id, descriptor as unknown as ModifierDescriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export interface ModifierProfile {
|
|||
readonly name: string;
|
||||
readonly description: string;
|
||||
/** Layout this profile's per-instance modifiers are bound to. Optional. */
|
||||
readonly layoutId?: string;
|
||||
readonly layoutId?: string | undefined;
|
||||
readonly perType: readonly TypeModifier[];
|
||||
readonly perInstance: readonly InstanceModifier[];
|
||||
readonly version: 1;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue