@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.
- package/README.md +160 -0
- package/package.json +45 -0
- package/src/__tests__/integration/move-enumeration.test.ts +256 -0
- package/src/__tests__/rules/section-01-concepts.test.ts +426 -0
- package/src/__tests__/rules/section-03-gameplay.test.ts +298 -0
- package/src/__tests__/rules/section-04-turn-structure.test.ts +708 -0
- package/src/__tests__/rules/section-05-cards.test.ts +158 -0
- package/src/__tests__/rules/section-06-card-types.test.ts +342 -0
- package/src/__tests__/rules/section-07-abilities.test.ts +333 -0
- package/src/__tests__/rules/section-08-zones.test.ts +231 -0
- package/src/__tests__/rules/section-09-damage.test.ts +148 -0
- package/src/__tests__/rules/section-10-keywords.test.ts +469 -0
- package/src/__tests__/spec-01-foundation-types.test.ts +534 -0
- package/src/__tests__/spec-02-zones-card-states.test.ts +295 -0
- package/src/card-utils.ts +302 -0
- package/src/cards/README.md +296 -0
- package/src/cards/abilities/index.ts +175 -0
- package/src/cards/index.ts +10 -0
- package/src/deck-validation.ts +175 -0
- package/src/engine/lorcana-engine.ts +625 -0
- package/src/game-definition/__tests__/core-zone-integration.test.ts +553 -0
- package/src/game-definition/__tests__/zone-operations.test.ts +362 -0
- package/src/game-definition/__tests__/zones.test.ts +176 -0
- package/src/game-definition/definition.ts +45 -0
- package/src/game-definition/flow/turn-flow.ts +216 -0
- package/src/game-definition/index.ts +31 -0
- package/src/game-definition/moves/abilities/activate-ability.ts +51 -0
- package/src/game-definition/moves/core/__tests__/move-parameter-enumeration.test.ts +316 -0
- package/src/game-definition/moves/core/challenge.test.ts +545 -0
- package/src/game-definition/moves/core/challenge.ts +81 -0
- package/src/game-definition/moves/core/play-card.ts +83 -0
- package/src/game-definition/moves/core/quest.test.ts +448 -0
- package/src/game-definition/moves/core/quest.ts +49 -0
- package/src/game-definition/moves/debug/manual-exert.ts +36 -0
- package/src/game-definition/moves/effects/resolve-bag.ts +35 -0
- package/src/game-definition/moves/effects/resolve-effect.ts +34 -0
- package/src/game-definition/moves/index.ts +85 -0
- package/src/game-definition/moves/locations/move-character-to-location.ts +42 -0
- package/src/game-definition/moves/resources/put-card-into-inkwell.test.ts +462 -0
- package/src/game-definition/moves/resources/put-card-into-inkwell.ts +51 -0
- package/src/game-definition/moves/setup/alter-hand.test.ts +395 -0
- package/src/game-definition/moves/setup/alter-hand.ts +210 -0
- package/src/game-definition/moves/setup/choose-first-player.test.ts +450 -0
- package/src/game-definition/moves/setup/choose-first-player.ts +105 -0
- package/src/game-definition/moves/setup/draw-cards.ts +37 -0
- package/src/game-definition/moves/songs/sing-together.ts +47 -0
- package/src/game-definition/moves/songs/sing.ts +56 -0
- package/src/game-definition/moves/standard/concede.test.ts +189 -0
- package/src/game-definition/moves/standard/concede.ts +72 -0
- package/src/game-definition/moves/standard/pass-turn.ts +49 -0
- package/src/game-definition/setup/game-setup.ts +19 -0
- package/src/game-definition/trackers/tracker-config.ts +23 -0
- package/src/game-definition/win-conditions/lore-victory.ts +26 -0
- package/src/game-definition/zone-operations.ts +405 -0
- package/src/game-definition/zones/zone-configs.ts +59 -0
- package/src/game-definition/zones.ts +283 -0
- package/src/index.ts +189 -0
- package/src/operations/index.ts +7 -0
- package/src/operations/lorcana-operations.ts +288 -0
- package/src/queries/README.md +56 -0
- package/src/resolvers/__tests__/condition-resolver.test.ts +301 -0
- package/src/resolvers/condition-registry.ts +70 -0
- package/src/resolvers/condition-resolver.ts +85 -0
- package/src/resolvers/conditions/basic.ts +81 -0
- package/src/resolvers/conditions/card-state.ts +12 -0
- package/src/resolvers/conditions/comparison.ts +102 -0
- package/src/resolvers/conditions/existence.ts +219 -0
- package/src/resolvers/conditions/history.ts +68 -0
- package/src/resolvers/conditions/index.ts +15 -0
- package/src/resolvers/conditions/logical.ts +55 -0
- package/src/resolvers/conditions/resolution.ts +41 -0
- package/src/resolvers/conditions/revealed.ts +42 -0
- package/src/resolvers/conditions/zone.ts +84 -0
- package/src/setup.test.ts +18 -0
- package/src/targeting/__tests__/filter-resolver.test.ts +294 -0
- package/src/targeting/__tests__/real-cards-targeting.test.ts +303 -0
- package/src/targeting/__tests__/targeting-dsl.test.ts +386 -0
- package/src/targeting/enum-expansion.ts +387 -0
- package/src/targeting/filter-registry.ts +322 -0
- package/src/targeting/filter-resolver.ts +145 -0
- package/src/targeting/index.ts +91 -0
- package/src/targeting/lorcana-target-dsl.ts +495 -0
- package/src/targeting/targeting-ui.ts +407 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/lorcana-test-engine.ts +813 -0
- package/src/types/README.md +303 -0
- package/src/types/__tests__/lorcana-state.test.ts +168 -0
- package/src/types/__tests__/move-enumeration.test.ts +179 -0
- package/src/types/branded-types.ts +106 -0
- package/src/types/game-state.ts +184 -0
- package/src/types/index.ts +87 -0
- package/src/types/keywords.ts +187 -0
- package/src/types/lorcana-state.ts +260 -0
- package/src/types/move-enumeration.ts +126 -0
- package/src/types/move-params.ts +216 -0
- package/src/validators/index.ts +7 -0
- package/src/validators/move-validators.ts +374 -0
- package/src/zones/card-state.ts +234 -0
- package/src/zones/index.ts +42 -0
- 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
|
+
});
|