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
|
* taken — defensive check because silently overwriting would create
|
||||||
* confusing debugging ("which of my two hp-bonus descriptors won?"
|
* confusing debugging ("which of my two hp-bonus descriptors won?"
|
||||||
* races on module import order).
|
* 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)) {
|
if (this.#byId.has(descriptor.id)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`ModifierRegistry: duplicate modifier id "${descriptor.id}". ` +
|
`ModifierRegistry: duplicate modifier id "${descriptor.id}". ` +
|
||||||
`Each modifier descriptor must have a unique 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 name: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
/** Layout this profile's per-instance modifiers are bound to. Optional. */
|
/** Layout this profile's per-instance modifiers are bound to. Optional. */
|
||||||
readonly layoutId?: string;
|
readonly layoutId?: string | undefined;
|
||||||
readonly perType: readonly TypeModifier[];
|
readonly perType: readonly TypeModifier[];
|
||||||
readonly perInstance: readonly InstanceModifier[];
|
readonly perInstance: readonly InstanceModifier[];
|
||||||
readonly version: 1;
|
readonly version: 1;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue