@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,296 @@
|
|
|
1
|
+
# Cards
|
|
2
|
+
|
|
3
|
+
This directory contains card definitions, abilities, and card-related functionality for Lorcana.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
### Card Definitions
|
|
8
|
+
|
|
9
|
+
- **`card-definitions/`** - Card data organized by set
|
|
10
|
+
- `set-001/` - The First Chapter cards
|
|
11
|
+
- `set-002/` - Rise of the Floodborn cards
|
|
12
|
+
- `set-003/` - Into the Inklands cards
|
|
13
|
+
- `index.ts` - Card registry aggregating all sets
|
|
14
|
+
|
|
15
|
+
### Abilities
|
|
16
|
+
|
|
17
|
+
- **`abilities/`** - Card ability definitions
|
|
18
|
+
- `keywords/` - Keyword abilities (Bodyguard, Challenger, Evasive, etc.)
|
|
19
|
+
- `triggered/` - Triggered abilities (When/Whenever)
|
|
20
|
+
- `activated/` - Activated abilities (tap to activate)
|
|
21
|
+
- `static/` - Static abilities (always active)
|
|
22
|
+
- `index.ts` - Ability registry
|
|
23
|
+
|
|
24
|
+
### Card Types
|
|
25
|
+
|
|
26
|
+
- **`card-types.ts`** - Lorcana-specific card type definitions
|
|
27
|
+
- **`card-instance.ts`** - Runtime card instance management
|
|
28
|
+
- **`card-queries.ts`** - Helper functions for querying cards
|
|
29
|
+
- **`index.ts`** - Public exports
|
|
30
|
+
|
|
31
|
+
## Purpose
|
|
32
|
+
|
|
33
|
+
This directory defines all Lorcana cards and their abilities, structured to work with the `@drmxrcy/tcg-core` card system.
|
|
34
|
+
|
|
35
|
+
## Card Definition Pattern
|
|
36
|
+
|
|
37
|
+
Cards are defined declaratively:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import type { CardDefinition } from "@drmxrcy/tcg-core";
|
|
41
|
+
import type { LorcanaCard } from "../card-types";
|
|
42
|
+
|
|
43
|
+
export const mickeyMouseTrueFriend: CardDefinition<LorcanaCard> = {
|
|
44
|
+
// Core properties
|
|
45
|
+
id: "001-001",
|
|
46
|
+
name: "Mickey Mouse - True Friend",
|
|
47
|
+
set: "001",
|
|
48
|
+
rarity: "legendary",
|
|
49
|
+
inkable: true,
|
|
50
|
+
|
|
51
|
+
// Lorcana-specific properties
|
|
52
|
+
cost: 8,
|
|
53
|
+
inkCost: 8,
|
|
54
|
+
type: "character",
|
|
55
|
+
color: "amber",
|
|
56
|
+
|
|
57
|
+
// Character properties
|
|
58
|
+
strength: 4,
|
|
59
|
+
willpower: 6,
|
|
60
|
+
loreValue: 3,
|
|
61
|
+
|
|
62
|
+
// Abilities
|
|
63
|
+
abilities: [
|
|
64
|
+
{
|
|
65
|
+
type: "keyword",
|
|
66
|
+
keyword: "challenger",
|
|
67
|
+
value: 5,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: "triggered",
|
|
71
|
+
trigger: "whenPlayed",
|
|
72
|
+
effect: "drawCards",
|
|
73
|
+
amount: 2,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
|
|
77
|
+
// Flavor text
|
|
78
|
+
flavorText: "A friend in need is a friend indeed.",
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Card Types
|
|
83
|
+
|
|
84
|
+
Lorcana has several card types:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export type LorcanaCardType =
|
|
88
|
+
| "character"
|
|
89
|
+
| "action"
|
|
90
|
+
| "item"
|
|
91
|
+
| "location"
|
|
92
|
+
| "song";
|
|
93
|
+
|
|
94
|
+
export type LorcanaCard = {
|
|
95
|
+
// Base card properties
|
|
96
|
+
id: CardId;
|
|
97
|
+
name: string;
|
|
98
|
+
type: LorcanaCardType;
|
|
99
|
+
cost: number;
|
|
100
|
+
inkCost: number;
|
|
101
|
+
color: LorcanaColor;
|
|
102
|
+
inkable: boolean;
|
|
103
|
+
rarity: LorcanaRarity;
|
|
104
|
+
set: string;
|
|
105
|
+
|
|
106
|
+
// Character-specific
|
|
107
|
+
strength?: number;
|
|
108
|
+
willpower?: number;
|
|
109
|
+
loreValue?: number;
|
|
110
|
+
|
|
111
|
+
// Abilities
|
|
112
|
+
abilities: LorcanaAbility[];
|
|
113
|
+
|
|
114
|
+
// Text
|
|
115
|
+
text?: string;
|
|
116
|
+
flavorText?: string;
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Keyword Abilities
|
|
121
|
+
|
|
122
|
+
Keyword abilities are standardized mechanics:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Bodyguard - Must be challenged before other characters
|
|
126
|
+
export const bodyguardAbility: KeywordAbility = {
|
|
127
|
+
keyword: "bodyguard",
|
|
128
|
+
|
|
129
|
+
// Modify challenge validation
|
|
130
|
+
modifyValidation: (state, context) => {
|
|
131
|
+
// Implementation: prevent challenging other characters
|
|
132
|
+
// if a bodyguard is available
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Challenger +N - Gets +N strength when challenging
|
|
137
|
+
export const challengerAbility: KeywordAbility = {
|
|
138
|
+
keyword: "challenger",
|
|
139
|
+
|
|
140
|
+
// Modify challenge damage
|
|
141
|
+
modifyDamage: (state, cardId, baseValue) => {
|
|
142
|
+
const card = getCard(state, cardId);
|
|
143
|
+
const challengerBonus = getKeywordValue(card, "challenger");
|
|
144
|
+
return baseValue + challengerBonus;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Triggered Abilities
|
|
150
|
+
|
|
151
|
+
Abilities that trigger on events:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
export type TriggeredAbility = {
|
|
155
|
+
type: "triggered";
|
|
156
|
+
trigger: TriggerTiming;
|
|
157
|
+
condition?: AbilityCondition;
|
|
158
|
+
effect: AbilityEffect;
|
|
159
|
+
target?: TargetDefinition;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Example: "When you play this character, draw 2 cards"
|
|
163
|
+
const drawOnPlayAbility: TriggeredAbility = {
|
|
164
|
+
type: "triggered",
|
|
165
|
+
trigger: "whenPlayed",
|
|
166
|
+
effect: {
|
|
167
|
+
type: "drawCards",
|
|
168
|
+
amount: 2,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Activated Abilities
|
|
174
|
+
|
|
175
|
+
Abilities players can choose to activate:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
export type ActivatedAbility = {
|
|
179
|
+
type: "activated";
|
|
180
|
+
cost?: AbilityCost;
|
|
181
|
+
effect: AbilityEffect;
|
|
182
|
+
target?: TargetDefinition;
|
|
183
|
+
restrictions?: AbilityRestriction[];
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Example: "Exert - Draw a card"
|
|
187
|
+
const exertToDrawAbility: ActivatedAbility = {
|
|
188
|
+
type: "activated",
|
|
189
|
+
cost: {
|
|
190
|
+
type: "exert",
|
|
191
|
+
},
|
|
192
|
+
effect: {
|
|
193
|
+
type: "drawCards",
|
|
194
|
+
amount: 1,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Card Registry
|
|
200
|
+
|
|
201
|
+
All cards registered for lookup:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// card-definitions/index.ts
|
|
205
|
+
import * as set001 from "./set-001";
|
|
206
|
+
import * as set002 from "./set-002";
|
|
207
|
+
|
|
208
|
+
export const allCards = {
|
|
209
|
+
...set001.cards,
|
|
210
|
+
...set002.cards,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const getCardDefinition = (cardId: CardId): CardDefinition => {
|
|
214
|
+
return allCards[cardId];
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Card Instances vs Definitions
|
|
219
|
+
|
|
220
|
+
- **CardDefinition**: The template (what the card does)
|
|
221
|
+
- **CardInstance**: The runtime instance (current state)
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Definition (static)
|
|
225
|
+
const mickeyDefinition: CardDefinition = {
|
|
226
|
+
id: "001-001",
|
|
227
|
+
name: "Mickey Mouse",
|
|
228
|
+
strength: 4,
|
|
229
|
+
willpower: 6,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Instance (dynamic)
|
|
233
|
+
const mickeyInstance: CardInstance = {
|
|
234
|
+
definitionId: "001-001",
|
|
235
|
+
instanceId: "game1-card-123",
|
|
236
|
+
ownerId: "player1",
|
|
237
|
+
zone: "play",
|
|
238
|
+
|
|
239
|
+
// Current state
|
|
240
|
+
damage: 2,
|
|
241
|
+
exerted: true,
|
|
242
|
+
playedThisTurn: true,
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Card Queries
|
|
247
|
+
|
|
248
|
+
Helper functions for common card operations:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
export const getCardsInZone = (
|
|
252
|
+
state: LorcanaState,
|
|
253
|
+
zone: ZoneId,
|
|
254
|
+
playerId: PlayerId
|
|
255
|
+
): CardInstance[] => {
|
|
256
|
+
return state.zones[zone][playerId].map(
|
|
257
|
+
cardId => state.cards[cardId]
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const getReadyCharacters = (
|
|
262
|
+
state: LorcanaState,
|
|
263
|
+
playerId: PlayerId
|
|
264
|
+
): CardInstance[] => {
|
|
265
|
+
return getCardsInZone(state, "play", playerId).filter(
|
|
266
|
+
card => card.type === "character" && !card.exerted
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Testing Cards
|
|
272
|
+
|
|
273
|
+
Card abilities are tested through integration tests:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
describe("Mickey Mouse - True Friend", () => {
|
|
277
|
+
it("draws 2 cards when played", () => {
|
|
278
|
+
const engine = createTestEngine();
|
|
279
|
+
const initialHandSize = getHandSize(engine.getState(), "player1");
|
|
280
|
+
|
|
281
|
+
engine.executeMove("playCard", {
|
|
282
|
+
playerId: "player1",
|
|
283
|
+
params: { cardId: "mickey-true-friend" },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const finalHandSize = getHandSize(engine.getState(), "player1");
|
|
287
|
+
expect(finalHandSize).toBe(initialHandSize + 2 - 1); // +2 draw, -1 played
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## References
|
|
293
|
+
|
|
294
|
+
- See `@packages/core/src/cards/` for base card system
|
|
295
|
+
- See `@packages/core/ENGINE_INTEGRATION.md` for card integration guide
|
|
296
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lorcana Ability System
|
|
3
|
+
*
|
|
4
|
+
* Provides a comprehensive, serializable type system for representing
|
|
5
|
+
* all Lorcana card abilities.
|
|
6
|
+
*
|
|
7
|
+
* ## Overview
|
|
8
|
+
*
|
|
9
|
+
* Lorcana abilities are composed of:
|
|
10
|
+
* - **Effects**: Atomic game actions (draw, damage, gain lore, etc.)
|
|
11
|
+
* - **Triggers**: Events that cause abilities to fire
|
|
12
|
+
* - **Conditions**: Requirements that must be met
|
|
13
|
+
* - **Costs**: What must be paid to activate abilities
|
|
14
|
+
* - **Targets**: Who/what is affected
|
|
15
|
+
*
|
|
16
|
+
* ## Ability Types
|
|
17
|
+
*
|
|
18
|
+
* - **Keyword**: Simple abilities like Rush, Ward, Challenger +X
|
|
19
|
+
* - **Triggered**: Fire when events occur (When/Whenever/At)
|
|
20
|
+
* - **Activated**: Player chooses to use by paying cost
|
|
21
|
+
* - **Static**: Always active, modify game state
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import {
|
|
27
|
+
* Ability,
|
|
28
|
+
* keyword,
|
|
29
|
+
* triggered,
|
|
30
|
+
* COMMON_TRIGGERS,
|
|
31
|
+
* } from "@lorcana/cards/abilities";
|
|
32
|
+
*
|
|
33
|
+
* // Simple keyword
|
|
34
|
+
* const rush: Ability = keyword("Rush");
|
|
35
|
+
*
|
|
36
|
+
* // Challenger +3
|
|
37
|
+
* const challengerAbility: Ability = challenger(3);
|
|
38
|
+
*
|
|
39
|
+
* // "When you play this character, draw 2 cards"
|
|
40
|
+
* const drawOnPlay: Ability = triggered(
|
|
41
|
+
* COMMON_TRIGGERS.WHEN_PLAY_SELF,
|
|
42
|
+
* { type: "draw", amount: 2, target: "CONTROLLER" }
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ## Design Principles
|
|
47
|
+
*
|
|
48
|
+
* 1. **Serializable**: All types can be serialized to JSON
|
|
49
|
+
* 2. **Composable**: Effects combine into complex abilities
|
|
50
|
+
* 3. **Type-safe**: Discriminated unions with type guards
|
|
51
|
+
* 4. **Executable**: Structure is directly usable by game engine
|
|
52
|
+
*
|
|
53
|
+
* @module abilities
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
// Re-export all types from lorcana-types
|
|
57
|
+
export * from "@drmxrcy/tcg-lorcana-types/abilities";
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Version Info
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Version of the ability type system
|
|
65
|
+
* Increment when making breaking changes
|
|
66
|
+
*/
|
|
67
|
+
export const ABILITY_TYPES_VERSION = "1.0.0";
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Quick Reference: Common Ability Patterns
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Example abilities for reference and testing
|
|
75
|
+
*/
|
|
76
|
+
export const EXAMPLE_ABILITIES = {
|
|
77
|
+
/**
|
|
78
|
+
* Simple Rush keyword
|
|
79
|
+
*/
|
|
80
|
+
rush: {
|
|
81
|
+
type: "keyword",
|
|
82
|
+
keyword: "Rush",
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Challenger +3
|
|
87
|
+
*/
|
|
88
|
+
challengerPlus3: {
|
|
89
|
+
type: "keyword",
|
|
90
|
+
keyword: "Challenger",
|
|
91
|
+
value: 3,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resist +2
|
|
96
|
+
*/
|
|
97
|
+
resistPlus2: {
|
|
98
|
+
type: "keyword",
|
|
99
|
+
keyword: "Resist",
|
|
100
|
+
value: 2,
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Shift 5 (can Shift onto any matching character)
|
|
105
|
+
*/
|
|
106
|
+
shift5: {
|
|
107
|
+
type: "keyword",
|
|
108
|
+
keyword: "Shift",
|
|
109
|
+
shiftCost: 5,
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* "When you play this character, draw 2 cards"
|
|
114
|
+
*/
|
|
115
|
+
drawOnPlay: {
|
|
116
|
+
type: "triggered",
|
|
117
|
+
trigger: { event: "play", timing: "when", on: "SELF" },
|
|
118
|
+
effect: { type: "draw", amount: 2, target: "CONTROLLER" },
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* "Whenever this character quests, gain 1 lore"
|
|
123
|
+
*/
|
|
124
|
+
gainLoreOnQuest: {
|
|
125
|
+
type: "triggered",
|
|
126
|
+
trigger: { event: "quest", timing: "whenever", on: "SELF" },
|
|
127
|
+
effect: { type: "gain-lore", amount: 1 },
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* "{E} - Draw a card"
|
|
132
|
+
*/
|
|
133
|
+
exertToDraw: {
|
|
134
|
+
type: "activated",
|
|
135
|
+
cost: { exert: true },
|
|
136
|
+
effect: { type: "draw", amount: 1, target: "CONTROLLER" },
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* "{E}, 2 {I} - Deal 3 damage to chosen character"
|
|
141
|
+
*/
|
|
142
|
+
exertInkToDamage: {
|
|
143
|
+
type: "activated",
|
|
144
|
+
cost: { exert: true, ink: 2 },
|
|
145
|
+
effect: { type: "deal-damage", amount: 3, target: "CHOSEN_CHARACTER" },
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* "Your characters gain Ward"
|
|
150
|
+
*/
|
|
151
|
+
yourCharactersGainWard: {
|
|
152
|
+
type: "static",
|
|
153
|
+
effect: {
|
|
154
|
+
type: "gain-keyword",
|
|
155
|
+
keyword: "Ward",
|
|
156
|
+
target: "YOUR_CHARACTERS",
|
|
157
|
+
duration: "while-condition",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* "While this character has no damage, he gets +2 Strength"
|
|
163
|
+
*/
|
|
164
|
+
bonusWhileNoDamage: {
|
|
165
|
+
type: "static",
|
|
166
|
+
condition: { type: "no-damage" },
|
|
167
|
+
effect: {
|
|
168
|
+
type: "modify-stat",
|
|
169
|
+
stat: "strength",
|
|
170
|
+
modifier: 2,
|
|
171
|
+
target: "SELF",
|
|
172
|
+
duration: "while-condition",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
} as const;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deck Validation (Rule 2.1)
|
|
3
|
+
*
|
|
4
|
+
* Validates a deck against Lorcana deck building rules:
|
|
5
|
+
* - Minimum 60 cards (Rule 2.1.1.1)
|
|
6
|
+
* - Maximum 2 ink types (Rule 2.1.1.2)
|
|
7
|
+
* - Maximum 4 copies per full name (Rule 2.1.1.3)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
DeckStats,
|
|
12
|
+
DeckValidationError,
|
|
13
|
+
DeckValidationResult,
|
|
14
|
+
InkType,
|
|
15
|
+
LorcanaCardDefinition,
|
|
16
|
+
} from "@drmxrcy/tcg-lorcana-types";
|
|
17
|
+
import {
|
|
18
|
+
getFullName,
|
|
19
|
+
getInkTypes,
|
|
20
|
+
MAX_COPIES_PER_CARD,
|
|
21
|
+
MAX_INK_TYPES,
|
|
22
|
+
MIN_DECK_SIZE,
|
|
23
|
+
} from "@drmxrcy/tcg-lorcana-types";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate a deck against Lorcana rules
|
|
27
|
+
*
|
|
28
|
+
* @param cards - Array of card definitions in the deck
|
|
29
|
+
* @returns Validation result with any errors
|
|
30
|
+
*/
|
|
31
|
+
export function validateDeck(
|
|
32
|
+
cards: LorcanaCardDefinition[],
|
|
33
|
+
): DeckValidationResult {
|
|
34
|
+
const errors: DeckValidationError[] = [];
|
|
35
|
+
|
|
36
|
+
// Rule 2.1.1.1: Minimum 60 cards
|
|
37
|
+
if (cards.length < MIN_DECK_SIZE) {
|
|
38
|
+
errors.push({
|
|
39
|
+
type: "TOO_FEW_CARDS",
|
|
40
|
+
count: cards.length,
|
|
41
|
+
minimum: MIN_DECK_SIZE,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Rule 2.1.1.2: Maximum 2 ink types
|
|
46
|
+
const inkTypes = getUniqueInkTypes(cards);
|
|
47
|
+
if (inkTypes.length > MAX_INK_TYPES) {
|
|
48
|
+
errors.push({
|
|
49
|
+
type: "TOO_MANY_INK_TYPES",
|
|
50
|
+
inkTypes,
|
|
51
|
+
maximum: MAX_INK_TYPES,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Rule 2.1.1.3: Maximum copies per full name (default 4, can be overridden)
|
|
56
|
+
const cardCounts = getCardCounts(cards);
|
|
57
|
+
const copyLimits = getCardCopyLimits(cards);
|
|
58
|
+
|
|
59
|
+
for (const [fullName, count] of cardCounts.entries()) {
|
|
60
|
+
const limit = copyLimits.get(fullName);
|
|
61
|
+
|
|
62
|
+
// Skip validation for cards with no limit
|
|
63
|
+
if (limit === "no-limit") {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const maxCopies = limit ?? MAX_COPIES_PER_CARD;
|
|
68
|
+
if (count > maxCopies) {
|
|
69
|
+
errors.push({
|
|
70
|
+
type: "TOO_MANY_COPIES",
|
|
71
|
+
fullName,
|
|
72
|
+
count,
|
|
73
|
+
maximum: maxCopies,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
valid: errors.length === 0,
|
|
80
|
+
errors,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get all unique ink types in a deck
|
|
86
|
+
*/
|
|
87
|
+
export function getUniqueInkTypes(cards: LorcanaCardDefinition[]): InkType[] {
|
|
88
|
+
const inkSet = new Set<InkType>();
|
|
89
|
+
for (const card of cards) {
|
|
90
|
+
for (const ink of getInkTypes(card)) {
|
|
91
|
+
inkSet.add(ink);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Array.from(inkSet);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Count cards by full name
|
|
99
|
+
*/
|
|
100
|
+
export function getCardCounts(
|
|
101
|
+
cards: LorcanaCardDefinition[],
|
|
102
|
+
): Map<string, number> {
|
|
103
|
+
const counts = new Map<string, number>();
|
|
104
|
+
for (const card of cards) {
|
|
105
|
+
const fullName = getFullName(card);
|
|
106
|
+
counts.set(fullName, (counts.get(fullName) ?? 0) + 1);
|
|
107
|
+
}
|
|
108
|
+
return counts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get card copy limits by full name
|
|
113
|
+
* Returns undefined for default limit (4), number for custom limit, or "no-limit" for unlimited
|
|
114
|
+
*/
|
|
115
|
+
export function getCardCopyLimits(
|
|
116
|
+
cards: LorcanaCardDefinition[],
|
|
117
|
+
): Map<string, number | "no-limit" | undefined> {
|
|
118
|
+
const limits = new Map<string, number | "no-limit" | undefined>();
|
|
119
|
+
for (const card of cards) {
|
|
120
|
+
const fullName = getFullName(card);
|
|
121
|
+
if (card.cardCopyLimit !== undefined && !limits.has(fullName)) {
|
|
122
|
+
limits.set(fullName, card.cardCopyLimit);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return limits;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Calculate deck statistics
|
|
130
|
+
*/
|
|
131
|
+
export function getDeckStats(cards: LorcanaCardDefinition[]): DeckStats {
|
|
132
|
+
const cardCounts = getCardCounts(cards);
|
|
133
|
+
const inkTypes = getUniqueInkTypes(cards);
|
|
134
|
+
|
|
135
|
+
const cardTypeBreakdown = {
|
|
136
|
+
characters: 0,
|
|
137
|
+
actions: 0,
|
|
138
|
+
items: 0,
|
|
139
|
+
locations: 0,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
let inkableCards = 0;
|
|
143
|
+
let totalCost = 0;
|
|
144
|
+
|
|
145
|
+
for (const card of cards) {
|
|
146
|
+
switch (card.cardType) {
|
|
147
|
+
case "character":
|
|
148
|
+
cardTypeBreakdown.characters++;
|
|
149
|
+
break;
|
|
150
|
+
case "action":
|
|
151
|
+
cardTypeBreakdown.actions++;
|
|
152
|
+
break;
|
|
153
|
+
case "item":
|
|
154
|
+
cardTypeBreakdown.items++;
|
|
155
|
+
break;
|
|
156
|
+
case "location":
|
|
157
|
+
cardTypeBreakdown.locations++;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (card.inkable) {
|
|
162
|
+
inkableCards++;
|
|
163
|
+
}
|
|
164
|
+
totalCost += card.cost;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
totalCards: cards.length,
|
|
169
|
+
inkTypes,
|
|
170
|
+
cardCounts,
|
|
171
|
+
cardTypeBreakdown,
|
|
172
|
+
inkableCards,
|
|
173
|
+
averageCost: cards.length > 0 ? totalCost / cards.length : 0,
|
|
174
|
+
};
|
|
175
|
+
}
|