@drmxrcy/tcg-lorcana-types 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.
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Card Types (Section 6)
3
+ *
4
+ * Four card types with distinct behaviors and properties.
5
+ * Provides both a unified LorcanaCardDefinition and discriminated union types
6
+ * for type-safe access to card-type-specific properties.
7
+ */
8
+
9
+ import type {
10
+ ActionAbility,
11
+ ActivatedAbility,
12
+ KeywordAbility,
13
+ ReplacementAbility,
14
+ StaticAbility,
15
+ TriggeredAbility,
16
+ } from "../abilities/ability-types";
17
+ import type { Classification } from "./classifications";
18
+ import type { InkType } from "./ink-types";
19
+
20
+ /** Card Types (Section 6) */
21
+ export const CARD_TYPES = ["character", "action", "item", "location"] as const;
22
+ export type CardType = (typeof CARD_TYPES)[number];
23
+
24
+ /** Action subtypes */
25
+ export type ActionSubtype = "song" | null;
26
+
27
+ /**
28
+ * Base interface for all ability definitions in card data
29
+ * Adds card-definition-specific metadata to the comprehensive ability types
30
+ */
31
+ export interface BaseAbilityDefinition {
32
+ /** Unique identifier for this ability instance */
33
+ id?: string;
34
+
35
+ /**
36
+ * Optional name for named abilities (e.g., "I SUMMON THEE", "DISASSEMBLE")
37
+ * Typically appears in ALL CAPS before the ability text
38
+ */
39
+ name?: string;
40
+
41
+ /**
42
+ * Original card text for this ability
43
+ * Used for display and debugging
44
+ */
45
+ text?: string;
46
+ }
47
+
48
+ /**
49
+ * Keyword ability definition
50
+ * @example { type: "keyword", keyword: "Rush", id: "card-001-ability-1", text: "Rush" }
51
+ * @example { type: "keyword", keyword: "Challenger", value: 3, id: "card-002-ability-1", text: "Challenger +3" }
52
+ * @example { type: "keyword", keyword: "Shift", cost: { ink: 5 }, id: "card-003-ability-1", text: "Shift 5" }
53
+ */
54
+ export type KeywordAbilityDefinition = BaseAbilityDefinition & KeywordAbility;
55
+
56
+ /**
57
+ * Triggered ability definition
58
+ * @example {
59
+ * type: "triggered",
60
+ * trigger: { event: "play", timing: "when", on: "SELF" },
61
+ * effect: { type: "draw", amount: 2, target: "CONTROLLER" },
62
+ * id: "card-001-ability-1",
63
+ * text: "When you play this character, draw 2 cards."
64
+ * }
65
+ */
66
+ export interface TriggeredAbilityDefinition
67
+ extends BaseAbilityDefinition,
68
+ Omit<TriggeredAbility, "type"> {
69
+ type: "triggered";
70
+ }
71
+
72
+ /**
73
+ * Activated ability definition
74
+ * @example {
75
+ * type: "activated",
76
+ * name: "I SUMMON THEE",
77
+ * cost: { exert: true },
78
+ * effect: { type: "draw", amount: 1, target: "CONTROLLER" },
79
+ * id: "card-001-ability-1",
80
+ * text: "I SUMMON THEE {E} − Draw a card."
81
+ * }
82
+ */
83
+ export interface ActivatedAbilityDefinition
84
+ extends BaseAbilityDefinition,
85
+ Omit<ActivatedAbility, "type" | "name"> {
86
+ type: "activated";
87
+ }
88
+
89
+ /**
90
+ * Static ability definition
91
+ * @example {
92
+ * type: "static",
93
+ * condition: { type: "during-turn", whose: "your" },
94
+ * effect: { type: "gain-keyword", keyword: "Evasive", target: "SELF" },
95
+ * id: "card-001-ability-1",
96
+ * text: "During your turn, this character gains Evasive."
97
+ * }
98
+ */
99
+ export interface StaticAbilityDefinition
100
+ extends BaseAbilityDefinition,
101
+ Omit<StaticAbility, "type" | "name"> {
102
+ type: "static";
103
+ }
104
+
105
+ /**
106
+ * Action ability definition
107
+ * @example {
108
+ * type: "action",
109
+ * effect: { type: "draw", amount: 2, target: "CONTROLLER" },
110
+ * id: "card-001-ability-1",
111
+ * text: "Draw 2 cards."
112
+ * }
113
+ */
114
+ export interface ActionAbilityDefinition
115
+ extends BaseAbilityDefinition,
116
+ Omit<ActionAbility, "type"> {
117
+ type: "action";
118
+ }
119
+
120
+ /**
121
+ * Replacement ability definition
122
+ * @example {
123
+ * type: "replacement",
124
+ * replaces: "damage-to-self",
125
+ * replacement: "prevent",
126
+ * id: "card-001-ability-1",
127
+ * text: "If this character would be dealt damage, prevent that damage."
128
+ * }
129
+ */
130
+ export interface ReplacementAbilityDefinition
131
+ extends BaseAbilityDefinition,
132
+ Omit<ReplacementAbility, "type" | "name"> {
133
+ type: "replacement";
134
+ }
135
+
136
+ /**
137
+ * Comprehensive ability definition type
138
+ *
139
+ * Represents any ability that can appear on a Lorcana card.
140
+ * Each variant includes the full ability structure from the comprehensive
141
+ * ability system, plus card-definition-specific metadata (id, name, text).
142
+ *
143
+ * This type bridges the card definition layer (static card data) with
144
+ * the ability execution layer (runtime ability resolution).
145
+ */
146
+ export type AbilityDefinition =
147
+ | KeywordAbilityDefinition
148
+ | TriggeredAbilityDefinition
149
+ | ActivatedAbilityDefinition
150
+ | StaticAbilityDefinition
151
+ | ActionAbilityDefinition
152
+ | ReplacementAbilityDefinition;
153
+
154
+ // ============================================================================
155
+ // Base Card Properties (shared by all card types)
156
+ // ============================================================================
157
+
158
+ /**
159
+ * Base properties shared by all card types
160
+ */
161
+ export interface BaseCardProperties {
162
+ /** Unique identifier for the card */
163
+ id: string;
164
+
165
+ /** Card name (Rule 6.2.4) - e.g., "Elsa" */
166
+ name: string;
167
+
168
+ /** Card version (Rule 6.2.5) - e.g., "Ice Queen" */
169
+ version?: string;
170
+
171
+ /**
172
+ * Full name (name + version) - e.g., "Elsa - Ice Queen"
173
+ * Used for deck building limits (max 4 copies per full name)
174
+ * When not present, card name is equal to full name
175
+ */
176
+ fullName?: string;
177
+
178
+ /** Ink type (Rule 6.2.3) - single or dual ink */
179
+ inkType: InkType[];
180
+
181
+ /** Ink cost (Rule 6.2.7) */
182
+ cost: number;
183
+
184
+ /** Inkable - has inkwell symbol (Rule 6.2.8) */
185
+ inkable: boolean;
186
+
187
+ /** Card abilities (includes keywords) */
188
+ abilities?: AbilityDefinition[];
189
+
190
+ /** Rules text - raw ability text as printed on the card */
191
+ text?: string;
192
+
193
+ /** Flavor text (not mechanically relevant) */
194
+ flavorText?: string;
195
+
196
+ /** Set information */
197
+ set: string;
198
+
199
+ /** Card number in set */
200
+ cardNumber?: number;
201
+
202
+ /** Rarity */
203
+ rarity?:
204
+ | "common"
205
+ | "uncommon"
206
+ | "rare"
207
+ | "super_rare"
208
+ | "legendary"
209
+ | "enchanted"
210
+ | "iconic"
211
+ | "promo";
212
+
213
+ /**
214
+ * Special card copy limit rules
215
+ * - "no-limit": Unlimited copies allowed (e.g., Microbots)
216
+ * - number: Custom limit (e.g., 99 for Dalmatian Puppy, 2 for The Glass Slipper)
217
+ * - undefined: Default 4 copies per full name
218
+ */
219
+ cardCopyLimit?: number | "no-limit";
220
+
221
+ /** Franchise the card belongs to (e.g., "Jungle Book", "Frozen") */
222
+ franchise?: string;
223
+
224
+ /** Whether the card is vanilla (no abilities/rules text) */
225
+ vanilla?: boolean;
226
+
227
+ /** External IDs from various systems */
228
+ externalIds?: {
229
+ ravensburger?: string;
230
+ lorcast?: string;
231
+ tcgPlayer?: number;
232
+ };
233
+
234
+ /** Printing references for cards with multiple printings */
235
+ printings?: Array<{
236
+ set: string;
237
+ collectorNumber: number;
238
+ id: string;
239
+ }>;
240
+
241
+ /**
242
+ * Flag indicating that the card logic is not yet fully implemented
243
+ * Used for stubbed cards during development
244
+ */
245
+ missingImplementation?: boolean;
246
+
247
+ /**
248
+ * Flag indicating that the card lacks comprehensive tests
249
+ * Used for stubbed cards during development
250
+ */
251
+ missingTests?: boolean;
252
+ }
253
+
254
+ // ============================================================================
255
+ // Discriminated Union Types by Card Type
256
+ // ============================================================================
257
+
258
+ /**
259
+ * Character Card Definition (Rule 6.1.2)
260
+ *
261
+ * Characters have strength, willpower, lore value, and classifications.
262
+ * They can quest, challenge, and be challenged.
263
+ */
264
+ export interface CharacterCard extends BaseCardProperties {
265
+ cardType: "character";
266
+
267
+ /** Strength (Rule 6.2.9) - damage dealt in challenges */
268
+ strength: number;
269
+
270
+ /** Willpower (Rule 6.2.10) - damage threshold before banishment */
271
+ willpower: number;
272
+
273
+ /** Lore value (Rule 6.2.11) - lore gained when questing */
274
+ lore: number;
275
+
276
+ /** Character classifications (Rule 6.2.6) */
277
+ classifications?: Classification[];
278
+ }
279
+
280
+ /**
281
+ * Action Card Definition (Rule 6.1.3)
282
+ *
283
+ * Actions are one-time effects that are played and then discarded.
284
+ * Songs are a subtype of actions that can be sung by characters.
285
+ */
286
+ export interface ActionCard extends BaseCardProperties {
287
+ cardType: "action";
288
+
289
+ /** Action subtype (Rule 6.3.3) - "song" for Song cards */
290
+ actionSubtype?: ActionSubtype;
291
+ }
292
+
293
+ /**
294
+ * Item Card Definition (Rule 6.1.4)
295
+ *
296
+ * Items are permanent cards that provide ongoing effects.
297
+ * They stay in play until removed.
298
+ */
299
+ export interface ItemCard extends BaseCardProperties {
300
+ cardType: "item";
301
+ }
302
+
303
+ /**
304
+ * Location Card Definition (Rule 6.5)
305
+ *
306
+ * Locations have a move cost and lore value.
307
+ * Characters can move to locations and quest at them.
308
+ */
309
+ export interface LocationCard extends BaseCardProperties {
310
+ cardType: "location";
311
+
312
+ /** Move cost (Rule 6.5.5) - ink cost to move a character here */
313
+ moveCost: number;
314
+
315
+ /** Lore value - lore gained when questing at this location */
316
+ lore: number;
317
+ }
318
+
319
+ /**
320
+ * Discriminated union of all card types
321
+ * Use this when you need type-safe access to card-type-specific properties.
322
+ */
323
+ export type LorcanaCard = CharacterCard | ActionCard | ItemCard | LocationCard;
324
+
325
+ // ============================================================================
326
+ // Type Guards for Card Types
327
+ // ============================================================================
328
+
329
+ /**
330
+ * Check if a card is a character
331
+ */
332
+ export function isCharacterCard(card: LorcanaCard): card is CharacterCard {
333
+ return card.cardType === "character";
334
+ }
335
+
336
+ /**
337
+ * Check if a card is an action
338
+ */
339
+ export function isActionCard(card: LorcanaCard): card is ActionCard {
340
+ return card.cardType === "action";
341
+ }
342
+
343
+ /**
344
+ * Check if a card is an item
345
+ */
346
+ export function isItemCard(card: LorcanaCard): card is ItemCard {
347
+ return card.cardType === "item";
348
+ }
349
+
350
+ /**
351
+ * Check if a card is a location
352
+ */
353
+ export function isLocationCard(card: LorcanaCard): card is LocationCard {
354
+ return card.cardType === "location";
355
+ }
356
+
357
+ /**
358
+ * Lorcana Card Definition (Rule 6.2)
359
+ *
360
+ * Unified type with all properties optional based on card type.
361
+ * Use `LorcanaCard` discriminated union for type-safe access to
362
+ * card-type-specific properties.
363
+ *
364
+ * All card properties including:
365
+ * - id, name, version, fullName
366
+ * - inkType (single or dual)
367
+ * - cost, inkable (inkwell symbol)
368
+ * - cardType
369
+ * - Character-specific: strength, willpower, lore, classifications
370
+ * - Action-specific: actionSubtype (Song)
371
+ * - Location-specific: moveCost
372
+ * - abilities
373
+ */
374
+ export interface LorcanaCardDefinition {
375
+ /** Unique identifier for the card */
376
+ id: string;
377
+
378
+ /** Card name (Rule 6.2.4) - e.g., "Elsa" */
379
+ name: string;
380
+
381
+ /** Card version (Rule 6.2.5) - e.g., "Ice Queen" */
382
+ version?: string;
383
+
384
+ /**
385
+ * Full name (name + version) - e.g., "Elsa - Ice Queen"
386
+ * Used for deck building limits (max 4 copies per full name)
387
+ * When not present, card name is equal to full name
388
+ */
389
+ fullName?: string;
390
+
391
+ /** Ink type (Rule 6.2.3) - single or dual ink */
392
+ inkType: InkType[];
393
+
394
+ /** Ink cost (Rule 6.2.7) */
395
+ cost: number;
396
+
397
+ /** Inkable - has inkwell symbol (Rule 6.2.8) */
398
+ inkable: boolean;
399
+
400
+ /** Card type (Rule 6.1) */
401
+ cardType: CardType;
402
+
403
+ // Character-specific properties (Rule 6.1.2)
404
+
405
+ /** Strength (Rule 6.2.9) - damage dealt in challenges */
406
+ strength?: number;
407
+
408
+ /** Willpower (Rule 6.2.10) - damage threshold before banishment */
409
+ willpower?: number;
410
+
411
+ /** Lore value (Rule 6.2.11) - lore gained when questing */
412
+ lore?: number;
413
+
414
+ /** Character classifications (Rule 6.2.6) */
415
+ classifications?: Classification[];
416
+
417
+ // Action-specific properties
418
+
419
+ /** Action subtype (Rule 6.3.3) - "song" for Song cards */
420
+ actionSubtype?: ActionSubtype;
421
+
422
+ // Location-specific properties (Rule 6.5)
423
+
424
+ /** Move cost (Rule 6.5.5) - ink cost to move a character here */
425
+ moveCost?: number;
426
+
427
+ // Abilities
428
+
429
+ /** Card abilities (includes keywords) */
430
+ abilities?: AbilityDefinition[];
431
+
432
+ /** Rules text - raw ability text as printed on the card */
433
+ text?: string;
434
+
435
+ /** Flavor text (not mechanically relevant) */
436
+ flavorText?: string;
437
+
438
+ /** Set information */
439
+ set: string;
440
+
441
+ /** Card number in set */
442
+ cardNumber?: number;
443
+
444
+ /** Rarity */
445
+ rarity?:
446
+ | "common"
447
+ | "uncommon"
448
+ | "rare"
449
+ | "super_rare"
450
+ | "legendary"
451
+ | "enchanted"
452
+ | "iconic"
453
+ | "promo";
454
+
455
+ /**
456
+ * Special card copy limit rules
457
+ * - "no-limit": Unlimited copies allowed (e.g., Microbots)
458
+ * - number: Custom limit (e.g., 99 for Dalmatian Puppy, 2 for The Glass Slipper)
459
+ * - undefined: Default 4 copies per full name
460
+ */
461
+ cardCopyLimit?: number | "no-limit";
462
+
463
+ /** Franchise the card belongs to (e.g., "Jungle Book", "Frozen") */
464
+ franchise?: string;
465
+ }
466
+
467
+ /**
468
+ * Check if a card type is valid
469
+ */
470
+ export function isCardType(value: unknown): value is CardType {
471
+ return typeof value === "string" && CARD_TYPES.includes(value as CardType);
472
+ }
473
+
474
+ /**
475
+ * Get the full name of a card (name + version)
476
+ * Uses the stored fullName or generates it from name and version
477
+ */
478
+ export function getFullName(card: LorcanaCardDefinition | LorcanaCard): string {
479
+ if (card.fullName) {
480
+ return card.fullName;
481
+ }
482
+ if (card.version) {
483
+ return `${card.name} - ${card.version}`;
484
+ }
485
+ return card.name;
486
+ }
487
+
488
+ /**
489
+ * Check if a card has dual ink types (Rule 6.2.3.1)
490
+ */
491
+ export function isDualInk(card: LorcanaCardDefinition | LorcanaCard): boolean {
492
+ return card.inkType.length === 2;
493
+ }
494
+
495
+ /**
496
+ * Get all ink types from a card
497
+ */
498
+ export function getInkTypes(
499
+ card: LorcanaCardDefinition | LorcanaCard,
500
+ ): InkType[] {
501
+ return card.inkType;
502
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Classifications (Rule 6.2.6)
3
+ *
4
+ * Character classifications that appear on character cards.
5
+ * Characters can have multiple classifications.
6
+ */
7
+
8
+ export const CLASSIFICATIONS = [
9
+ "Alien",
10
+ "Ally",
11
+ "Broom",
12
+ "Captain",
13
+ "Deity",
14
+ "Detective",
15
+ "Dragon",
16
+ "Dreamborn",
17
+ "Entangled",
18
+ "Fairy",
19
+ "Floodborn",
20
+ "Gargoyle",
21
+ "Ghost",
22
+ "Hero",
23
+ "Hunny",
24
+ "Hyena",
25
+ "Illusion",
26
+ "Inventor",
27
+ "King",
28
+ "Knight",
29
+ "Madrigal",
30
+ "Mentor",
31
+ "Musketeer",
32
+ "Pirate",
33
+ "Prince",
34
+ "Princess",
35
+ "Puppy",
36
+ "Queen",
37
+ "Racer",
38
+ "Robot",
39
+ "Seven Dwarfs",
40
+ "Sorcerer",
41
+ "Storyborn",
42
+ "Tigger",
43
+ "Titan",
44
+ "Villain",
45
+ "Whisper",
46
+ ] as const;
47
+
48
+ export type Classification = (typeof CLASSIFICATIONS)[number];
49
+
50
+ /**
51
+ * Check if a value is a valid classification
52
+ */
53
+ export function isClassification(value: unknown): value is Classification {
54
+ return (
55
+ typeof value === "string" &&
56
+ CLASSIFICATIONS.includes(value as Classification)
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Check if a classification is Storyborn
62
+ */
63
+ export function isStoryborn(classification: Classification): boolean {
64
+ return classification === "Storyborn";
65
+ }
66
+
67
+ /**
68
+ * Check if a classification is Floodborn
69
+ */
70
+ export function isFloodborn(classification: Classification): boolean {
71
+ return classification === "Floodborn";
72
+ }
73
+
74
+ /**
75
+ * Check if a classification is Dreamborn
76
+ */
77
+ export function isDreamborn(classification: Classification): boolean {
78
+ return classification === "Dreamborn";
79
+ }
80
+
81
+ /**
82
+ * Get all classifications
83
+ */
84
+ export function getAllClassifications(): readonly Classification[] {
85
+ return CLASSIFICATIONS;
86
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Deck Validation Types (Rule 2.1)
3
+ *
4
+ * Deck 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 { InkType } from "./ink-types";
11
+
12
+ /** Minimum cards required in a deck */
13
+ export const MIN_DECK_SIZE = 60;
14
+
15
+ /** Maximum different ink types allowed in a deck */
16
+ export const MAX_INK_TYPES = 2;
17
+
18
+ /** Maximum copies of the same card (by full name) allowed */
19
+ export const MAX_COPIES_PER_CARD = 4;
20
+
21
+ /**
22
+ * Deck validation error types
23
+ */
24
+ export type DeckValidationError =
25
+ | TooFewCardsError
26
+ | TooManyInkTypesError
27
+ | TooManyCopiesError;
28
+
29
+ export interface TooFewCardsError {
30
+ type: "TOO_FEW_CARDS";
31
+ count: number;
32
+ minimum: typeof MIN_DECK_SIZE;
33
+ }
34
+
35
+ export interface TooManyInkTypesError {
36
+ type: "TOO_MANY_INK_TYPES";
37
+ inkTypes: InkType[];
38
+ maximum: typeof MAX_INK_TYPES;
39
+ }
40
+
41
+ export interface TooManyCopiesError {
42
+ type: "TOO_MANY_COPIES";
43
+ fullName: string;
44
+ count: number;
45
+ maximum: number;
46
+ }
47
+
48
+ /**
49
+ * Result of deck validation
50
+ */
51
+ export interface DeckValidationResult {
52
+ valid: boolean;
53
+ errors: DeckValidationError[];
54
+ }
55
+
56
+ /**
57
+ * Deck statistics for informational purposes
58
+ */
59
+ export interface DeckStats {
60
+ totalCards: number;
61
+ inkTypes: InkType[];
62
+ cardCounts: Map<string, number>;
63
+ cardTypeBreakdown: {
64
+ characters: number;
65
+ actions: number;
66
+ items: number;
67
+ locations: number;
68
+ };
69
+ inkableCards: number;
70
+ averageCost: number;
71
+ }