@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,85 @@
1
+ import type { CardInstance, CardRegistry } from "@drmxrcy/tcg-core";
2
+ import type { Condition, LorcanaCardDefinition } from "@drmxrcy/tcg-lorcana-types";
3
+ import type { LorcanaContext } from "../targeting/lorcana-target-dsl";
4
+ import type { LorcanaCardMeta, LorcanaGameState } from "../types/game-state";
5
+ import { conditionRegistry } from "./condition-registry";
6
+
7
+ const MAX_RECURSION_DEPTH = 10;
8
+
9
+ /**
10
+ * Check if a condition is met
11
+ */
12
+ export function isConditionMet(
13
+ condition: Condition,
14
+ sourceCard: CardInstance<LorcanaCardMeta>,
15
+ state: LorcanaGameState,
16
+ registry: CardRegistry<LorcanaCardDefinition>,
17
+ context?: LorcanaContext,
18
+ ): boolean {
19
+ // Initialize context if needed
20
+ const ctx = context ?? ({} as LorcanaContext);
21
+ const depth = ctx.recursionDepth ?? 0;
22
+
23
+ // 1. Recursion protection
24
+ if (depth > MAX_RECURSION_DEPTH) {
25
+ console.warn(
26
+ `Max recursion depth ${MAX_RECURSION_DEPTH} reached evaluating condition for ${sourceCard.id}`,
27
+ );
28
+ return false; // Fail safe
29
+ }
30
+
31
+ // 2. Evaluation
32
+ try {
33
+ // Increment depth in context
34
+ ctx.recursionDepth = depth + 1;
35
+
36
+ // Get handler
37
+ const handler = conditionRegistry.get(condition.type);
38
+ if (!handler) {
39
+ console.warn(`No handler found for condition type: ${condition.type}`);
40
+ return false;
41
+ }
42
+
43
+ const result = handler.evaluate(condition, sourceCard, {
44
+ state,
45
+ registry,
46
+ context: ctx,
47
+ });
48
+
49
+ return result;
50
+ } catch (error) {
51
+ console.warn("Error evaluating condition", error);
52
+ return false;
53
+ } finally {
54
+ // Restore depth
55
+ ctx.recursionDepth = depth;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check if a list of conditions are ALL met (Implicit AND)
61
+ */
62
+ export function areConditionsMet(
63
+ conditions: Condition[] | undefined,
64
+ sourceCard: CardInstance<LorcanaCardMeta>,
65
+ state: LorcanaGameState,
66
+ registry: CardRegistry<LorcanaCardDefinition>,
67
+ context?: LorcanaContext,
68
+ ): boolean {
69
+ if (!conditions || conditions.length === 0) {
70
+ return true;
71
+ }
72
+
73
+ // Sort by complexity to fail fast
74
+ const sorted = [...conditions].sort((a, b) => {
75
+ const ha = conditionRegistry.get(a.type);
76
+ const hb = conditionRegistry.get(b.type);
77
+ const ca = ha ? ha.complexity : 100;
78
+ const cb = hb ? hb.complexity : 100;
79
+ return ca - cb;
80
+ });
81
+
82
+ return sorted.every((cond) =>
83
+ isConditionMet(cond, sourceCard, state, registry, context),
84
+ );
85
+ }
@@ -0,0 +1,81 @@
1
+ import type {
2
+ HasAnyDamageCondition,
3
+ IfCondition,
4
+ InChallengeCondition,
5
+ InInkwellCondition,
6
+ InPlayCondition,
7
+ IsExertedCondition,
8
+ IsReadyCondition,
9
+ NoDamageCondition,
10
+ TurnCondition,
11
+ } from "@drmxrcy/tcg-lorcana-types";
12
+ import { conditionRegistry } from "../condition-registry";
13
+
14
+ conditionRegistry.register<TurnCondition>("turn", {
15
+ complexity: 10,
16
+ evaluate: (condition, sourceCard, { state }) => {
17
+ const isActivePlayer =
18
+ state.external.activePlayerId === sourceCard.controller;
19
+ return condition.whose === "your" ? isActivePlayer : !isActivePlayer;
20
+ },
21
+ });
22
+
23
+ conditionRegistry.register<IsExertedCondition>("is-exerted", {
24
+ complexity: 10,
25
+ evaluate: (_condition, sourceCard) => {
26
+ return sourceCard.state === "exerted";
27
+ },
28
+ });
29
+
30
+ conditionRegistry.register<IsReadyCondition>("is-ready", {
31
+ complexity: 10,
32
+ evaluate: (_condition, sourceCard) => {
33
+ return sourceCard.state === "ready";
34
+ },
35
+ });
36
+
37
+ conditionRegistry.register<InInkwellCondition>("in-inkwell", {
38
+ complexity: 10,
39
+ evaluate: (_condition, sourceCard) => {
40
+ return sourceCard.zone === "inkwell";
41
+ },
42
+ });
43
+
44
+ conditionRegistry.register<InPlayCondition>("in-play", {
45
+ complexity: 10,
46
+ evaluate: (_condition, sourceCard) => {
47
+ return sourceCard.zone === "play";
48
+ },
49
+ });
50
+
51
+ conditionRegistry.register<HasAnyDamageCondition>("has-any-damage", {
52
+ complexity: 2,
53
+ evaluate: (_condition, sourceCard) => {
54
+ return (sourceCard.damage || 0) > 0;
55
+ },
56
+ });
57
+
58
+ conditionRegistry.register<NoDamageCondition>("no-damage", {
59
+ complexity: 2,
60
+ evaluate: (_condition, sourceCard) => {
61
+ return (sourceCard.damage || 0) === 0;
62
+ },
63
+ });
64
+
65
+ conditionRegistry.register<InChallengeCondition>("in-challenge", {
66
+ complexity: 5,
67
+ evaluate: (_condition, _sourceCard, { context }) => {
68
+ return !!(context?.attacker || context?.defender);
69
+ },
70
+ });
71
+
72
+ // Register IfCondition handler (parser catch-all)
73
+ conditionRegistry.register<IfCondition>("if", {
74
+ complexity: 99,
75
+ evaluate: (_condition, _sourceCard, _context) => {
76
+ // IfCondition is a parser catch-all for unparseable expressions
77
+ // It should be converted to specific conditions before evaluation
78
+ // For now, always return false to indicate it cannot be evaluated
79
+ return false;
80
+ },
81
+ });
@@ -0,0 +1,12 @@
1
+ import type { HasCardUnderCondition } from "@drmxrcy/tcg-lorcana-types";
2
+ import { conditionRegistry } from "../condition-registry";
3
+
4
+ conditionRegistry.register<HasCardUnderCondition>("has-card-under", {
5
+ complexity: 20,
6
+ evaluate: (_condition, sourceCard) => {
7
+ const stack = sourceCard.stackPosition;
8
+ if (!stack) return false;
9
+ // Check if there are cards underneath
10
+ return !!(stack.cardsUnderneath && stack.cardsUnderneath.length > 0);
11
+ },
12
+ });
@@ -0,0 +1,102 @@
1
+ import type { CardInstance } from "@drmxrcy/tcg-core";
2
+ import type { ComparisonCondition, ComparisonValue } from "@drmxrcy/tcg-lorcana-types";
3
+ import type { LorcanaCardMeta, LorcanaGameState } from "../../types/game-state";
4
+ import { conditionRegistry } from "../condition-registry";
5
+
6
+ // Rewriting register to include registry in helper
7
+ conditionRegistry.register<ComparisonCondition>("comparison", {
8
+ complexity: 40,
9
+ evaluate: (condition, sourceCard, { state, registry }) => {
10
+ const resolve = (v: ComparisonValue): number => {
11
+ if ("value" in v) return v.value;
12
+
13
+ // Types that have controller
14
+ let targetController: string | undefined;
15
+ if ("controller" in v) {
16
+ if (v.controller === "you") {
17
+ targetController = sourceCard.controller;
18
+ } else if (v.controller === "opponent") {
19
+ const playerIds = Object.keys(state.external.loreScores);
20
+ targetController = playerIds.find(
21
+ (id) => id !== sourceCard.controller,
22
+ );
23
+ }
24
+ }
25
+
26
+ if (v.type === "damage-on-self") return sourceCard.damage || 0;
27
+
28
+ if (!targetController && v.type !== "strength-of-self") return 0;
29
+
30
+ // Needs definitions for Strength/Willpower/Type checks
31
+ if (v.type === "strength-of-self") {
32
+ const def = registry.getCard(sourceCard.definitionId);
33
+ // TODO: Add modifiers? Conditions usually check CURRENT strength.
34
+ // This requires the whole Engine/Modifier system which we might not have access to here.
35
+ // For now, return base strength.
36
+ return def?.strength || 0;
37
+ }
38
+
39
+ return resolveValueWithState(v, state, targetController || "", registry);
40
+ };
41
+
42
+ const left = resolve(condition.left);
43
+ const right = resolve(condition.right);
44
+
45
+ // console.log("Comparison:", { left, right, op: condition.comparison, sourceCtrl: sourceCard.controller, keys: Object.keys(sourceCard) });
46
+
47
+ switch (condition.comparison) {
48
+ // @ts-expect-error - comparison operators mismatch
49
+ case "eq":
50
+ return left === right;
51
+ // @ts-expect-error - comparison operators mismatch
52
+ case "ne":
53
+ return left !== right;
54
+ // @ts-expect-error - comparison operators mismatch
55
+ case "gt":
56
+ return left > right;
57
+ // @ts-expect-error - comparison operators mismatch
58
+ case "gte":
59
+ return left >= right;
60
+ // @ts-expect-error - comparison operators mismatch
61
+ case "lt":
62
+ return left < right;
63
+ // @ts-expect-error - comparison operators mismatch
64
+ case "lte":
65
+ return left <= right;
66
+ default:
67
+ return false;
68
+ }
69
+ },
70
+ });
71
+
72
+ function resolveValueWithState(
73
+ v: ComparisonValue,
74
+ state: LorcanaGameState,
75
+ targetOwnerId: string,
76
+ registry: any, // Typing as any to avoid circular deps or complex type logic for now
77
+ ): number {
78
+ if ("value" in v) return v.value;
79
+ if (v.type === "lore") {
80
+ return state.external.loreScores[targetOwnerId as any] || 0;
81
+ }
82
+
83
+ // Zone counts
84
+ if (v.type === "cards-in-hand") {
85
+ return Object.values(state.internal.cards).filter(
86
+ (c) => c.zone === "hand" && c.controller === targetOwnerId,
87
+ ).length;
88
+ }
89
+ if (v.type === "cards-in-inkwell") {
90
+ return Object.values(state.internal.cards).filter(
91
+ (c) => c.zone === "inkwell" && c.controller === targetOwnerId,
92
+ ).length;
93
+ }
94
+ if (v.type === "character-count") {
95
+ return Object.values(state.internal.cards).filter((c) => {
96
+ if (c.zone !== "play" || c.controller !== targetOwnerId) return false;
97
+ const def = registry.getCard(c.definitionId);
98
+ return def?.cardType === "character";
99
+ }).length;
100
+ }
101
+ return 0;
102
+ }
@@ -0,0 +1,219 @@
1
+ import type { CardInstance } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ HasCharacterCountCondition,
4
+ HasCharacterWithClassificationCondition,
5
+ HasNamedCharacterCondition,
6
+ ResourceCountCondition,
7
+ } from "@drmxrcy/tcg-lorcana-types";
8
+ import type { LorcanaFilter } from "../../targeting/lorcana-target-dsl";
9
+ import type { LorcanaCardMeta, LorcanaGameState } from "../../types/game-state";
10
+ import { conditionRegistry } from "../condition-registry";
11
+
12
+ /**
13
+ * Helper to count cards matching a predicate
14
+ */
15
+ function countCards(
16
+ state: LorcanaGameState,
17
+ predicate: (card: CardInstance<LorcanaCardMeta>) => boolean,
18
+ ): number {
19
+ let count = 0;
20
+ for (const card of Object.values(state.internal.cards)) {
21
+ if (predicate(card as CardInstance<LorcanaCardMeta>)) {
22
+ count++;
23
+ }
24
+ }
25
+ return count;
26
+ }
27
+
28
+ /**
29
+ * Helper to compare numbers
30
+ */
31
+ function compare(val: number, op: string, target: number): boolean {
32
+ switch (op) {
33
+ case "eq":
34
+ return val === target;
35
+ case "ne":
36
+ return val !== target;
37
+ case "gt":
38
+ return val > target;
39
+ case "gte":
40
+ return val >= target;
41
+ case "lt":
42
+ return val < target;
43
+ case "lte":
44
+ return val <= target;
45
+ default:
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Helper to get zone filter for "in play" (default for "have a character")
52
+ */
53
+ function getPlayZoneFilter(
54
+ controller: "you" | "opponent" | "any",
55
+ sourceCard: CardInstance<LorcanaCardMeta>,
56
+ ): (card: CardInstance<LorcanaCardMeta>) => boolean {
57
+ return (card) => {
58
+ // Check Zone (must be in play)
59
+ if (card.zone !== "play") return false;
60
+
61
+ // Check Controller
62
+ if (controller === "you") {
63
+ return card.controller === sourceCard.controller;
64
+ }
65
+ if (controller === "opponent") {
66
+ return card.controller !== sourceCard.controller;
67
+ }
68
+ return true; // any
69
+ };
70
+ }
71
+
72
+ // Register HasNamedCharacterCondition
73
+ conditionRegistry.register<HasNamedCharacterCondition>("has-named-character", {
74
+ complexity: 40,
75
+ evaluate: (condition, sourceCard, { state, registry }) => {
76
+ const zoneFilter = getPlayZoneFilter(
77
+ condition.controller ?? "you",
78
+ sourceCard,
79
+ );
80
+
81
+ // Check if ANY card matches
82
+ return Object.values(state.internal.cards).some((c) => {
83
+ const card = c as CardInstance<LorcanaCardMeta>;
84
+ if (!zoneFilter(card)) return false;
85
+ if (card.definitionId === undefined) return false;
86
+
87
+ const def = registry.getCard(card.definitionId);
88
+ // Usually "named X" implies the name property.
89
+ return (
90
+ def && (def.name === condition.name || def.fullName === condition.name)
91
+ );
92
+ });
93
+ },
94
+ });
95
+
96
+ // Register HasCharacterWithClassificationCondition
97
+ conditionRegistry.register<HasCharacterWithClassificationCondition>(
98
+ "has-character-with-classification",
99
+ {
100
+ complexity: 45,
101
+ evaluate: (condition, sourceCard, { state, registry }) => {
102
+ const zoneFilter = getPlayZoneFilter(condition.controller, sourceCard);
103
+
104
+ return Object.values(state.internal.cards).some((c) => {
105
+ const card = c as CardInstance<LorcanaCardMeta>;
106
+ if (!zoneFilter(card)) return false;
107
+
108
+ // This condition doesn't seem to have a dedicated filterResolver helper in DSL?
109
+ // But we can construct one or manually check.
110
+ const def = registry.getCard(card.definitionId);
111
+ if (def?.cardType !== "character") return false;
112
+
113
+ return def.classifications?.includes(condition.classification as any);
114
+ });
115
+ },
116
+ },
117
+ );
118
+
119
+ // Register HasCharacterCountCondition
120
+ conditionRegistry.register<HasCharacterCountCondition>("has-character-count", {
121
+ complexity: 50,
122
+ evaluate: (condition, sourceCard, { state, registry }) => {
123
+ const zoneFilter = getPlayZoneFilter(
124
+ condition.controller ?? "you",
125
+ sourceCard,
126
+ );
127
+
128
+ let count = 0;
129
+
130
+ // Check if there are generic filters?
131
+ // The condition usually implies "Has X characters"
132
+ // Usually no extra filters unless specified.
133
+
134
+ count = countCards(state, (c) => {
135
+ if (!zoneFilter(c)) return false;
136
+ const def = registry.getCard(c.definitionId);
137
+ return def?.cardType === "character";
138
+ });
139
+
140
+ return compare(count, condition.comparison ?? "gte", condition.count ?? 1);
141
+ },
142
+ });
143
+
144
+ // Register ResourceCountCondition (Ink, Cards in Hand, etc.)
145
+ conditionRegistry.register<ResourceCountCondition>("resource-count", {
146
+ complexity: 40,
147
+ evaluate: (condition, sourceCard, { state, registry }) => {
148
+ // Check controller
149
+ const targetOwnerId =
150
+ condition.controller === "you"
151
+ ? sourceCard.controller
152
+ : Object.keys(state.external.loreScores).find(
153
+ (id) => id !== sourceCard.controller,
154
+ );
155
+
156
+ if (!targetOwnerId) return false;
157
+
158
+ // Helper for specific resource type
159
+ const isTargetController = (cId: string) => cId === targetOwnerId;
160
+
161
+ let count = 0;
162
+ switch (condition.what) {
163
+ case "cards-in-hand":
164
+ count = countCards(state, (c) => {
165
+ return c.zone === "hand" && isTargetController(c.controller);
166
+ });
167
+ break;
168
+
169
+ case "cards-in-inkwell":
170
+ count = countCards(state, (c) => {
171
+ return c.zone === "inkwell" && isTargetController(c.controller);
172
+ });
173
+ break;
174
+
175
+ case "characters":
176
+ // This duplicates has-character-count but under resource-count umbrella
177
+ count = countCards(state, (c) => {
178
+ if (c.zone !== "play" || !isTargetController(c.controller))
179
+ return false;
180
+ const def = registry.getCard(c.definitionId);
181
+ return def?.cardType === "character";
182
+ });
183
+ break;
184
+
185
+ case "items":
186
+ count = countCards(state, (c) => {
187
+ if (c.zone !== "play" || !isTargetController(c.controller))
188
+ return false;
189
+ const def = registry.getCard(c.definitionId);
190
+ return def?.cardType === "item";
191
+ });
192
+ break;
193
+
194
+ case "locations":
195
+ count = countCards(state, (c) => {
196
+ if (c.zone !== "play" || !isTargetController(c.controller))
197
+ return false;
198
+ const def = registry.getCard(c.definitionId);
199
+ return def?.cardType === "location";
200
+ });
201
+ break;
202
+
203
+ case "damage-on-characters" as any:
204
+ // Special case: Total damage on ALL characters?
205
+ // Condition usually is "damage-on-self".
206
+ // If "resource-count" type "damage-on-characters" exists?
207
+ // I don't think it's robustly defined yet.
208
+ break;
209
+
210
+ default:
211
+ console.warn(
212
+ `Resource counting for ${condition.what} not fully implemented in existence.ts`,
213
+ );
214
+ return false;
215
+ }
216
+
217
+ return compare(count, condition.comparison, condition.value);
218
+ },
219
+ });
@@ -0,0 +1,68 @@
1
+ import type {
2
+ ThisTurnCountCondition,
3
+ ThisTurnHappenedCondition,
4
+ } from "@drmxrcy/tcg-lorcana-types";
5
+ import { conditionRegistry } from "../condition-registry";
6
+
7
+ conditionRegistry.register<ThisTurnHappenedCondition>("this-turn-happened", {
8
+ complexity: 60,
9
+ evaluate: (condition, sourceCard, { state }) => {
10
+ const events = state.external.turnHistory || [];
11
+ // We need to filter by controller
12
+ const matchedEvents = events.filter((e) => {
13
+ if (e.type !== condition.event) return false;
14
+
15
+ if (condition.who === "you") {
16
+ return e.controllerId === sourceCard.controller;
17
+ }
18
+
19
+ return e.controllerId !== sourceCard.controller;
20
+ });
21
+
22
+ return matchedEvents.length > 0;
23
+ },
24
+ });
25
+
26
+ conditionRegistry.register<ThisTurnCountCondition>("this-turn-count", {
27
+ complexity: 70,
28
+ evaluate: (condition, sourceCard, { state }) => {
29
+ const events = state.external.turnHistory || [];
30
+
31
+ const count = events.reduce((acc, e) => {
32
+ if (e.type !== condition.event) return acc;
33
+
34
+ let matchesWho = false;
35
+ if (condition.who === "you") {
36
+ matchesWho = e.controllerId === sourceCard.controller;
37
+ } else {
38
+ matchesWho = e.controllerId !== sourceCard.controller;
39
+ }
40
+
41
+ if (matchesWho) return acc + e.count;
42
+ return acc;
43
+ }, 0);
44
+
45
+ switch (condition.comparison) {
46
+ // @ts-expect-error - comparison operators mismatch
47
+ case "eq":
48
+ return count === condition.count;
49
+ // @ts-expect-error - comparison operators mismatch
50
+ case "ne":
51
+ return count !== condition.count;
52
+ // @ts-expect-error - comparison operators mismatch
53
+ case "gt":
54
+ return count > condition.count;
55
+ // @ts-expect-error - comparison operators mismatch
56
+ case "gte":
57
+ return count >= condition.count;
58
+ // @ts-expect-error - comparison operators mismatch in shared types
59
+ case "lt":
60
+ return count < condition.count;
61
+ // @ts-expect-error - comparison operators mismatch in shared types
62
+ case "lte":
63
+ return count <= condition.count;
64
+ default:
65
+ return false;
66
+ }
67
+ },
68
+ });
@@ -0,0 +1,15 @@
1
+ import "./basic";
2
+ import "./logical";
3
+ import "./resolution";
4
+ import "./existence";
5
+ import "./comparison";
6
+ import "./history";
7
+ import "./zone";
8
+ import "./card-state";
9
+ import "./revealed";
10
+
11
+ // Ensure all conditions are registered by side effect
12
+ export * from "./basic";
13
+ export * from "./existence";
14
+ export * from "./logical";
15
+ export * from "./resolution";
@@ -0,0 +1,55 @@
1
+ import type {
2
+ AndCondition,
3
+ NotCondition,
4
+ OrCondition,
5
+ } from "@drmxrcy/tcg-lorcana-types";
6
+ import { conditionRegistry } from "../condition-registry";
7
+ import { isConditionMet } from "../condition-resolver";
8
+
9
+ // Register AND Condition
10
+ conditionRegistry.register<AndCondition>("and", {
11
+ complexity: 25, // Higher than simple, depends on children
12
+ evaluate: (condition, sourceCard, context) => {
13
+ // Short-circuit: if any is false, return false
14
+ return condition.conditions.every((subCondition) =>
15
+ isConditionMet(
16
+ subCondition,
17
+ sourceCard,
18
+ context.state,
19
+ context.registry,
20
+ context.context,
21
+ ),
22
+ );
23
+ },
24
+ });
25
+
26
+ // Register OR Condition
27
+ conditionRegistry.register<OrCondition>("or", {
28
+ complexity: 25,
29
+ evaluate: (condition, sourceCard, context) => {
30
+ // Short-circuit: if any is true, return true
31
+ return condition.conditions.some((subCondition) =>
32
+ isConditionMet(
33
+ subCondition,
34
+ sourceCard,
35
+ context.state,
36
+ context.registry,
37
+ context.context,
38
+ ),
39
+ );
40
+ },
41
+ });
42
+
43
+ // Register NOT Condition
44
+ conditionRegistry.register<NotCondition>("not", {
45
+ complexity: 15,
46
+ evaluate: (condition, sourceCard, context) => {
47
+ return !isConditionMet(
48
+ condition.condition,
49
+ sourceCard,
50
+ context.state,
51
+ context.registry,
52
+ context.context,
53
+ );
54
+ },
55
+ });
@@ -0,0 +1,41 @@
1
+ import type {
2
+ ResolutionCondition,
3
+ UsedShiftCondition,
4
+ } from "@drmxrcy/tcg-lorcana-types";
5
+ import { conditionRegistry } from "../condition-registry";
6
+
7
+ // Register Used-Shift Condition
8
+ conditionRegistry.register<UsedShiftCondition>("used-shift", {
9
+ complexity: 5,
10
+ evaluate: (_condition, sourceCard) => {
11
+ // A card is shifted if it has cards underneath it in the stack
12
+ return !!(
13
+ sourceCard.stackPosition?.cardsUnderneath &&
14
+ sourceCard.stackPosition.cardsUnderneath.length > 0
15
+ );
16
+ },
17
+ });
18
+
19
+ // Register Resolution Condition (Legacy/Context)
20
+ conditionRegistry.register<ResolutionCondition>("resolution", {
21
+ complexity: 10,
22
+ evaluate: (condition, sourceCard, { context }) => {
23
+ if (condition.value === "bodyguard") {
24
+ // Check if we are currently resolving a Bodyguard trigger/check
25
+ return context?.resolutionContext === "bodyguard";
26
+ }
27
+
28
+ if (condition.value === "shift") {
29
+ // Check if the card is shifted (similar to used-shift)
30
+ // Or if we are in "shift" resolution?
31
+ // The old docs said "Checks sourceCard.hasShift && sourceCard.meta.shifted"
32
+ // We'll treat it as "is currently a shifted character"
33
+ return !!(
34
+ sourceCard.stackPosition?.cardsUnderneath &&
35
+ sourceCard.stackPosition.cardsUnderneath.length > 0
36
+ );
37
+ }
38
+
39
+ return false;
40
+ },
41
+ });