1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4use ts_rs::TS;
5
6use crate::{prompts::common::TargetRef, TokenScript};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, TS)]
9#[ts(export, export_to = "game/index.ts")]
10pub enum ManaColor {
11 #[serde(rename = "W")]
12 White,
13 #[serde(rename = "U")]
14 Blue,
15 #[serde(rename = "B")]
16 Black,
17 #[serde(rename = "R")]
18 Red,
19 #[serde(rename = "G")]
20 Green,
21 #[serde(rename = "C")]
22 Colorless,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
26#[serde(
27 tag = "kind",
28 rename_all = "camelCase",
29 rename_all_fields = "camelCase"
30)]
31#[ts(export, export_to = "game/index.ts")]
32pub enum CardChoiceDto {
33 Color { colors: Vec<ManaColor> },
34 Type { values: Vec<String> },
35 NamedCard { names: Vec<String> },
36 ChosenCard { count: usize },
37 Number { value: i32 },
38 Mode { value: String },
39 Player { player_id: String, name: String },
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
43#[serde(rename_all = "camelCase")]
44#[ts(export, export_to = "game/index.ts")]
45pub struct Mana {
46 pub color: ManaColor,
47 pub amount: i32,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, TS)]
51#[serde(rename_all = "camelCase")]
52#[ts(export, export_to = "game/index.ts")]
53pub enum PlayerCounterKind {
54 Poison,
55 Energy,
56 Experience,
57 Radiation,
58 Ticket,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
62#[serde(rename_all = "camelCase")]
63#[ts(export, export_to = "game/index.ts")]
64pub enum ZoneKind {
65 Battlefield,
66 Hand,
67 Library,
68 Graveyard,
69 Exile,
70 Command,
71 Attractions,
72 Junkyard,
73 Sideboard,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
77#[serde(rename_all = "camelCase")]
78#[ts(export, export_to = "game/index.ts")]
79pub enum StepKind {
80 #[default]
81 Untap,
82 Upkeep,
83 Draw,
84 Main1,
85 CombatBegin,
86 CombatDeclareAttackers,
87 CombatDeclareBlockers,
88 CombatFirstStrikeDamage,
89 CombatDamage,
90 CombatEnd,
91 Main2,
92 EndOfTurn,
93 Cleanup,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
97#[serde(rename_all = "camelCase")]
98#[ts(export, export_to = "game/index.ts")]
99pub enum DayTime {
100 #[default]
101 Neither,
102 Day,
103 Night,
104}
105
106#[allow(clippy::large_enum_variant)]
107#[derive(Debug, Clone, Serialize, Deserialize, TS)]
108#[serde(
109 tag = "visibility",
110 rename_all = "camelCase",
111 rename_all_fields = "camelCase"
112)]
113#[ts(export, export_to = "game/index.ts")]
114pub enum CardView {
115 Visible(CardDto),
116 Hidden { id: String },
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, TS)]
121#[serde(rename_all = "camelCase")]
122#[ts(export, export_to = "game/index.ts")]
123pub struct ZoneDto {
124 pub zone: ZoneKind,
125 pub owner_id: String,
126 pub cards: Vec<CardView>,
129 pub count: usize,
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
133#[serde(rename_all = "camelCase")]
134#[ts(export, export_to = "game/index.ts")]
135pub struct GameViewDto {
136 pub game_id: String,
137 pub turn: u32,
138 pub step: StepKind,
139 pub combat_assignments: Vec<CombatAssignmentDto>,
140 pub active_player_id: String,
141 pub priority_player_id: String,
142 pub players: Vec<PlayerDto>,
143 pub zones: Vec<ZoneDto>,
144 pub stack: Vec<StackObjectDto>,
145 pub game_over: bool,
146 pub winner_id: Option<String>,
147 pub monarch_id: Option<String>,
148 pub initiative_holder_id: Option<String>,
149 pub day_time: DayTime,
150 #[ts(optional)]
151 pub active_plane_names: Option<Vec<String>>,
152 #[serde(default)]
153 pub checkpoints: Vec<CheckpointDto>,
154 #[serde(default)]
155 pub restore_vote: Option<RestoreVoteDto>,
156 #[serde(default)]
157 pub snapshot_recording: bool,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, TS)]
161#[serde(rename_all = "camelCase")]
162#[ts(export, export_to = "game/index.ts")]
163pub struct RestoreVoteDto {
164 pub vote_id: u32,
165 pub checkpoint: CheckpointDto,
166 pub requested_by_player_id: String,
167 pub awaiting_player_ids: Vec<String>,
168 pub status: RestoreVoteStatus,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, TS)]
172#[serde(
173 tag = "type",
174 rename_all = "camelCase",
175 rename_all_fields = "camelCase"
176)]
177#[ts(export, export_to = "game/index.ts")]
178pub enum RestoreVoteStatus {
179 Pending,
180 Approved,
181 Declined { player_id: String },
182 Unavailable,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, TS)]
186#[serde(rename_all = "camelCase")]
187#[ts(export, export_to = "game/index.ts")]
188pub struct CheckpointDto {
189 pub checkpoint_id: u32,
190 pub turn: u32,
191 pub step: StepKind,
192 pub active_player_id: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, TS)]
196#[serde(rename_all = "camelCase")]
197#[ts(export, export_to = "game/index.ts")]
198pub struct CombatAssignmentDto {
199 pub blocker_id: String,
200 pub attacker_id: String,
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
204#[serde(rename_all = "camelCase")]
205#[ts(export, export_to = "game/index.ts")]
206pub enum PlayerStatus {
207 #[default]
208 Playing,
209 Lost,
210 Conceded,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, TS)]
214#[serde(rename_all = "camelCase")]
215#[ts(export, export_to = "game/index.ts")]
216pub struct DungeonStateDto {
217 pub name: String,
218 pub room: String,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, TS)]
222#[serde(rename_all = "camelCase")]
223#[ts(export, export_to = "game/index.ts")]
224pub struct PlayerDto {
225 pub id: String,
226 pub name: String,
227 pub status: PlayerStatus,
228 pub is_human: bool,
229 pub life: i32,
230 #[serde(default)]
231 pub max_hand_size: i32,
232 #[serde(default)]
233 pub unlimited_hand_size: bool,
234 #[serde(default)]
235 pub lands_played_this_turn: i32,
236 #[serde(default)]
237 pub max_land_plays_per_turn: i32,
238 #[serde(default)]
239 pub unlimited_land_plays: bool,
240 #[serde(default)]
241 pub cards_drawn_this_turn: i32,
242 #[serde(default)]
243 pub damage_prevention: i32,
244 #[serde(default)]
245 pub is_extra_turn: bool,
246 #[serde(default)]
247 pub extra_turn_count: u32,
248 #[ts(optional)]
249 pub controlled_by: Option<String>,
250 #[serde(default)]
251 pub player_keywords: Vec<String>,
252 #[serde(default)]
253 #[ts(type = "Record<string, number>")]
254 pub commander_casts: HashMap<String, u32>,
255 #[ts(optional)]
256 pub dungeon_state: Option<DungeonStateDto>,
257 #[ts(optional)]
258 pub active_scheme_names: Option<Vec<String>>,
259 #[ts(optional)]
260 pub team_number: Option<i32>,
261 pub counters: BTreeMap<PlayerCounterKind, u32>,
262 pub mana_pool: BTreeMap<ManaColor, u32>,
263 #[ts(type = "Record<string, number>")]
264 pub commander_damage: HashMap<String, i32>,
265 pub has_city_blessing: bool,
266 pub has_enduring_story: bool,
267 pub ring_level: i32,
268 pub speed: i32,
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
272#[serde(rename_all = "camelCase", default)]
273#[ts(export, export_to = "game/index.ts")]
274pub struct CardIdentity {
275 pub name: String,
276 pub set_code: String,
277 pub card_number: String,
278 pub is_token: bool,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 #[ts(optional)]
281 pub token_script: Option<TokenScript>,
282}
283
284#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
285#[serde(rename_all = "camelCase", default)]
286#[ts(export, export_to = "game/index.ts")]
287pub struct ClassLevelDto {
288 pub level: i32,
289 pub oracle: String,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 #[ts(optional)]
292 pub cost: Option<String>,
293}
294
295#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
296#[serde(rename_all = "camelCase", default)]
297#[ts(export, export_to = "game/index.ts")]
298pub struct SagaChapterDto {
299 pub chapters: Vec<i32>,
300 pub oracle: String,
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
304#[serde(rename_all = "camelCase", default)]
305#[ts(export, export_to = "game/index.ts")]
306pub struct CardDto {
307 pub id: String,
308 pub identity: CardIdentity,
309 #[serde(
310 default,
311 deserialize_with = "crate::deck_dto::CardCosmetics::deserialize_optional",
312 skip_serializing_if = "Option::is_none"
313 )]
314 #[ts(optional)]
315 pub cosmetics: Option<crate::deck_dto::CardCosmetics>,
316 pub color: String,
317 pub mana_cost: String,
318 pub cmc: i32,
319 pub types: Vec<String>,
320 pub subtypes: Vec<String>,
321 pub supertypes: Vec<String>,
322 pub power: Option<String>,
323 pub toughness: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 #[ts(optional)]
326 pub base_power: Option<i32>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 #[ts(optional)]
329 pub base_toughness: Option<i32>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 #[ts(optional)]
332 pub final_chapter: Option<i32>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 #[ts(optional)]
335 pub class_level: Option<i32>,
336 pub class_levels: Vec<ClassLevelDto>,
337 pub saga_chapters: Vec<SagaChapterDto>,
338 pub text: String,
339 #[serde(default)]
340 pub choices: Vec<CardChoiceDto>,
341 pub controller_id: String,
342 pub owner_id: String,
343 pub tapped: bool,
344 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
345 pub is_crewed: bool,
346 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
347 pub is_attacking: bool,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 #[ts(optional)]
350 pub attacking_player_id: Option<String>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 #[ts(optional)]
353 pub attack_target_id: Option<String>,
354 pub keywords: Vec<String>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 #[ts(optional)]
357 pub keyword_labels: Option<BTreeMap<String, String>>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub granted_keywords: Vec<String>,
360 #[ts(type = "Record<string, number>")]
363 pub counters: BTreeMap<String, u32>,
364 pub damage: i32,
365 pub summoning_sick: bool,
366 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
367 pub is_copy: bool,
368 pub is_double_faced: bool,
369 pub is_transformed: bool,
370 pub is_face_down: bool,
371 pub is_bestowed: bool,
372 pub phased_out: bool,
373 pub exerted: bool,
374 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
375 pub is_ring_bearer: bool,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 #[ts(optional)]
378 pub attached_to: Option<String>,
379 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub attachment_ids: Vec<String>,
381 #[serde(default, skip_serializing_if = "Vec::is_empty")]
383 pub merged_card_ids: Vec<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 #[ts(optional)]
386 pub flashback_cost: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 #[ts(optional)]
389 pub kicker_cost: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 #[ts(optional)]
392 pub effective_mana_cost: Option<String>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 #[ts(optional)]
395 pub commander_tax: Option<i32>,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 #[ts(optional)]
398 pub madness_cost: Option<String>,
399 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
400 pub is_madness_exiled: bool,
401 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
402 pub is_plotted: bool,
403 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
404 pub is_warp_exiled: bool,
405 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
406 pub foil: bool,
407 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
408 pub would_die_in_combat: bool,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
410 #[ts(optional)]
411 pub zone: Option<ZoneKind>,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
415#[serde(rename_all = "camelCase", default)]
416#[ts(export, export_to = "game/index.ts")]
417pub struct StackObjectDto {
418 pub id: String,
419 pub source_id: String,
420 pub controller_id: String,
421 pub owner_id: String,
422 pub identity: CardIdentity,
423 #[serde(default)]
424 pub foil: bool,
425 #[serde(
426 default,
427 deserialize_with = "crate::deck_dto::CardCosmetics::deserialize_optional",
428 skip_serializing_if = "Option::is_none"
429 )]
430 #[ts(optional)]
431 pub cosmetics: Option<crate::deck_dto::CardCosmetics>,
432 pub text: String,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 #[ts(optional)]
435 pub source_ability_text: Option<String>,
436 pub is_permanent_spell: bool,
437 pub is_casting: bool,
438 pub is_double_faced: bool,
439 pub face_index: u8,
440 pub targets: Vec<TargetRef>,
441}
442
443#[cfg(test)]
444mod stack_object_tests {
445 use super::{CardIdentity, StackObjectDto};
446
447 #[test]
448 fn serializes_card_face_fields() {
449 let stack_object = StackObjectDto {
450 id: "stack-1".into(),
451 source_id: "card-1".into(),
452 controller_id: "player-0".into(),
453 owner_id: "player-1".into(),
454 identity: CardIdentity::default(),
455 foil: false,
456 cosmetics: None,
457 text: String::new(),
458 source_ability_text: Some("When this enters, draw a card.".into()),
459 is_permanent_spell: true,
460 is_casting: true,
461 is_double_faced: true,
462 face_index: 1,
463 targets: Vec::new(),
464 };
465
466 let value = serde_json::to_value(stack_object).unwrap();
467 assert_eq!(value["sourceAbilityText"], "When this enters, draw a card.");
468
469 assert_eq!(value["ownerId"], "player-1");
470 assert_eq!(value["isDoubleFaced"], true);
471 assert_eq!(value["faceIndex"], 1);
472 }
473
474 #[test]
475 fn defaults_missing_card_face_fields() {
476 let value = serde_json::json!({
477 "id": "stack-1",
478 "sourceId": "card-1",
479 "controllerId": "player-0"
480 });
481
482 let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
483
484 assert!(stack_object.owner_id.is_empty());
485 assert!(!stack_object.is_double_faced);
486 assert_eq!(stack_object.face_index, 0);
487 }
488}
489
490#[derive(
491 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
492)]
493#[serde(rename_all = "camelCase")]
494#[ts(export, export_to = "game/index.ts")]
495pub enum TargetingIntent {
496 #[default]
497 Damage,
498 Destroy,
499 Sacrifice,
500 Exile,
501 Bounce,
502 Mill,
503 Discard,
504 Counter,
505 Tap,
506 Untap,
507 Copy,
508 Buff,
509 Debuff,
510 Heal,
511 LoseLife,
512 Reveal,
513 Draw,
514 Fetch,
515 GainControl,
516 Fight,
517 Attach,
518 Attack,
519 Block,
520 Hostile,
521 Friendly,
522}
523
524#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
525#[serde(rename_all = "camelCase")]
526#[ts(export, export_to = "game/index.ts")]
527pub struct PlaymatSettings {
528 #[serde(default, skip_serializing_if = "Option::is_none")]
529 #[ts(optional)]
530 pub opacity: Option<f32>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 #[ts(optional)]
533 pub texture: Option<f32>,
534 #[serde(default, skip_serializing_if = "Option::is_none")]
535 #[ts(optional)]
536 pub border_width: Option<f32>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
538 #[ts(optional)]
539 pub border_color: Option<String>,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 #[ts(optional)]
542 pub fit: Option<String>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 #[ts(optional)]
545 pub offset_x: Option<f32>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 #[ts(optional)]
548 pub offset_y: Option<f32>,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 #[ts(optional)]
551 pub zoom: Option<f32>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 #[ts(optional)]
554 pub blur: Option<f32>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 #[ts(optional)]
557 pub brightness: Option<f32>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 #[ts(optional)]
560 pub color: Option<String>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
564#[ts(export, export_to = "game/index.ts")]
565pub enum GameFormat {
566 Any,
567 Standard,
568 Pioneer,
569 Modern,
570 Legacy,
571 Vintage,
572 Pauper,
573 Premodern,
574 Commander,
575 PauperCommander,
576 Brawl,
577 Oathbreaker,
578 Draft,
579 Sealed,
580}
581
582#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
583#[ts(export, export_to = "game/index.ts")]
584pub enum EngineKind {
585 #[default]
586 Manabrew,
587 Forge,
588 Ironsmith,
589}