@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,216 @@
1
+ /**
2
+ * Movement Effect Types
3
+ *
4
+ * Effects that move cards between zones:
5
+ * - Return to hand
6
+ * - Put into inkwell
7
+ * - Shuffle into deck
8
+ * - Play cards from various zones
9
+ * - Move characters to locations
10
+ */
11
+
12
+ import type { CardType } from "../../cards/card-types";
13
+ import type {
14
+ CardTarget,
15
+ CharacterTarget,
16
+ ItemTarget,
17
+ LocationTarget,
18
+ PlayerTarget,
19
+ } from "../target-types";
20
+ import type { EffectDuration } from "./amount-types";
21
+
22
+ // ============================================================================
23
+ // Zone Movement Effects
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Return to hand effect
28
+ *
29
+ * @example "Return chosen character to their player's hand"
30
+ */
31
+ export interface ReturnToHandEffect {
32
+ type: "return-to-hand";
33
+ target?: CardTarget;
34
+ /** Filter for what can be returned */
35
+ filter?: {
36
+ cardType?: CardType | "song";
37
+ maxCost?: number;
38
+ classification?: string;
39
+ name?: string;
40
+ };
41
+ /** Amount of cards to return */
42
+ amount?: number;
43
+ }
44
+
45
+ /**
46
+ * Return from discard to hand
47
+ *
48
+ * @example "Return an action card from your discard to your hand"
49
+ */
50
+ export interface ReturnFromDiscardEffect {
51
+ type: "return-from-discard";
52
+ cardType?: CardType | "song";
53
+ cardName?: string;
54
+ target?: PlayerTarget;
55
+ count?: number;
56
+ destination?: "hand" | "play" | "top-of-deck";
57
+ }
58
+
59
+ /**
60
+ * Put into inkwell effect
61
+ *
62
+ * @example "Put the top card of your deck into your inkwell facedown and exerted"
63
+ */
64
+ export interface PutIntoInkwellEffect {
65
+ type: "put-into-inkwell";
66
+ source?:
67
+ | "top-of-deck"
68
+ | "hand"
69
+ | "chosen-card-in-play"
70
+ | "chosen-character"
71
+ | "this-card"
72
+ | "discard"
73
+ | "revealed"
74
+ | "deck"
75
+ | CardTarget;
76
+ target?: PlayerTarget | CharacterTarget | "SELF";
77
+ cardType?: CardType;
78
+ exerted?: boolean;
79
+ /** Whether the card is placed facedown in the inkwell */
80
+ facedown?: boolean;
81
+ /** Position in deck to take from */
82
+ position?: "top" | "bottom";
83
+ /** Who chooses the card */
84
+ chosenBy?: "you" | "opponent" | "TARGET";
85
+ }
86
+
87
+ /**
88
+ * Put card under another card (Boost mechanic)
89
+ */
90
+ export interface PutUnderEffect {
91
+ type: "put-under";
92
+ source: "top-of-deck" | "hand" | "discard";
93
+ under: CharacterTarget | LocationTarget | "self";
94
+ cardType?: CardType;
95
+ }
96
+
97
+ /**
98
+ * Shuffle into deck effect
99
+ */
100
+ export interface ShuffleIntoDeckEffect {
101
+ type: "shuffle-into-deck";
102
+ target?: CharacterTarget | ItemTarget | LocationTarget | CardTarget;
103
+ /** Whose deck to shuffle into */
104
+ intoDeck?: "owner" | "controller";
105
+ }
106
+
107
+ /**
108
+ * Put on bottom of deck
109
+ */
110
+ export interface PutOnBottomEffect {
111
+ type: "put-on-bottom";
112
+ target: CharacterTarget | ItemTarget | LocationTarget;
113
+ }
114
+
115
+ // ============================================================================
116
+ // Play Card Effects
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Play a card effect
121
+ *
122
+ * @example "Play a character with cost 3 or less for free"
123
+ * @example "Play a character from your discard for free"
124
+ */
125
+ export interface PlayCardEffect {
126
+ type: "play-card";
127
+ from?: "hand" | "discard" | "deck" | "under-self";
128
+ cardType?: CardType | "song" | "floodborn";
129
+ costRestriction?: { comparison: "less-or-equal" | "equal"; value: number };
130
+ cost?: "free" | "reduced";
131
+ reducedBy?: number;
132
+ /** Character enters play exerted */
133
+ entersExerted?: boolean;
134
+ /** Grants Rush for this turn */
135
+ grantsRush?: boolean;
136
+ /** Banish at end of turn */
137
+ banishAtEndOfTurn?: boolean;
138
+ /** Filter for what can be played */
139
+ filter?: {
140
+ cardType?: CardType | "song" | "floodborn";
141
+ maxCost?: number;
142
+ classification?: string;
143
+ name?: string;
144
+ };
145
+ /** Whether to play for free (alias for cost: "free") */
146
+ free?: boolean;
147
+ /** Target for the play effect */
148
+ target?: string;
149
+ }
150
+
151
+ /**
152
+ * Enable playing from under a card
153
+ */
154
+ export interface EnablePlayFromUnderEffect {
155
+ type: "enable-play-from-under";
156
+ cardType?: CardType | "song" | "floodborn";
157
+ duration?: EffectDuration;
158
+ }
159
+
160
+ // ============================================================================
161
+ // Location Movement Effects
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Move character to location
166
+ *
167
+ * @example "Move a character of yours to a location for free"
168
+ */
169
+ export interface MoveToLocationEffect {
170
+ type: "move-to-location";
171
+ character: CharacterTarget;
172
+ location?: LocationTarget;
173
+ cost?: "free" | "normal";
174
+ }
175
+
176
+ /**
177
+ * Move cost reduction effect for locations
178
+ *
179
+ * @example "Your characters named Robin Hood may move here for free"
180
+ * @example "Your Pirate characters may move here for free"
181
+ */
182
+ export interface MoveCostReductionEffect {
183
+ type: "move-cost-reduction";
184
+ /** Filter for which characters get the reduction */
185
+ filter?: {
186
+ name?: string;
187
+ classification?: string;
188
+ };
189
+ /** How much to reduce the cost (0 = free) */
190
+ reduction: number | "free";
191
+ /** Target location (usually "here" for location abilities) */
192
+ location?: "here" | LocationTarget;
193
+ }
194
+
195
+ /**
196
+ * Grant abilities to characters while at this location
197
+ *
198
+ * @example "Characters gain Ward while here"
199
+ * @example "Characters gain Ward and activated ability while here"
200
+ */
201
+ export interface GrantAbilitiesWhileHereEffect {
202
+ type: "grant-abilities-while-here";
203
+ abilities: Array<
204
+ | { type: "keyword"; keyword: string; value?: number }
205
+ | {
206
+ type: "activated";
207
+ cost: { exert?: boolean; ink?: number };
208
+ effect: {
209
+ type: string;
210
+ amount?: number;
211
+ target?: unknown;
212
+ [key: string]: unknown;
213
+ };
214
+ }
215
+ >;
216
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Scry Effect Types
3
+ *
4
+ * Defines the "look at top X cards" effect system with declarative destinations.
5
+ * Used for effects like "Look at the top 4 cards of your deck..."
6
+ */
7
+
8
+ import type { CardType } from "../../cards/card-types";
9
+ import type { PlayerTarget } from "../target-types";
10
+ import type { Amount, VariableAmount } from "./amount-types";
11
+
12
+ // ============================================================================
13
+ // Scry Effect
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Scry effect - Look at top cards and distribute to destinations
18
+ *
19
+ * Replaces the old LookAtCardsEffect with a declarative destinations array.
20
+ *
21
+ * Design principles:
22
+ * 1. Declarative: Describes WHAT happens, not HOW
23
+ * 2. Composable: Multiple independent selections from same pool
24
+ * 3. Parser-friendly: Can be constructed from natural language
25
+ *
26
+ * @example "Look at top 4, reveal a character, put into hand, rest on bottom"
27
+ * @example "Look at top 5, reveal up to 1 Madrigal AND up to 1 song, put in hand"
28
+ */
29
+ export interface ScryEffect {
30
+ type: "scry";
31
+ /** Number of cards to look at from top of deck */
32
+ amount?: Amount;
33
+ /** Whose deck to look at (default: CONTROLLER) */
34
+ target?: PlayerTarget;
35
+ /** Destinations for the looked-at cards, processed in order */
36
+ destinations?: ScryDestination[];
37
+ /** Whether all looked-at cards should be revealed to opponent */
38
+ revealAll?: boolean;
39
+ }
40
+
41
+ // ============================================================================
42
+ // Scry Destinations (Discriminated Union)
43
+ // ============================================================================
44
+
45
+ /**
46
+ * A destination describes where cards can go and constraints on the selection.
47
+ * Uses discriminated union on 'zone' property for type safety.
48
+ */
49
+ export type ScryDestination =
50
+ | ScryHandDestination
51
+ | ScryDeckTopDestination
52
+ | ScryDeckBottomDestination
53
+ | ScryInkwellDestination
54
+ | ScryPlayDestination
55
+ | ScryDiscardDestination;
56
+
57
+ /**
58
+ * Base properties shared by all scry destinations
59
+ */
60
+ interface ScryBaseDestination {
61
+ /** Minimum cards that MUST go to this destination (default: 0) */
62
+ min?: number;
63
+ /** Maximum cards that CAN go to this destination */
64
+ max?: number;
65
+ /** Filter for which cards can go to this destination */
66
+ filter?: ScryCardFilter | ScryCardFilter[];
67
+ /** Whether cards going here must be revealed to opponent */
68
+ reveal?: boolean;
69
+ /** Mark as remainder destination - receives all unselected cards */
70
+ remainder?: boolean;
71
+ /** Destinations in the same exclusiveGroup are mutually exclusive */
72
+ exclusiveGroup?: string;
73
+ /** Label for UI display (e.g., "up to 1 Madrigal character") */
74
+ label?: string;
75
+ }
76
+
77
+ /** Put cards into hand */
78
+ export interface ScryHandDestination extends ScryBaseDestination {
79
+ zone: "hand";
80
+ }
81
+
82
+ /** Put cards on top of deck */
83
+ export interface ScryDeckTopDestination extends ScryBaseDestination {
84
+ zone: "deck-top";
85
+ /** How cards are ordered when placed on top */
86
+ ordering?: ScryCardOrdering;
87
+ }
88
+
89
+ /** Put cards on bottom of deck */
90
+ export interface ScryDeckBottomDestination extends ScryBaseDestination {
91
+ zone: "deck-bottom";
92
+ /** How cards are ordered when placed on bottom */
93
+ ordering?: ScryCardOrdering;
94
+ }
95
+
96
+ /** Put cards into inkwell */
97
+ export interface ScryInkwellDestination extends ScryBaseDestination {
98
+ zone: "inkwell";
99
+ /** Whether ink enters exerted (default: true) */
100
+ exerted?: boolean;
101
+ /** Whether card is facedown (default: true) */
102
+ facedown?: boolean;
103
+ }
104
+
105
+ /** Put cards into play (for "play for free" effects) */
106
+ export interface ScryPlayDestination extends ScryBaseDestination {
107
+ zone: "play";
108
+ /** Cost modification when playing */
109
+ cost?: "free" | "reduced";
110
+ /** Amount to reduce cost by (when cost is "reduced") */
111
+ reducedBy?: number;
112
+ /** Additional filter for cost restriction (beyond selection filter) */
113
+ playFilter?: ScryCardFilter | ScryCardFilter[];
114
+ /** Character enters play exerted */
115
+ entersExerted?: boolean;
116
+ /** Grants Rush for this turn */
117
+ grantsRush?: boolean;
118
+ /** Banish at end of turn */
119
+ banishAtEndOfTurn?: boolean;
120
+ }
121
+
122
+ /** Put cards into discard */
123
+ export interface ScryDiscardDestination extends ScryBaseDestination {
124
+ zone: "discard";
125
+ }
126
+
127
+ /** How cards are ordered when placed on deck */
128
+ export type ScryCardOrdering =
129
+ | "player-choice" // Player decides order
130
+ | "original-order" // Keep original order from look
131
+ | "random"; // Shuffle before placing
132
+
133
+ // ============================================================================
134
+ // Scry Card Filters
135
+ // ============================================================================
136
+
137
+ /** Filters for selecting cards during scry */
138
+ export type ScryCardFilter =
139
+ // Card Type Filters
140
+ | ScryCardTypeFilter
141
+ | ScrySongFilter
142
+ | ScryFloodbornFilter
143
+ // Property Filters
144
+ | ScryClassificationFilter
145
+ | ScryNameFilter
146
+ | ScryKeywordFilter
147
+ // Cost Filters
148
+ | ScryCostComparisonFilter
149
+ // Composite Filters
150
+ | ScryAndFilter
151
+ | ScryOrFilter
152
+ | ScryNotFilter;
153
+
154
+ /** Filter by card type (character, item, action, location) */
155
+ export interface ScryCardTypeFilter {
156
+ type: "card-type";
157
+ cardType: CardType;
158
+ }
159
+
160
+ /** Filter for songs specifically (action with song subtype) */
161
+ export interface ScrySongFilter {
162
+ type: "song";
163
+ }
164
+
165
+ /** Filter for floodborn characters */
166
+ export interface ScryFloodbornFilter {
167
+ type: "floodborn";
168
+ }
169
+
170
+ /** Filter by classification (e.g., "Madrigal", "Puppy", "Princess") */
171
+ export interface ScryClassificationFilter {
172
+ type: "classification";
173
+ classification: string;
174
+ }
175
+
176
+ /** Filter by card name */
177
+ export interface ScryNameFilter {
178
+ type: "name";
179
+ name: string;
180
+ /** Match mode: exact or contains */
181
+ match?: "exact" | "contains";
182
+ }
183
+
184
+ /** Filter by keyword (e.g., "Bodyguard", "Evasive") */
185
+ export interface ScryKeywordFilter {
186
+ type: "keyword";
187
+ keyword: string;
188
+ }
189
+
190
+ /** Filter by cost comparison */
191
+ export interface ScryCostComparisonFilter {
192
+ type: "cost-comparison";
193
+ comparison:
194
+ | "less-or-equal"
195
+ | "equal"
196
+ | "greater-or-equal"
197
+ | "less"
198
+ | "greater";
199
+ value: number | VariableAmount;
200
+ }
201
+
202
+ /** AND filter - all sub-filters must match */
203
+ export interface ScryAndFilter {
204
+ type: "and";
205
+ filters: ScryCardFilter[];
206
+ }
207
+
208
+ /** OR filter - any sub-filter can match */
209
+ export interface ScryOrFilter {
210
+ type: "or";
211
+ filters: ScryCardFilter[];
212
+ }
213
+
214
+ /** NOT filter - inverts the sub-filter */
215
+ export interface ScryNotFilter {
216
+ type: "not";
217
+ filter: ScryCardFilter;
218
+ }
219
+
220
+ // ============================================================================
221
+ // Scry Type Guards
222
+ // ============================================================================
223
+
224
+ /** Check if destination is a hand destination */
225
+ export function isScryHandDestination(
226
+ dest: ScryDestination,
227
+ ): dest is ScryHandDestination {
228
+ return dest.zone === "hand";
229
+ }
230
+
231
+ /** Check if destination is a deck-top destination */
232
+ export function isScryDeckTopDestination(
233
+ dest: ScryDestination,
234
+ ): dest is ScryDeckTopDestination {
235
+ return dest.zone === "deck-top";
236
+ }
237
+
238
+ /** Check if destination is a deck-bottom destination */
239
+ export function isScryDeckBottomDestination(
240
+ dest: ScryDestination,
241
+ ): dest is ScryDeckBottomDestination {
242
+ return dest.zone === "deck-bottom";
243
+ }
244
+
245
+ /** Check if destination is an inkwell destination */
246
+ export function isScryInkwellDestination(
247
+ dest: ScryDestination,
248
+ ): dest is ScryInkwellDestination {
249
+ return dest.zone === "inkwell";
250
+ }
251
+
252
+ /** Check if destination is a play destination */
253
+ export function isScryPlayDestination(
254
+ dest: ScryDestination,
255
+ ): dest is ScryPlayDestination {
256
+ return dest.zone === "play";
257
+ }
258
+
259
+ /** Check if destination is a discard destination */
260
+ export function isScryDiscardDestination(
261
+ dest: ScryDestination,
262
+ ): dest is ScryDiscardDestination {
263
+ return dest.zone === "discard";
264
+ }
265
+
266
+ /** Check if destination is a remainder destination */
267
+ export function isScryRemainderDestination(dest: ScryDestination): boolean {
268
+ return dest.remainder === true;
269
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Ability Helpers for Lorcana Abilities
3
+ *
4
+ * Provides a fluent API for building ability definitions.
5
+ * These helpers make it easy to construct all ability types.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const ability = Abilities.Keyword("Rush");
10
+ * const ability = Abilities.Triggered({
11
+ * trigger: Triggers.WhenYouPlay(),
12
+ * effect: Effects.Draw({ amount: 2 })
13
+ * });
14
+ * const ability = Abilities.Action(Effects.Draw({ amount: 2 }));
15
+ * ```
16
+ */
17
+
18
+ import type {
19
+ ActionAbility,
20
+ ActivatedAbility,
21
+ KeywordAbility,
22
+ ParameterizedKeywordType,
23
+ ShiftKeywordAbility,
24
+ SimpleKeywordType,
25
+ StaticAbility,
26
+ TriggeredAbility,
27
+ ValueKeywordType,
28
+ } from "../ability-types";
29
+ import type { Effect, StaticEffect } from "../effect-types";
30
+ import type { AbilityCost, Condition } from "../index";
31
+ import type { Trigger } from "../trigger-types";
32
+
33
+ export const Abilities = {
34
+ /**
35
+ * Simple keyword ability (no parameters)
36
+ *
37
+ * @example Abilities.Keyword("Rush")
38
+ * @example Abilities.Keyword("Ward")
39
+ */
40
+ Keyword: (keyword: SimpleKeywordType): KeywordAbility => ({
41
+ type: "keyword",
42
+ keyword,
43
+ }),
44
+
45
+ /**
46
+ * Value-based keyword ability (Singer, SingTogether, Boost)
47
+ *
48
+ * @example Abilities.Keyword("Singer", { value: 5 })
49
+ */
50
+ KeywordWithValue: (
51
+ keyword: ValueKeywordType,
52
+ params: { value: number },
53
+ ): KeywordAbility => ({
54
+ type: "keyword",
55
+ keyword,
56
+ value: params.value,
57
+ }),
58
+
59
+ /**
60
+ * Parameterized keyword ability (Challenger, Resist)
61
+ *
62
+ * @example Abilities.Keyword("Challenger", { value: 3 })
63
+ * @example Abilities.Keyword("Resist", { value: 2, condition: Conditions.WhileDamaged() })
64
+ */
65
+ KeywordParameterized: (
66
+ keyword: ParameterizedKeywordType,
67
+ params: { value: number; condition?: Condition },
68
+ ): KeywordAbility => ({
69
+ type: "keyword",
70
+ keyword,
71
+ value: params.value,
72
+ ...(params.condition && { condition: params.condition }),
73
+ }),
74
+
75
+ /**
76
+ * Shift keyword ability
77
+ *
78
+ * @example Abilities.Shift({ cost: { ink: 5 } })
79
+ * @example Abilities.Shift({ cost: { ink: 3 }, shiftTarget: "Elsa" })
80
+ */
81
+ Shift: (params: {
82
+ cost: AbilityCost;
83
+ shiftTarget?: string;
84
+ }): ShiftKeywordAbility => ({
85
+ type: "keyword",
86
+ keyword: "Shift",
87
+ cost: params.cost,
88
+ ...(params.shiftTarget && { shiftTarget: params.shiftTarget }),
89
+ }),
90
+
91
+ /**
92
+ * Triggered ability - fires when conditions are met
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * Abilities.Triggered({
97
+ * trigger: Triggers.WhenYouPlay(),
98
+ * effect: Effects.Draw({ amount: 2 })
99
+ * })
100
+ * ```
101
+ */
102
+ Triggered: (params: {
103
+ name?: string;
104
+ trigger: Trigger;
105
+ effect: Effect;
106
+ condition?: Condition;
107
+ }): TriggeredAbility => ({
108
+ type: "triggered",
109
+ ...(params.name && { name: params.name }),
110
+ trigger: params.trigger,
111
+ effect: params.effect,
112
+ ...(params.condition && { condition: params.condition }),
113
+ }),
114
+
115
+ /**
116
+ * Activated ability - player chooses to use by paying cost
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * Abilities.Activated({
121
+ * cost: Costs.Ink(2),
122
+ * effect: Effects.Draw({ amount: 1 })
123
+ * })
124
+ * ```
125
+ */
126
+ Activated: (params: {
127
+ name?: string;
128
+ cost: AbilityCost;
129
+ effect: Effect;
130
+ condition?: Condition;
131
+ }): ActivatedAbility => ({
132
+ type: "activated",
133
+ ...(params.name && { name: params.name }),
134
+ cost: params.cost,
135
+ effect: params.effect,
136
+ ...(params.condition && { condition: params.condition }),
137
+ }),
138
+
139
+ /**
140
+ * Static ability - always active effect
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * Abilities.Static({
145
+ * effect: Effects.GainKeyword({ keyword: "Ward" })
146
+ * })
147
+ * ```
148
+ */
149
+ Static: (params: {
150
+ name?: string;
151
+ effect: StaticEffect;
152
+ condition?: Condition;
153
+ }): StaticAbility => ({
154
+ type: "static",
155
+ ...(params.name && { name: params.name }),
156
+ effect: params.effect,
157
+ ...(params.condition && { condition: params.condition }),
158
+ }),
159
+
160
+ /**
161
+ * Action ability - standalone effect on action cards
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * Abilities.Action(Effects.Draw({ amount: 2 }))
166
+ * ```
167
+ */
168
+ Action: (effect: Effect): ActionAbility => ({
169
+ type: "action",
170
+ effect,
171
+ }),
172
+ };