@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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable Move Validators
|
|
3
|
+
*
|
|
4
|
+
* Reusable validation functions for Lorcana moves.
|
|
5
|
+
* Each validator is a pure predicate that can be composed with others.
|
|
6
|
+
*
|
|
7
|
+
* Design principles:
|
|
8
|
+
* - Small, focused validators
|
|
9
|
+
* - Composable via &&, ||, !
|
|
10
|
+
* - Testable in isolation
|
|
11
|
+
* - Type-safe with proper context
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { CardId, MoveContext, PlayerId, ZoneId } from "@drmxrcy/tcg-core";
|
|
15
|
+
import type { LorcanaCardMeta, LorcanaGameState } from "../types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validator function type
|
|
19
|
+
*
|
|
20
|
+
* Matches the condition signature from GameMoveDefinition.
|
|
21
|
+
* Takes state and context, returns boolean indicating validity.
|
|
22
|
+
*/
|
|
23
|
+
export type Validator<TParams = unknown> = (
|
|
24
|
+
state: LorcanaGameState,
|
|
25
|
+
context: MoveContext<TParams, LorcanaCardMeta>,
|
|
26
|
+
) => boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parameterized validator type
|
|
30
|
+
*
|
|
31
|
+
* Takes arguments and returns a validator function.
|
|
32
|
+
* Allows validators to be configured per-move.
|
|
33
|
+
*/
|
|
34
|
+
export type ParameterizedValidator<
|
|
35
|
+
TArgs extends unknown[],
|
|
36
|
+
TParams = unknown,
|
|
37
|
+
> = (...args: TArgs) => Validator<TParams>;
|
|
38
|
+
|
|
39
|
+
// ===== Phase/Flow Validators =====
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate current phase
|
|
43
|
+
*
|
|
44
|
+
* @param expectedPhase - Required phase name
|
|
45
|
+
* @returns Validator that checks if current phase matches
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* condition: isPhase("main")
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const isPhase =
|
|
53
|
+
<TParams = unknown>(expectedPhase: string): Validator<TParams> =>
|
|
54
|
+
(_state, context) => {
|
|
55
|
+
return context.flow?.currentPhase === expectedPhase;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate it's the main phase specifically
|
|
60
|
+
*
|
|
61
|
+
* Most Lorcana actions require main phase.
|
|
62
|
+
*/
|
|
63
|
+
export const isMainPhase = <TParams = unknown>(): Validator<TParams> =>
|
|
64
|
+
isPhase<TParams>("main");
|
|
65
|
+
|
|
66
|
+
// ===== Card Location Validators =====
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate card is in a specific zone
|
|
70
|
+
*
|
|
71
|
+
* @param cardId - Card to check
|
|
72
|
+
* @param zoneId - Expected zone
|
|
73
|
+
* @returns Validator that checks card location
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* condition: (state, context) =>
|
|
78
|
+
* cardInZone(context.params.cardId, "hand")(context)
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export const cardInZone =
|
|
82
|
+
<TParams = unknown>(cardId: CardId, zoneId: ZoneId): Validator<TParams> =>
|
|
83
|
+
(_state, context) => {
|
|
84
|
+
return context.zones.getCardZone(cardId) === zoneId;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate card is in hand
|
|
89
|
+
*/
|
|
90
|
+
export const cardInHand = <TParams = unknown>(
|
|
91
|
+
cardId: CardId,
|
|
92
|
+
): Validator<TParams> => cardInZone(cardId, "hand" as ZoneId);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate card is in play
|
|
96
|
+
*/
|
|
97
|
+
export const cardInPlay = <TParams = unknown>(
|
|
98
|
+
cardId: CardId,
|
|
99
|
+
): Validator<TParams> => cardInZone(cardId, "play" as ZoneId);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate card exists in any zone
|
|
103
|
+
*
|
|
104
|
+
* @param cardId - Card to check
|
|
105
|
+
* @returns Validator that checks if card exists
|
|
106
|
+
*/
|
|
107
|
+
export const cardExists =
|
|
108
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
109
|
+
(_state, context) => {
|
|
110
|
+
return context.zones.getCardZone(cardId) !== undefined;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ===== Ownership Validators =====
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validate card is owned by the current player
|
|
117
|
+
*
|
|
118
|
+
* @param cardId - Card to check
|
|
119
|
+
* @returns Validator that checks ownership
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* condition: (state, context) =>
|
|
124
|
+
* cardOwnedByPlayer(context.params.cardId)(context)
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export const cardOwnedByPlayer =
|
|
128
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
129
|
+
(_state, context) => {
|
|
130
|
+
return context.cards.getCardOwner(cardId) === context.playerId;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate card is owned by a specific player
|
|
135
|
+
*/
|
|
136
|
+
export const cardOwnedBy =
|
|
137
|
+
<TParams = unknown>(cardId: CardId, playerId: PlayerId): Validator<TParams> =>
|
|
138
|
+
(_state, context) => {
|
|
139
|
+
return context.cards.getCardOwner(cardId) === playerId;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ===== Tracker Validators =====
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate action has not been used this turn
|
|
146
|
+
*
|
|
147
|
+
* @param actionName - Tracker name to check
|
|
148
|
+
* @param playerId - Optional player ID (defaults to current player)
|
|
149
|
+
* @returns Validator that checks tracker
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* condition: hasNotUsedAction("hasInked")
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export const hasNotUsedAction =
|
|
157
|
+
<TParams = unknown>(
|
|
158
|
+
actionName: string,
|
|
159
|
+
playerId?: PlayerId,
|
|
160
|
+
): Validator<TParams> =>
|
|
161
|
+
(_state, context) => {
|
|
162
|
+
const player = playerId ?? context.playerId;
|
|
163
|
+
return !context.trackers?.check(actionName, player);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate action has been used
|
|
168
|
+
*/
|
|
169
|
+
export const hasUsedAction =
|
|
170
|
+
<TParams = unknown>(
|
|
171
|
+
actionName: string,
|
|
172
|
+
playerId?: PlayerId,
|
|
173
|
+
): Validator<TParams> =>
|
|
174
|
+
(_state, context) => {
|
|
175
|
+
const player = playerId ?? context.playerId;
|
|
176
|
+
return context.trackers?.check(actionName, player) ?? false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ===== Card State Validators =====
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate card is not exerted (ready)
|
|
183
|
+
*
|
|
184
|
+
* @param cardId - Card to check
|
|
185
|
+
* @returns Validator that checks exerted status
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* condition: (state, context) =>
|
|
190
|
+
* cardIsReady(context.params.cardId)(context)
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export const cardIsReady =
|
|
194
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
195
|
+
(_state, context) => {
|
|
196
|
+
const meta = context.cards.getCardMeta(cardId);
|
|
197
|
+
return meta?.state === "ready";
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate card is exerted
|
|
202
|
+
*/
|
|
203
|
+
export const cardIsExerted =
|
|
204
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
205
|
+
(_state, context) => {
|
|
206
|
+
const meta = context.cards.getCardMeta(cardId);
|
|
207
|
+
return meta?.state === "exerted";
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate card is "dry" (not played this turn)
|
|
212
|
+
*
|
|
213
|
+
* Rule 4.2.2.1: Characters are "drying" the turn they're played
|
|
214
|
+
* Rule 6.1.4: Must be dry to quest, challenge, or exert
|
|
215
|
+
*
|
|
216
|
+
* @param cardId - Card to check
|
|
217
|
+
* @returns Validator that checks if card is dry
|
|
218
|
+
*/
|
|
219
|
+
export const cardIsDry =
|
|
220
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
221
|
+
(_state, context) => {
|
|
222
|
+
const meta = context.cards.getCardMeta(cardId);
|
|
223
|
+
return !meta?.isDrying;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Validate card was played this turn (is drying)
|
|
228
|
+
*/
|
|
229
|
+
export const cardIsDrying =
|
|
230
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
231
|
+
(_state, context) => {
|
|
232
|
+
const meta = context.cards.getCardMeta(cardId);
|
|
233
|
+
return meta?.isDrying ?? false;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// ===== Composite Validators =====
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validate card can quest
|
|
240
|
+
*
|
|
241
|
+
* Requirements (Rule 4.3.5, Rule 6.1.4):
|
|
242
|
+
* - Card is in play
|
|
243
|
+
* - Card is owned by current player
|
|
244
|
+
* - Card is ready (not exerted)
|
|
245
|
+
* - Card is dry (not played this turn)
|
|
246
|
+
* - Card hasn't quested this turn
|
|
247
|
+
*
|
|
248
|
+
* @param cardId - Card to check
|
|
249
|
+
* @returns Validator that checks all quest requirements
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```typescript
|
|
253
|
+
* condition: (state, context) =>
|
|
254
|
+
* canQuest(context.params.cardId)(context)
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export const canQuest =
|
|
258
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
259
|
+
(state, context) => {
|
|
260
|
+
return (
|
|
261
|
+
cardInPlay(cardId)(state, context) &&
|
|
262
|
+
cardOwnedByPlayer(cardId)(state, context) &&
|
|
263
|
+
cardIsReady(cardId)(state, context) &&
|
|
264
|
+
cardIsDry(cardId)(state, context) &&
|
|
265
|
+
hasNotUsedAction(`quested:${cardId}`)(state, context)
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Validate card can challenge
|
|
271
|
+
*
|
|
272
|
+
* Requirements (Rule 4.3.6, Rule 6.1.4):
|
|
273
|
+
* - Card is in play
|
|
274
|
+
* - Card is owned by current player
|
|
275
|
+
* - Card is ready (not exerted)
|
|
276
|
+
* - Card is dry (not played this turn)
|
|
277
|
+
*
|
|
278
|
+
* @param cardId - Card to check
|
|
279
|
+
* @returns Validator that checks all challenge requirements
|
|
280
|
+
*/
|
|
281
|
+
export const canChallenge =
|
|
282
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
283
|
+
(state, context) => {
|
|
284
|
+
return (
|
|
285
|
+
cardInPlay(cardId)(state, context) &&
|
|
286
|
+
cardOwnedByPlayer(cardId)(state, context) &&
|
|
287
|
+
cardIsReady(cardId)(state, context) &&
|
|
288
|
+
cardIsDry(cardId)(state, context)
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validate card can be used to pay costs (e.g., singing, abilities)
|
|
294
|
+
*
|
|
295
|
+
* Requirements (Rule 6.1.4):
|
|
296
|
+
* - Card is in play
|
|
297
|
+
* - Card is owned by current player
|
|
298
|
+
* - Card is ready (not exerted)
|
|
299
|
+
* - Card is dry (not played this turn)
|
|
300
|
+
*
|
|
301
|
+
* @param cardId - Card to check
|
|
302
|
+
* @returns Validator that checks if card can pay costs
|
|
303
|
+
*/
|
|
304
|
+
export const canPayCosts =
|
|
305
|
+
<TParams = unknown>(cardId: CardId): Validator<TParams> =>
|
|
306
|
+
(state, context) => {
|
|
307
|
+
return (
|
|
308
|
+
cardInPlay(cardId)(state, context) &&
|
|
309
|
+
cardOwnedByPlayer(cardId)(state, context) &&
|
|
310
|
+
cardIsReady(cardId)(state, context) &&
|
|
311
|
+
cardIsDry(cardId)(state, context)
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// ===== Logical Combinators =====
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Combine validators with AND logic
|
|
319
|
+
*
|
|
320
|
+
* @param validators - Validators to combine
|
|
321
|
+
* @returns Validator that checks all validators
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```typescript
|
|
325
|
+
* condition: and(
|
|
326
|
+
* isMainPhase(),
|
|
327
|
+
* (context) => cardInHand(context.params.cardId)(context),
|
|
328
|
+
* (context) => hasNotUsedAction("hasInked")()(context)
|
|
329
|
+
* )
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
export const and =
|
|
333
|
+
<TParams = unknown>(
|
|
334
|
+
...validators: Validator<TParams>[]
|
|
335
|
+
): Validator<TParams> =>
|
|
336
|
+
(state, context) => {
|
|
337
|
+
return validators.every((validator) => validator(state, context));
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Combine validators with OR logic
|
|
342
|
+
*/
|
|
343
|
+
export const or =
|
|
344
|
+
<TParams = unknown>(
|
|
345
|
+
...validators: Validator<TParams>[]
|
|
346
|
+
): Validator<TParams> =>
|
|
347
|
+
(state, context) => {
|
|
348
|
+
return validators.some((validator) => validator(state, context));
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Negate a validator
|
|
353
|
+
*/
|
|
354
|
+
export const not =
|
|
355
|
+
<TParams = unknown>(validator: Validator<TParams>): Validator<TParams> =>
|
|
356
|
+
(state, context) => {
|
|
357
|
+
return !validator(state, context);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Always returns true (no-op validator)
|
|
362
|
+
*/
|
|
363
|
+
export const always =
|
|
364
|
+
<TParams = unknown>(): Validator<TParams> =>
|
|
365
|
+
() =>
|
|
366
|
+
true;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Always returns false (deny-all validator)
|
|
370
|
+
*/
|
|
371
|
+
export const never =
|
|
372
|
+
<TParams = unknown>(): Validator<TParams> =>
|
|
373
|
+
() =>
|
|
374
|
+
false;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card State (Section 5.1)
|
|
3
|
+
*
|
|
4
|
+
* Card states in Lorcana:
|
|
5
|
+
* - Ready/Exerted (5.1.1-5.1.2)
|
|
6
|
+
* - Damaged/Undamaged (5.1.3-5.1.4)
|
|
7
|
+
* - Stack relationships (5.1.5-5.1.7) for Shift
|
|
8
|
+
* - Drying state (summoning sickness)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CardId } from "@drmxrcy/tcg-core";
|
|
12
|
+
import type {
|
|
13
|
+
CardReadyState,
|
|
14
|
+
LorcanaCardMeta,
|
|
15
|
+
StackPosition,
|
|
16
|
+
} from "../types/game-state";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runtime state for a card instance in play
|
|
20
|
+
* Combines the stored metadata with the instance ID for logical operations
|
|
21
|
+
*/
|
|
22
|
+
export type CardInstanceState = LorcanaCardMeta & {
|
|
23
|
+
/** Unique instance ID */
|
|
24
|
+
cardId: CardId;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create default card instance state for a newly played card
|
|
29
|
+
*/
|
|
30
|
+
export function createCardInstanceState(
|
|
31
|
+
cardId: CardId,
|
|
32
|
+
options?: {
|
|
33
|
+
isDrying?: boolean;
|
|
34
|
+
state?: CardReadyState;
|
|
35
|
+
},
|
|
36
|
+
): CardInstanceState {
|
|
37
|
+
return {
|
|
38
|
+
cardId,
|
|
39
|
+
state: options?.state ?? "ready",
|
|
40
|
+
damage: 0,
|
|
41
|
+
isDrying: options?.isDrying ?? true, // Characters start drying
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a card is ready
|
|
47
|
+
*/
|
|
48
|
+
export function isReady(cardState: CardInstanceState): boolean {
|
|
49
|
+
return cardState.state === "ready";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a card is exerted
|
|
54
|
+
*/
|
|
55
|
+
export function isExerted(cardState: CardInstanceState): boolean {
|
|
56
|
+
return cardState.state === "exerted";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a card is drying (has summoning sickness)
|
|
61
|
+
*/
|
|
62
|
+
export function isDrying(cardState: CardInstanceState): boolean {
|
|
63
|
+
return cardState.isDrying;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a card is dry (no summoning sickness)
|
|
68
|
+
*/
|
|
69
|
+
export function isDry(cardState: CardInstanceState): boolean {
|
|
70
|
+
return !cardState.isDrying;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if a card is damaged (has 1+ damage)
|
|
75
|
+
*/
|
|
76
|
+
export function isDamaged(cardState: CardInstanceState): boolean {
|
|
77
|
+
return cardState.damage > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get current damage on a card
|
|
82
|
+
*/
|
|
83
|
+
export function getDamage(cardState: CardInstanceState): number {
|
|
84
|
+
return cardState.damage;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if card is in a stack (shifted)
|
|
89
|
+
*/
|
|
90
|
+
export function isInStack(cardState: CardInstanceState): boolean {
|
|
91
|
+
return cardState.stackPosition !== undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if card is the top of a stack
|
|
96
|
+
*/
|
|
97
|
+
export function isTopOfStack(cardState: CardInstanceState): boolean {
|
|
98
|
+
const stack = cardState.stackPosition;
|
|
99
|
+
return stack !== undefined && !stack.isUnder;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if card is under another card in a stack
|
|
104
|
+
*/
|
|
105
|
+
export function isUnderCard(cardState: CardInstanceState): boolean {
|
|
106
|
+
const stack = cardState.stackPosition;
|
|
107
|
+
return stack !== undefined && stack.isUnder;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Exert a card (turn sideways)
|
|
112
|
+
*/
|
|
113
|
+
export function exertCard(cardState: CardInstanceState): CardInstanceState {
|
|
114
|
+
return { ...cardState, state: "exerted" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ready a card (turn upright)
|
|
119
|
+
*/
|
|
120
|
+
export function readyCard(cardState: CardInstanceState): CardInstanceState {
|
|
121
|
+
return { ...cardState, state: "ready" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add damage to a card
|
|
126
|
+
*/
|
|
127
|
+
export function addDamage(
|
|
128
|
+
cardState: CardInstanceState,
|
|
129
|
+
amount: number,
|
|
130
|
+
): CardInstanceState {
|
|
131
|
+
if (amount < 0) return cardState;
|
|
132
|
+
return { ...cardState, damage: cardState.damage + amount };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove damage from a card
|
|
137
|
+
*/
|
|
138
|
+
export function removeDamage(
|
|
139
|
+
cardState: CardInstanceState,
|
|
140
|
+
amount: number,
|
|
141
|
+
): CardInstanceState {
|
|
142
|
+
if (amount < 0) return cardState;
|
|
143
|
+
return { ...cardState, damage: Math.max(0, cardState.damage - amount) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Set damage to a specific value
|
|
148
|
+
*/
|
|
149
|
+
export function setDamage(
|
|
150
|
+
cardState: CardInstanceState,
|
|
151
|
+
damage: number,
|
|
152
|
+
): CardInstanceState {
|
|
153
|
+
return { ...cardState, damage: Math.max(0, damage) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Set drying state
|
|
158
|
+
*/
|
|
159
|
+
export function setDrying(
|
|
160
|
+
cardState: CardInstanceState,
|
|
161
|
+
isDrying: boolean,
|
|
162
|
+
): CardInstanceState {
|
|
163
|
+
return { ...cardState, isDrying };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear drying state (character becomes dry)
|
|
168
|
+
*/
|
|
169
|
+
export function clearDrying(cardState: CardInstanceState): CardInstanceState {
|
|
170
|
+
return { ...cardState, isDrying: false };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Set the location a character is at
|
|
175
|
+
*/
|
|
176
|
+
export function setAtLocation(
|
|
177
|
+
cardState: CardInstanceState,
|
|
178
|
+
locationId: CardId | undefined,
|
|
179
|
+
): CardInstanceState {
|
|
180
|
+
return { ...cardState, atLocationId: locationId };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a stack by shifting onto a target
|
|
185
|
+
* The new card goes on top, target goes underneath
|
|
186
|
+
*/
|
|
187
|
+
export function createStack(
|
|
188
|
+
topCardState: CardInstanceState,
|
|
189
|
+
underneathCardState: CardInstanceState,
|
|
190
|
+
): {
|
|
191
|
+
topCard: CardInstanceState;
|
|
192
|
+
underneathCard: CardInstanceState;
|
|
193
|
+
} {
|
|
194
|
+
const topCard: CardInstanceState = {
|
|
195
|
+
...topCardState,
|
|
196
|
+
// Damage carries over from underneath card (Rule 10.8.x)
|
|
197
|
+
damage: underneathCardState.damage,
|
|
198
|
+
// Shifted card is ready regardless of what's underneath
|
|
199
|
+
state: "ready",
|
|
200
|
+
isDrying: false, // Shifted cards are not drying
|
|
201
|
+
stackPosition: {
|
|
202
|
+
isUnder: false,
|
|
203
|
+
cardsUnderneath: [underneathCardState.cardId],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const underneathCard: CardInstanceState = {
|
|
208
|
+
...underneathCardState,
|
|
209
|
+
stackPosition: {
|
|
210
|
+
isUnder: true,
|
|
211
|
+
topCardId: topCardState.cardId,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return { topCard, underneathCard };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get all cards in a stack (including top card)
|
|
220
|
+
*/
|
|
221
|
+
export function getStackCardIds(cardState: CardInstanceState): CardId[] {
|
|
222
|
+
if (!cardState.stackPosition) {
|
|
223
|
+
return [cardState.cardId];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (cardState.stackPosition.isUnder) {
|
|
227
|
+
// This is an underneath card - return just this card
|
|
228
|
+
// The full stack should be queried from the top card
|
|
229
|
+
return [cardState.cardId];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// This is the top card
|
|
233
|
+
return [cardState.cardId, ...(cardState.stackPosition.cardsUnderneath ?? [])];
|
|
234
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zones Module
|
|
3
|
+
*
|
|
4
|
+
* Zone configuration and card state management for Lorcana.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Card state
|
|
8
|
+
export {
|
|
9
|
+
addDamage,
|
|
10
|
+
type CardInstanceState,
|
|
11
|
+
clearDrying,
|
|
12
|
+
createCardInstanceState,
|
|
13
|
+
createStack,
|
|
14
|
+
exertCard,
|
|
15
|
+
getDamage,
|
|
16
|
+
getStackCardIds,
|
|
17
|
+
isDamaged,
|
|
18
|
+
isDry,
|
|
19
|
+
isDrying,
|
|
20
|
+
isExerted,
|
|
21
|
+
isInStack,
|
|
22
|
+
isReady,
|
|
23
|
+
isTopOfStack,
|
|
24
|
+
isUnderCard,
|
|
25
|
+
readyCard,
|
|
26
|
+
removeDamage,
|
|
27
|
+
setAtLocation,
|
|
28
|
+
setDamage,
|
|
29
|
+
setDrying,
|
|
30
|
+
} from "./card-state";
|
|
31
|
+
// Zone configuration
|
|
32
|
+
export {
|
|
33
|
+
areCardsVisibleIn,
|
|
34
|
+
getZoneConfig,
|
|
35
|
+
isLorcanaZoneId,
|
|
36
|
+
isZoneVisibleTo,
|
|
37
|
+
LORCANA_ZONES,
|
|
38
|
+
type LorcanaZoneId,
|
|
39
|
+
ZONE_IDS,
|
|
40
|
+
type ZoneConfig,
|
|
41
|
+
type ZoneVisibility,
|
|
42
|
+
} from "./zone-config";
|