feat(ui): enhanced modifier source chain in pinned panel
This commit is contained in:
parent
ca9072ce48
commit
980d567354
4 changed files with 154 additions and 1 deletions
|
|
@ -339,6 +339,11 @@ export class ChessEngine {
|
|||
* move calculation with no reset.
|
||||
*/
|
||||
public readonly activePresets: ActivePresetSet;
|
||||
/**
|
||||
* The currently active ModifierProfile, if any.
|
||||
* Profiles attach rule modifiers to pieces by type or layout slot.
|
||||
*/
|
||||
public readonly activeProfile: ModifierProfile | null;
|
||||
|
||||
/**
|
||||
* Chronological log of every successful applyMove. Callers consume
|
||||
|
|
@ -443,6 +448,8 @@ export class ChessEngine {
|
|||
? { activePresets: arg as ActivePresetSet }
|
||||
: ((arg as EngineOptions | undefined) ?? {});
|
||||
|
||||
this.activeProfile = opts.profile ?? null;
|
||||
|
||||
const layout = opts.layout ?? CLASSIC_LAYOUT;
|
||||
applyLayout(this.session, layout);
|
||||
|
||||
|
|
|
|||
71
packages/chess/src/modifiers/source.test.ts
Normal file
71
packages/chess/src/modifiers/source.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { expect, test } from "vitest";
|
||||
import { ChessEngine } from "../engine.js";
|
||||
import { getModifierSource } from "./source.js";
|
||||
import type { ModifierProfile } from "./types.js";
|
||||
import { PRESET_REGISTRY } from "../presets/registry.js";
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
|
||||
const TEST_PROFILE: ModifierProfile = {
|
||||
id: "test-profile",
|
||||
name: "Test Profile",
|
||||
description: "",
|
||||
version: 1,
|
||||
source: "custom",
|
||||
perInstance: [
|
||||
{ kind: "hp-bonus", square: "b1", value: 2 },
|
||||
],
|
||||
perType: [
|
||||
{ kind: "range-bonus", pieceType: "knight", color: "white", value: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
test("returns per-instance for a square match", () => {
|
||||
const engine = new ChessEngine({ profile: TEST_PROFILE });
|
||||
// Find the knight on b1
|
||||
const b1Id = engine.session.allFacts().find(
|
||||
(f) => f.attr === "Position" && f.value === 1 // b1 is index 1
|
||||
)?.id as number;
|
||||
|
||||
const source = getModifierSource(engine, b1Id as unknown as EntityId, "hp-bonus");
|
||||
expect(source).toEqual({ kind: "per-instance", square: "b1" });
|
||||
});
|
||||
|
||||
test("returns per-type for a type/color match when no instance match", () => {
|
||||
const engine = new ChessEngine({ profile: TEST_PROFILE });
|
||||
// Find a white knight that ISN'T on b1 (e.g. g1)
|
||||
const g1Id = engine.session.allFacts().find(
|
||||
(f) => f.attr === "Position" && f.value === 6 // g1 is index 6
|
||||
)?.id as number;
|
||||
|
||||
const source = getModifierSource(engine, g1Id as unknown as EntityId, "range-bonus");
|
||||
expect(source).toEqual({ kind: "per-type", pieceType: "knight", color: "white" });
|
||||
});
|
||||
|
||||
test("returns preset if no profile overrides but a preset declares the attribute", () => {
|
||||
// Use a preset that declares Hp. Let's make sure 'piece-hp' is active.
|
||||
const engine = new ChessEngine();
|
||||
engine.activePresets.replaceAll([{ id: "piece-hp", scope: "both", turnsRemaining: null }]);
|
||||
|
||||
// Find any piece, e.g., a1 rook
|
||||
const a1Id = engine.session.allFacts().find(
|
||||
(f) => f.attr === "Position" && f.value === 0
|
||||
)?.id as EntityId;
|
||||
|
||||
const source = getModifierSource(engine, a1Id, "hp-bonus");
|
||||
const hpDef = PRESET_REGISTRY.get("piece-hp")!;
|
||||
|
||||
expect(source).toEqual({ kind: "preset", presetId: "piece-hp", presetName: hpDef.name });
|
||||
});
|
||||
|
||||
test("returns default if nothing overrides or declares it", () => {
|
||||
const engine = new ChessEngine();
|
||||
|
||||
// Find any piece
|
||||
const e2Id = engine.session.allFacts().find(
|
||||
(f) => f.attr === "Position" && f.value === 12
|
||||
)?.id as EntityId;
|
||||
|
||||
// With no profile and no active preset declaring DirectionAdditions, it should be default
|
||||
const source = getModifierSource(engine, e2Id, "direction-additions");
|
||||
expect(source).toEqual({ kind: "default" });
|
||||
});
|
||||
73
packages/chess/src/modifiers/source.ts
Normal file
73
packages/chess/src/modifiers/source.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { EntityId } from "@paratype/rete";
|
||||
import type { ChessEngine } from "../engine.js";
|
||||
import type { PieceColor, PieceType } from "../schema.js";
|
||||
import type { ModifierKindId } from "./types.js";
|
||||
import { PRESET_REGISTRY } from "../presets/registry.js";
|
||||
import { MODIFIER_REGISTRY } from "./index.js";
|
||||
import { squareToAlgebraic } from "../coord.js";
|
||||
|
||||
export type ModifierSource =
|
||||
| { kind: "per-instance"; square: string }
|
||||
| { kind: "per-type"; pieceType: PieceType; color: PieceColor | "both" }
|
||||
| { kind: "preset"; presetId: string; presetName: string }
|
||||
| { kind: "default" };
|
||||
|
||||
/**
|
||||
* Determine the origin of a modifier applied to a specific piece.
|
||||
* Checks the active profile (per-instance, then per-type), then active presets,
|
||||
* falling back to "default".
|
||||
*/
|
||||
export function getModifierSource(
|
||||
engine: ChessEngine,
|
||||
pieceId: EntityId,
|
||||
modifierKind: ModifierKindId
|
||||
): ModifierSource {
|
||||
const profile = engine.activeProfile;
|
||||
const squareNum = engine.session.get(pieceId, "Position") as number | undefined;
|
||||
const algebraicSq = squareNum !== undefined ? squareToAlgebraic(squareNum) : null;
|
||||
const pieceType = engine.session.get(pieceId, "PieceType") as PieceType | undefined;
|
||||
const color = engine.session.get(pieceId, "Color") as PieceColor | undefined;
|
||||
|
||||
if (profile) {
|
||||
// 1. Check per-instance modifiers
|
||||
if (algebraicSq) {
|
||||
const instanceMatch = profile.perInstance.find(
|
||||
(m) => m.kind === modifierKind && m.square === algebraicSq
|
||||
);
|
||||
if (instanceMatch) {
|
||||
return { kind: "per-instance", square: instanceMatch.square };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check per-type modifiers
|
||||
if (pieceType && color) {
|
||||
const typeMatch = profile.perType.find(
|
||||
(m) =>
|
||||
m.kind === modifierKind &&
|
||||
m.pieceType === pieceType &&
|
||||
(m.color === "both" || m.color === color)
|
||||
);
|
||||
if (typeMatch) {
|
||||
return { kind: "per-type", pieceType: typeMatch.pieceType, color: typeMatch.color };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check active presets
|
||||
const descriptor = MODIFIER_REGISTRY.get(modifierKind);
|
||||
if (descriptor) {
|
||||
let attrName = descriptor.attrName as string;
|
||||
if (attrName === "HpBonus") attrName = "Hp";
|
||||
else if (attrName === "RangeBonus") attrName = "Range";
|
||||
|
||||
for (const presetEntry of engine.activePresets.list()) {
|
||||
const def = PRESET_REGISTRY.get(presetEntry.id);
|
||||
if (def?.pieceAttributes?.includes(attrName as keyof import("../schema.js").ChessAttrMap)) {
|
||||
return { kind: "preset", presetId: presetEntry.id, presetName: def.name };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Default
|
||||
return { kind: "default" };
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
*/
|
||||
import type { EntityId } from '@paratype/rete';
|
||||
import { MODIFIER_REGISTRY } from '../modifiers/index.js';
|
||||
import { getModifierSource } from "../modifiers/source.js";
|
||||
import type { ChessEngine } from '../engine.js';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -40,7 +41,8 @@ export function ModifierPinnedPanel({ pieceId, engine, onClose }: Props) {
|
|||
// level; the registry widens to ModifierDescriptor<unknown>. The value
|
||||
// was written by the same descriptor's apply(), so types match at runtime.
|
||||
const description = (descriptor.describe as (v: unknown) => string)(value);
|
||||
return [{ id: descriptor.id, label: descriptor.label, description }];
|
||||
const source = getModifierSource(engine, id, descriptor.id);
|
||||
return [{ id: descriptor.id, label: descriptor.label, description, source }];
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue