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.
|
* move calculation with no reset.
|
||||||
*/
|
*/
|
||||||
public readonly activePresets: ActivePresetSet;
|
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
|
* Chronological log of every successful applyMove. Callers consume
|
||||||
|
|
@ -443,6 +448,8 @@ export class ChessEngine {
|
||||||
? { activePresets: arg as ActivePresetSet }
|
? { activePresets: arg as ActivePresetSet }
|
||||||
: ((arg as EngineOptions | undefined) ?? {});
|
: ((arg as EngineOptions | undefined) ?? {});
|
||||||
|
|
||||||
|
this.activeProfile = opts.profile ?? null;
|
||||||
|
|
||||||
const layout = opts.layout ?? CLASSIC_LAYOUT;
|
const layout = opts.layout ?? CLASSIC_LAYOUT;
|
||||||
applyLayout(this.session, 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 type { EntityId } from '@paratype/rete';
|
||||||
import { MODIFIER_REGISTRY } from '../modifiers/index.js';
|
import { MODIFIER_REGISTRY } from '../modifiers/index.js';
|
||||||
|
import { getModifierSource } from "../modifiers/source.js";
|
||||||
import type { ChessEngine } from '../engine.js';
|
import type { ChessEngine } from '../engine.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -40,7 +41,8 @@ export function ModifierPinnedPanel({ pieceId, engine, onClose }: Props) {
|
||||||
// level; the registry widens to ModifierDescriptor<unknown>. The value
|
// level; the registry widens to ModifierDescriptor<unknown>. The value
|
||||||
// was written by the same descriptor's apply(), so types match at runtime.
|
// was written by the same descriptor's apply(), so types match at runtime.
|
||||||
const description = (descriptor.describe as (v: unknown) => string)(value);
|
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 (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue