@drmxrcy/tcg-lorcana 0.0.0-202602060544

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +160 -0
  2. package/package.json +45 -0
  3. package/src/__tests__/integration/move-enumeration.test.ts +256 -0
  4. package/src/__tests__/rules/section-01-concepts.test.ts +426 -0
  5. package/src/__tests__/rules/section-03-gameplay.test.ts +298 -0
  6. package/src/__tests__/rules/section-04-turn-structure.test.ts +708 -0
  7. package/src/__tests__/rules/section-05-cards.test.ts +158 -0
  8. package/src/__tests__/rules/section-06-card-types.test.ts +342 -0
  9. package/src/__tests__/rules/section-07-abilities.test.ts +333 -0
  10. package/src/__tests__/rules/section-08-zones.test.ts +231 -0
  11. package/src/__tests__/rules/section-09-damage.test.ts +148 -0
  12. package/src/__tests__/rules/section-10-keywords.test.ts +469 -0
  13. package/src/__tests__/spec-01-foundation-types.test.ts +534 -0
  14. package/src/__tests__/spec-02-zones-card-states.test.ts +295 -0
  15. package/src/card-utils.ts +302 -0
  16. package/src/cards/README.md +296 -0
  17. package/src/cards/abilities/index.ts +175 -0
  18. package/src/cards/index.ts +10 -0
  19. package/src/deck-validation.ts +175 -0
  20. package/src/engine/lorcana-engine.ts +625 -0
  21. package/src/game-definition/__tests__/core-zone-integration.test.ts +553 -0
  22. package/src/game-definition/__tests__/zone-operations.test.ts +362 -0
  23. package/src/game-definition/__tests__/zones.test.ts +176 -0
  24. package/src/game-definition/definition.ts +45 -0
  25. package/src/game-definition/flow/turn-flow.ts +216 -0
  26. package/src/game-definition/index.ts +31 -0
  27. package/src/game-definition/moves/abilities/activate-ability.ts +51 -0
  28. package/src/game-definition/moves/core/__tests__/move-parameter-enumeration.test.ts +316 -0
  29. package/src/game-definition/moves/core/challenge.test.ts +545 -0
  30. package/src/game-definition/moves/core/challenge.ts +81 -0
  31. package/src/game-definition/moves/core/play-card.ts +83 -0
  32. package/src/game-definition/moves/core/quest.test.ts +448 -0
  33. package/src/game-definition/moves/core/quest.ts +49 -0
  34. package/src/game-definition/moves/debug/manual-exert.ts +36 -0
  35. package/src/game-definition/moves/effects/resolve-bag.ts +35 -0
  36. package/src/game-definition/moves/effects/resolve-effect.ts +34 -0
  37. package/src/game-definition/moves/index.ts +85 -0
  38. package/src/game-definition/moves/locations/move-character-to-location.ts +42 -0
  39. package/src/game-definition/moves/resources/put-card-into-inkwell.test.ts +462 -0
  40. package/src/game-definition/moves/resources/put-card-into-inkwell.ts +51 -0
  41. package/src/game-definition/moves/setup/alter-hand.test.ts +395 -0
  42. package/src/game-definition/moves/setup/alter-hand.ts +210 -0
  43. package/src/game-definition/moves/setup/choose-first-player.test.ts +450 -0
  44. package/src/game-definition/moves/setup/choose-first-player.ts +105 -0
  45. package/src/game-definition/moves/setup/draw-cards.ts +37 -0
  46. package/src/game-definition/moves/songs/sing-together.ts +47 -0
  47. package/src/game-definition/moves/songs/sing.ts +56 -0
  48. package/src/game-definition/moves/standard/concede.test.ts +189 -0
  49. package/src/game-definition/moves/standard/concede.ts +72 -0
  50. package/src/game-definition/moves/standard/pass-turn.ts +49 -0
  51. package/src/game-definition/setup/game-setup.ts +19 -0
  52. package/src/game-definition/trackers/tracker-config.ts +23 -0
  53. package/src/game-definition/win-conditions/lore-victory.ts +26 -0
  54. package/src/game-definition/zone-operations.ts +405 -0
  55. package/src/game-definition/zones/zone-configs.ts +59 -0
  56. package/src/game-definition/zones.ts +283 -0
  57. package/src/index.ts +189 -0
  58. package/src/operations/index.ts +7 -0
  59. package/src/operations/lorcana-operations.ts +288 -0
  60. package/src/queries/README.md +56 -0
  61. package/src/resolvers/__tests__/condition-resolver.test.ts +301 -0
  62. package/src/resolvers/condition-registry.ts +70 -0
  63. package/src/resolvers/condition-resolver.ts +85 -0
  64. package/src/resolvers/conditions/basic.ts +81 -0
  65. package/src/resolvers/conditions/card-state.ts +12 -0
  66. package/src/resolvers/conditions/comparison.ts +102 -0
  67. package/src/resolvers/conditions/existence.ts +219 -0
  68. package/src/resolvers/conditions/history.ts +68 -0
  69. package/src/resolvers/conditions/index.ts +15 -0
  70. package/src/resolvers/conditions/logical.ts +55 -0
  71. package/src/resolvers/conditions/resolution.ts +41 -0
  72. package/src/resolvers/conditions/revealed.ts +42 -0
  73. package/src/resolvers/conditions/zone.ts +84 -0
  74. package/src/setup.test.ts +18 -0
  75. package/src/targeting/__tests__/filter-resolver.test.ts +294 -0
  76. package/src/targeting/__tests__/real-cards-targeting.test.ts +303 -0
  77. package/src/targeting/__tests__/targeting-dsl.test.ts +386 -0
  78. package/src/targeting/enum-expansion.ts +387 -0
  79. package/src/targeting/filter-registry.ts +322 -0
  80. package/src/targeting/filter-resolver.ts +145 -0
  81. package/src/targeting/index.ts +91 -0
  82. package/src/targeting/lorcana-target-dsl.ts +495 -0
  83. package/src/targeting/targeting-ui.ts +407 -0
  84. package/src/testing/index.ts +14 -0
  85. package/src/testing/lorcana-test-engine.ts +813 -0
  86. package/src/types/README.md +303 -0
  87. package/src/types/__tests__/lorcana-state.test.ts +168 -0
  88. package/src/types/__tests__/move-enumeration.test.ts +179 -0
  89. package/src/types/branded-types.ts +106 -0
  90. package/src/types/game-state.ts +184 -0
  91. package/src/types/index.ts +87 -0
  92. package/src/types/keywords.ts +187 -0
  93. package/src/types/lorcana-state.ts +260 -0
  94. package/src/types/move-enumeration.ts +126 -0
  95. package/src/types/move-params.ts +216 -0
  96. package/src/validators/index.ts +7 -0
  97. package/src/validators/move-validators.ts +374 -0
  98. package/src/zones/card-state.ts +234 -0
  99. package/src/zones/index.ts +42 -0
  100. package/src/zones/zone-config.ts +150 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Enum Expansion - Converts enum shortcuts to full DSL objects
3
+ *
4
+ * This module provides the mapping from string enum shortcuts to
5
+ * their full DSL representations. This enables enums as syntactic
6
+ * sugar while the DSL is the canonical form.
7
+ *
8
+ * @module targeting/enum-expansion
9
+ */
10
+
11
+ import type {
12
+ CharacterTarget,
13
+ CharacterTargetEnum,
14
+ ItemTarget,
15
+ ItemTargetEnum,
16
+ LocationTarget,
17
+ LocationTargetEnum,
18
+ LorcanaCardTarget,
19
+ LorcanaCharacterTarget,
20
+ LorcanaItemTarget,
21
+ LorcanaLocationTarget,
22
+ } from "./lorcana-target-dsl";
23
+
24
+ // ============================================================================
25
+ // Character Target Expansions
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Mapping from character enum shortcuts to full DSL
30
+ */
31
+ const CHARACTER_ENUM_EXPANSIONS: Record<CharacterTargetEnum, CharacterTarget> =
32
+ {
33
+ // Self-referential
34
+ SELF: {
35
+ selector: "self",
36
+ cardType: "character",
37
+ context: { self: true },
38
+ },
39
+ THIS_CHARACTER: {
40
+ selector: "self",
41
+ cardType: "character",
42
+ context: { self: true },
43
+ },
44
+
45
+ // Chosen (single target, player choice)
46
+ CHOSEN_CHARACTER: {
47
+ selector: "chosen",
48
+ count: 1,
49
+ owner: "any",
50
+ cardType: "character",
51
+ zones: ["play"],
52
+ },
53
+ CHOSEN_OPPOSING_CHARACTER: {
54
+ selector: "chosen",
55
+ count: 1,
56
+ owner: "opponent",
57
+ cardType: "character",
58
+ zones: ["play"],
59
+ },
60
+ CHOSEN_CHARACTER_OF_YOURS: {
61
+ selector: "chosen",
62
+ count: 1,
63
+ owner: "you",
64
+ cardType: "character",
65
+ zones: ["play"],
66
+ },
67
+ ANOTHER_CHOSEN_CHARACTER: {
68
+ selector: "chosen",
69
+ count: 1,
70
+ owner: "any",
71
+ cardType: "character",
72
+ zones: ["play"],
73
+ excludeSelf: true,
74
+ },
75
+ ANOTHER_CHOSEN_CHARACTER_OF_YOURS: {
76
+ selector: "chosen",
77
+ count: 1,
78
+ owner: "you",
79
+ cardType: "character",
80
+ zones: ["play"],
81
+ excludeSelf: true,
82
+ },
83
+
84
+ // All/Each (multiple targets, automatic)
85
+ ALL_CHARACTERS: {
86
+ selector: "all",
87
+ count: "all",
88
+ owner: "any",
89
+ cardType: "character",
90
+ zones: ["play"],
91
+ },
92
+ ALL_OPPOSING_CHARACTERS: {
93
+ selector: "all",
94
+ count: "all",
95
+ owner: "opponent",
96
+ cardType: "character",
97
+ zones: ["play"],
98
+ },
99
+ YOUR_CHARACTERS: {
100
+ selector: "all",
101
+ count: "all",
102
+ owner: "you",
103
+ cardType: "character",
104
+ zones: ["play"],
105
+ },
106
+ YOUR_OTHER_CHARACTERS: {
107
+ selector: "all",
108
+ count: "all",
109
+ owner: "you",
110
+ cardType: "character",
111
+ zones: ["play"],
112
+ excludeSelf: true,
113
+ },
114
+ EACH_CHARACTER: {
115
+ selector: "each",
116
+ count: "all",
117
+ owner: "any",
118
+ cardType: "character",
119
+ zones: ["play"],
120
+ },
121
+ EACH_OPPOSING_CHARACTER: {
122
+ selector: "each",
123
+ count: "all",
124
+ owner: "opponent",
125
+ cardType: "character",
126
+ zones: ["play"],
127
+ },
128
+
129
+ // Damaged variants
130
+ CHOSEN_DAMAGED_CHARACTER: {
131
+ selector: "chosen",
132
+ count: 1,
133
+ owner: "any",
134
+ cardType: "character",
135
+ zones: ["play"],
136
+ filters: [{ type: "damaged" }],
137
+ },
138
+ CHOSEN_OPPOSING_DAMAGED_CHARACTER: {
139
+ selector: "chosen",
140
+ count: 1,
141
+ owner: "opponent",
142
+ cardType: "character",
143
+ zones: ["play"],
144
+ filters: [{ type: "damaged" }],
145
+ },
146
+ ALL_OPPOSING_DAMAGED_CHARACTERS: {
147
+ selector: "all",
148
+ count: "all",
149
+ owner: "opponent",
150
+ cardType: "character",
151
+ zones: ["play"],
152
+ filters: [{ type: "damaged" }],
153
+ },
154
+ };
155
+
156
+ // ============================================================================
157
+ // Item Target Expansions
158
+ // ============================================================================
159
+
160
+ /**
161
+ * Mapping from item enum shortcuts to full DSL
162
+ */
163
+ const ITEM_ENUM_EXPANSIONS: Record<ItemTargetEnum, ItemTarget> = {
164
+ CHOSEN_ITEM: {
165
+ selector: "chosen",
166
+ count: 1,
167
+ owner: "any",
168
+ cardType: "item",
169
+ zones: ["play"],
170
+ },
171
+ CHOSEN_OPPOSING_ITEM: {
172
+ selector: "chosen",
173
+ count: 1,
174
+ owner: "opponent",
175
+ cardType: "item",
176
+ zones: ["play"],
177
+ },
178
+ YOUR_ITEMS: {
179
+ selector: "all",
180
+ count: "all",
181
+ owner: "you",
182
+ cardType: "item",
183
+ zones: ["play"],
184
+ },
185
+ ALL_ITEMS: {
186
+ selector: "all",
187
+ count: "all",
188
+ owner: "any",
189
+ cardType: "item",
190
+ zones: ["play"],
191
+ },
192
+ ALL_OPPOSING_ITEMS: {
193
+ selector: "all",
194
+ count: "all",
195
+ owner: "opponent",
196
+ cardType: "item",
197
+ zones: ["play"],
198
+ },
199
+ THIS_ITEM: {
200
+ selector: "self",
201
+ cardType: "item",
202
+ context: { self: true },
203
+ },
204
+ };
205
+
206
+ // ============================================================================
207
+ // Location Target Expansions
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Mapping from location enum shortcuts to full DSL
212
+ */
213
+ const LOCATION_ENUM_EXPANSIONS: Record<LocationTargetEnum, LocationTarget> = {
214
+ CHOSEN_LOCATION: {
215
+ selector: "chosen",
216
+ count: 1,
217
+ owner: "any",
218
+ cardType: "location",
219
+ zones: ["play"],
220
+ },
221
+ CHOSEN_OPPOSING_LOCATION: {
222
+ selector: "chosen",
223
+ count: 1,
224
+ owner: "opponent",
225
+ cardType: "location",
226
+ zones: ["play"],
227
+ },
228
+ YOUR_LOCATIONS: {
229
+ selector: "all",
230
+ count: "all",
231
+ owner: "you",
232
+ cardType: "location",
233
+ zones: ["play"],
234
+ },
235
+ ALL_OPPOSING_LOCATIONS: {
236
+ selector: "all",
237
+ count: "all",
238
+ owner: "opponent",
239
+ cardType: "location",
240
+ zones: ["play"],
241
+ },
242
+ THIS_LOCATION: {
243
+ selector: "self",
244
+ cardType: "location",
245
+ context: { self: true },
246
+ },
247
+ };
248
+
249
+ // ============================================================================
250
+ // Expansion Functions
251
+ // ============================================================================
252
+
253
+ /**
254
+ * Check if a character target is an enum (vs DSL object)
255
+ */
256
+ export function isCharacterEnum(
257
+ target: LorcanaCharacterTarget,
258
+ ): target is CharacterTargetEnum {
259
+ return typeof target === "string" && target in CHARACTER_ENUM_EXPANSIONS;
260
+ }
261
+
262
+ /**
263
+ * Expand a character target enum to full DSL
264
+ *
265
+ * @param target - Character target (enum or DSL)
266
+ * @returns Full DSL representation
267
+ */
268
+ export function expandCharacterTarget(
269
+ target: LorcanaCharacterTarget,
270
+ ): CharacterTarget {
271
+ if (isCharacterEnum(target)) {
272
+ const expansion = CHARACTER_ENUM_EXPANSIONS[target];
273
+ if (!expansion) {
274
+ throw new Error(`Unknown character target enum: ${target}`);
275
+ }
276
+ return expansion;
277
+ }
278
+ return target;
279
+ }
280
+
281
+ /**
282
+ * Check if an item target is an enum
283
+ */
284
+ export function isItemEnum(
285
+ target: LorcanaItemTarget,
286
+ ): target is ItemTargetEnum {
287
+ return typeof target === "string" && target in ITEM_ENUM_EXPANSIONS;
288
+ }
289
+
290
+ /**
291
+ * Expand an item target enum to full DSL
292
+ */
293
+ export function expandItemTarget(target: LorcanaItemTarget): ItemTarget {
294
+ if (isItemEnum(target)) {
295
+ const expansion = ITEM_ENUM_EXPANSIONS[target];
296
+ if (!expansion) {
297
+ throw new Error(`Unknown item target enum: ${target}`);
298
+ }
299
+ return expansion;
300
+ }
301
+ return target;
302
+ }
303
+
304
+ /**
305
+ * Check if a location target is an enum
306
+ */
307
+ export function isLocationEnum(
308
+ target: LorcanaLocationTarget,
309
+ ): target is LocationTargetEnum {
310
+ return typeof target === "string" && target in LOCATION_ENUM_EXPANSIONS;
311
+ }
312
+
313
+ /**
314
+ * Expand a location target enum to full DSL
315
+ */
316
+ export function expandLocationTarget(
317
+ target: LorcanaLocationTarget,
318
+ ): LocationTarget {
319
+ if (isLocationEnum(target)) {
320
+ const expansion = LOCATION_ENUM_EXPANSIONS[target];
321
+ if (!expansion) {
322
+ throw new Error(`Unknown location target enum: ${target}`);
323
+ }
324
+ return expansion;
325
+ }
326
+ return target;
327
+ }
328
+
329
+ /**
330
+ * Expand any Lorcana target to full DSL
331
+ *
332
+ * Attempts to determine the type and expand accordingly
333
+ */
334
+ export function expandTarget(
335
+ target: LorcanaCharacterTarget | LorcanaItemTarget | LorcanaLocationTarget,
336
+ ): LorcanaCardTarget {
337
+ if (typeof target === "string") {
338
+ // Try character enum first (most common)
339
+ if (target in CHARACTER_ENUM_EXPANSIONS) {
340
+ return CHARACTER_ENUM_EXPANSIONS[target as CharacterTargetEnum];
341
+ }
342
+ // Try item enum
343
+ if (target in ITEM_ENUM_EXPANSIONS) {
344
+ return ITEM_ENUM_EXPANSIONS[target as ItemTargetEnum];
345
+ }
346
+ // Try location enum
347
+ if (target in LOCATION_ENUM_EXPANSIONS) {
348
+ return LOCATION_ENUM_EXPANSIONS[target as LocationTargetEnum];
349
+ }
350
+ throw new Error(`Unknown target enum: ${target}`);
351
+ }
352
+ return target;
353
+ }
354
+
355
+ // ============================================================================
356
+ // All Enum Values (for validation)
357
+ // ============================================================================
358
+
359
+ /**
360
+ * Set of all valid character target enum values
361
+ */
362
+ export const CHARACTER_TARGET_ENUMS = new Set<string>(
363
+ Object.keys(CHARACTER_ENUM_EXPANSIONS),
364
+ );
365
+
366
+ /**
367
+ * Set of all valid item target enum values
368
+ */
369
+ export const ITEM_TARGET_ENUMS = new Set<string>(
370
+ Object.keys(ITEM_ENUM_EXPANSIONS),
371
+ );
372
+
373
+ /**
374
+ * Set of all valid location target enum values
375
+ */
376
+ export const LOCATION_TARGET_ENUMS = new Set<string>(
377
+ Object.keys(LOCATION_ENUM_EXPANSIONS),
378
+ );
379
+
380
+ /**
381
+ * Set of all valid target enum values
382
+ */
383
+ export const ALL_TARGET_ENUMS = new Set<string>([
384
+ ...CHARACTER_TARGET_ENUMS,
385
+ ...ITEM_TARGET_ENUMS,
386
+ ...LOCATION_TARGET_ENUMS,
387
+ ]);
@@ -0,0 +1,322 @@
1
+ import type { CardInstance, CardRegistry } from "@drmxrcy/tcg-core";
2
+ import type { LorcanaCardDefinition } from "@drmxrcy/tcg-lorcana-types";
3
+ import type { LorcanaCardMeta, LorcanaGameState } from "../types/game-state";
4
+ import type { LorcanaContext, LorcanaFilter } from "./lorcana-target-dsl";
5
+
6
+ /**
7
+ * Filter evaluation context
8
+ */
9
+ export interface FilterContext {
10
+ state: LorcanaGameState;
11
+ registry: CardRegistry<LorcanaCardDefinition>;
12
+ context?: LorcanaContext;
13
+ }
14
+
15
+ /**
16
+ * Handler for a specific filter type
17
+ */
18
+ export interface FilterHandler<T extends LorcanaFilter = LorcanaFilter> {
19
+ name: T["type"];
20
+ complexity: number;
21
+ evaluate: (
22
+ filter: T,
23
+ card: CardInstance<LorcanaCardMeta>,
24
+ context: FilterContext,
25
+ ) => boolean;
26
+ }
27
+
28
+ export class FilterRegistry {
29
+ private handlers = new Map<string, FilterHandler>();
30
+
31
+ /**
32
+ * Register a filter handler
33
+ */
34
+ register<T extends LorcanaFilter>(handler: FilterHandler<T>): void {
35
+ this.handlers.set(handler.name, handler as unknown as FilterHandler);
36
+ }
37
+
38
+ /**
39
+ * Get a handler for a filter type
40
+ */
41
+ get(type: string): FilterHandler | undefined {
42
+ return this.handlers.get(type);
43
+ }
44
+
45
+ /**
46
+ * Get all registered handlers
47
+ */
48
+ getAll(): FilterHandler[] {
49
+ return Array.from(this.handlers.values());
50
+ }
51
+ }
52
+
53
+ // Global registry instance
54
+ export const filterRegistry = new FilterRegistry();
55
+
56
+ export function registerDefaultFilters() {
57
+ // --- State Filters ---
58
+ filterRegistry.register<LorcanaFilter & { type: "damaged" }>({
59
+ name: "damaged",
60
+ complexity: 0,
61
+ evaluate: (_, card) => (card.damage ?? 0) > 0,
62
+ });
63
+
64
+ filterRegistry.register<LorcanaFilter & { type: "undamaged" }>({
65
+ name: "undamaged",
66
+ complexity: 0,
67
+ evaluate: (_, card) => (card.damage ?? 0) === 0,
68
+ });
69
+
70
+ filterRegistry.register<LorcanaFilter & { type: "exerted" }>({
71
+ name: "exerted",
72
+ complexity: 0,
73
+ evaluate: (_, card) => card.state === "exerted",
74
+ });
75
+
76
+ filterRegistry.register<LorcanaFilter & { type: "ready" }>({
77
+ name: "ready",
78
+ complexity: 0,
79
+ evaluate: (_, card) => card.state === "ready",
80
+ });
81
+
82
+ filterRegistry.register<LorcanaFilter & { type: "dry" }>({
83
+ name: "dry",
84
+ complexity: 0,
85
+ evaluate: (_, card) => !(card.isDrying ?? false),
86
+ });
87
+
88
+ filterRegistry.register<LorcanaFilter & { type: "inkable"; value: boolean }>({
89
+ name: "inkable",
90
+ complexity: 0,
91
+ evaluate: (filter, card, { registry }) => {
92
+ const def = registry.getCard(card.definitionId);
93
+ return def ? def.inkable === filter.value : false;
94
+ },
95
+ });
96
+
97
+ // --- Property Filters ---
98
+ filterRegistry.register<
99
+ LorcanaFilter & { type: "has-keyword"; keyword: any }
100
+ >({
101
+ name: "has-keyword",
102
+ complexity: 10,
103
+ evaluate: (filter, card, { registry }) => {
104
+ const def = registry.getCard(card.definitionId);
105
+ return (
106
+ def?.abilities?.some(
107
+ (a: any) => a.type === "keyword" && a.keyword === filter.keyword,
108
+ ) ?? false
109
+ );
110
+ },
111
+ });
112
+
113
+ filterRegistry.register<
114
+ LorcanaFilter & { type: "has-classification"; classification: string }
115
+ >({
116
+ name: "has-classification",
117
+ complexity: 10,
118
+ evaluate: (filter, card, { registry }) => {
119
+ const def = registry.getCard(card.definitionId);
120
+ if (!def || def.cardType !== "character") return false;
121
+ return (
122
+ def.classifications?.some(
123
+ (c: string) =>
124
+ c.toLowerCase() === filter.classification.toLowerCase(),
125
+ ) ?? false
126
+ );
127
+ },
128
+ });
129
+
130
+ filterRegistry.register<
131
+ LorcanaFilter & { type: "name" } & (
132
+ | { equals: string }
133
+ | { contains: string }
134
+ )
135
+ >({
136
+ name: "name",
137
+ complexity: 10,
138
+ evaluate: (filter, card, { registry }) => {
139
+ const def = registry.getCard(card.definitionId);
140
+ if (!def) return false;
141
+ if ("equals" in filter) {
142
+ return def.name === filter.equals;
143
+ }
144
+ if ("contains" in filter) {
145
+ return def.name.includes(filter.contains);
146
+ }
147
+ return false;
148
+ },
149
+ });
150
+
151
+ filterRegistry.register<LorcanaFilter & { type: "card-type"; value: any }>({
152
+ name: "card-type",
153
+ complexity: 5,
154
+ evaluate: (filter, card, { registry }) => {
155
+ const def = registry.getCard(card.definitionId);
156
+ return def ? def.cardType === filter.value : false;
157
+ },
158
+ });
159
+
160
+ // --- Numeric Filters ---
161
+ const checkComparison = (
162
+ actual: number,
163
+ operator: string,
164
+ target: number,
165
+ ) => {
166
+ switch (operator) {
167
+ case "eq":
168
+ return actual === target;
169
+ case "ne":
170
+ return actual !== target;
171
+ case "gt":
172
+ return actual > target;
173
+ case "gte":
174
+ return actual >= target;
175
+ case "lt":
176
+ return actual < target;
177
+ case "lte":
178
+ return actual <= target;
179
+ default:
180
+ return false;
181
+ }
182
+ };
183
+
184
+ const getComparisonValue = (
185
+ value: number | "target",
186
+ // context: FilterContext - unused for now
187
+ // TODO: Implement getTargetValue when context is fully fleshed out
188
+ ): number => {
189
+ if (value === "target") return 0; // Placeholder
190
+ return value;
191
+ };
192
+
193
+ filterRegistry.register<
194
+ LorcanaFilter & { type: "strength"; comparison: any; value: any }
195
+ >({
196
+ name: "strength",
197
+ complexity: 20,
198
+ evaluate: (filter, card, ctx) => {
199
+ const def = ctx.registry.getCard(card.definitionId);
200
+ if (!def || def.cardType !== "character") return false;
201
+ const targetVal = getComparisonValue(filter.value);
202
+ return checkComparison(
203
+ (def as any).strength ?? 0,
204
+ filter.comparison,
205
+ targetVal,
206
+ );
207
+ },
208
+ });
209
+
210
+ filterRegistry.register<
211
+ LorcanaFilter & { type: "willpower"; comparison: any; value: any }
212
+ >({
213
+ name: "willpower",
214
+ complexity: 20,
215
+ evaluate: (filter, card, ctx) => {
216
+ const def = ctx.registry.getCard(card.definitionId);
217
+ if (!def || def.cardType !== "character") return false;
218
+ const targetVal = getComparisonValue(filter.value);
219
+ return checkComparison(
220
+ (def as any).willpower ?? 0,
221
+ filter.comparison,
222
+ targetVal,
223
+ );
224
+ },
225
+ });
226
+
227
+ filterRegistry.register<
228
+ LorcanaFilter & { type: "cost"; comparison: any; value: any }
229
+ >({
230
+ name: "cost",
231
+ complexity: 20,
232
+ evaluate: (filter, card, ctx) => {
233
+ const def = ctx.registry.getCard(card.definitionId);
234
+ if (!def) return false;
235
+ const targetVal = getComparisonValue(filter.value);
236
+ return checkComparison(def.cost, filter.comparison, targetVal);
237
+ },
238
+ });
239
+
240
+ filterRegistry.register<
241
+ LorcanaFilter & { type: "lore-value"; comparison: any; value: any }
242
+ >({
243
+ name: "lore-value",
244
+ complexity: 20,
245
+ evaluate: (filter, card, ctx) => {
246
+ const def = ctx.registry.getCard(card.definitionId);
247
+ if (!def || (def.cardType !== "character" && def.cardType !== "location"))
248
+ return false;
249
+ const targetVal = getComparisonValue(filter.value);
250
+ return checkComparison(
251
+ (def as any).lore ?? 0,
252
+ filter.comparison,
253
+ targetVal,
254
+ );
255
+ },
256
+ });
257
+
258
+ filterRegistry.register<
259
+ LorcanaFilter & { type: "move-cost"; comparison: any; value: number }
260
+ >({
261
+ name: "move-cost",
262
+ complexity: 20,
263
+ evaluate: (filter, card, { registry }) => {
264
+ const def = registry.getCard(card.definitionId);
265
+ if (!def || def.cardType !== "location") return false;
266
+ return checkComparison(
267
+ def.moveCost ?? 0,
268
+ filter.comparison,
269
+ filter.value,
270
+ );
271
+ },
272
+ });
273
+
274
+ filterRegistry.register<LorcanaFilter & { type: "at-location" }>({
275
+ name: "at-location",
276
+ complexity: 30,
277
+ evaluate: (_, card) => {
278
+ return !!card.atLocationId;
279
+ },
280
+ });
281
+
282
+ // --- Composite Filters ---
283
+ filterRegistry.register<
284
+ LorcanaFilter & { type: "and"; filters: LorcanaFilter[] }
285
+ >({
286
+ name: "and",
287
+ complexity: 50,
288
+ evaluate: (filter, card, context) => {
289
+ return filter.filters.every((f) => {
290
+ const handler = filterRegistry.get(f.type);
291
+ return handler ? handler.evaluate(f, card, context) : false;
292
+ });
293
+ },
294
+ });
295
+
296
+ filterRegistry.register<
297
+ LorcanaFilter & { type: "or"; filters: LorcanaFilter[] }
298
+ >({
299
+ name: "or",
300
+ complexity: 50,
301
+ evaluate: (filter, card, context) => {
302
+ return filter.filters.some((f) => {
303
+ const handler = filterRegistry.get(f.type);
304
+ return handler ? handler.evaluate(f, card, context) : false;
305
+ });
306
+ },
307
+ });
308
+
309
+ filterRegistry.register<
310
+ LorcanaFilter & { type: "not"; filter: LorcanaFilter }
311
+ >({
312
+ name: "not",
313
+ complexity: 50,
314
+ evaluate: (filter, card, context) => {
315
+ // Safe cast because we validated the type string
316
+ const handler = filterRegistry.get(
317
+ filter.filter.type,
318
+ ) as unknown as FilterHandler<LorcanaFilter>;
319
+ return handler ? !handler.evaluate(filter.filter, card, context) : false;
320
+ },
321
+ });
322
+ }