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 pub color: String,
310 pub mana_cost: String,
311 pub cmc: i32,
312 pub types: Vec<String>,
313 pub subtypes: Vec<String>,
314 pub supertypes: Vec<String>,
315 pub power: Option<String>,
316 pub toughness: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 #[ts(optional)]
319 pub base_power: Option<i32>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 #[ts(optional)]
322 pub base_toughness: Option<i32>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 #[ts(optional)]
325 pub final_chapter: Option<i32>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 #[ts(optional)]
328 pub class_level: Option<i32>,
329 pub class_levels: Vec<ClassLevelDto>,
330 pub saga_chapters: Vec<SagaChapterDto>,
331 pub text: String,
332 #[serde(default)]
333 pub choices: Vec<CardChoiceDto>,
334 pub controller_id: String,
335 pub owner_id: String,
336 pub tapped: bool,
337 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
338 pub is_crewed: bool,
339 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
340 pub is_attacking: bool,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 #[ts(optional)]
343 pub attacking_player_id: Option<String>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 #[ts(optional)]
346 pub attack_target_id: Option<String>,
347 pub keywords: Vec<String>,
348 #[ts(type = "Record<string, number>")]
351 pub counters: BTreeMap<String, u32>,
352 pub damage: i32,
353 pub summoning_sick: bool,
354 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
355 pub is_copy: bool,
356 pub is_double_faced: bool,
357 pub is_transformed: bool,
358 pub is_face_down: bool,
359 pub is_bestowed: bool,
360 pub phased_out: bool,
361 pub exerted: bool,
362 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
363 pub is_ring_bearer: bool,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 #[ts(optional)]
366 pub attached_to: Option<String>,
367 #[serde(default, skip_serializing_if = "Vec::is_empty")]
368 pub attachment_ids: Vec<String>,
369 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub merged_card_ids: Vec<String>,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 #[ts(optional)]
374 pub flashback_cost: Option<String>,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 #[ts(optional)]
377 pub kicker_cost: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 #[ts(optional)]
380 pub effective_mana_cost: Option<String>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 #[ts(optional)]
383 pub commander_tax: Option<i32>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 #[ts(optional)]
386 pub madness_cost: Option<String>,
387 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
388 pub is_madness_exiled: bool,
389 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
390 pub is_plotted: bool,
391 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
392 pub is_warp_exiled: bool,
393 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
394 pub foil: bool,
395 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
396 pub would_die_in_combat: bool,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 #[ts(optional)]
399 pub zone: Option<ZoneKind>,
400}
401
402#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
403#[serde(rename_all = "camelCase", default)]
404#[ts(export, export_to = "game/index.ts")]
405pub struct StackObjectDto {
406 pub id: String,
407 pub source_id: String,
408 pub controller_id: String,
409 pub owner_id: String,
410 pub identity: CardIdentity,
411 pub text: String,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 #[ts(optional)]
414 pub source_ability_text: Option<String>,
415 pub is_permanent_spell: bool,
416 pub is_casting: bool,
417 pub is_double_faced: bool,
418 pub face_index: u8,
419 pub targets: Vec<TargetRef>,
420}
421
422#[cfg(test)]
423mod stack_object_tests {
424 use super::{CardIdentity, StackObjectDto};
425
426 #[test]
427 fn serializes_card_face_fields() {
428 let stack_object = StackObjectDto {
429 id: "stack-1".into(),
430 source_id: "card-1".into(),
431 controller_id: "player-0".into(),
432 owner_id: "player-1".into(),
433 identity: CardIdentity::default(),
434 text: String::new(),
435 source_ability_text: Some("When this enters, draw a card.".into()),
436 is_permanent_spell: true,
437 is_casting: true,
438 is_double_faced: true,
439 face_index: 1,
440 targets: Vec::new(),
441 };
442
443 let value = serde_json::to_value(stack_object).unwrap();
444 assert_eq!(value["sourceAbilityText"], "When this enters, draw a card.");
445
446 assert_eq!(value["ownerId"], "player-1");
447 assert_eq!(value["isDoubleFaced"], true);
448 assert_eq!(value["faceIndex"], 1);
449 }
450
451 #[test]
452 fn defaults_missing_card_face_fields() {
453 let value = serde_json::json!({
454 "id": "stack-1",
455 "sourceId": "card-1",
456 "controllerId": "player-0"
457 });
458
459 let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
460
461 assert!(stack_object.owner_id.is_empty());
462 assert!(!stack_object.is_double_faced);
463 assert_eq!(stack_object.face_index, 0);
464 }
465}
466
467#[derive(
468 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
469)]
470#[serde(rename_all = "camelCase")]
471#[ts(export, export_to = "game/index.ts")]
472pub enum TargetingIntent {
473 #[default]
474 Damage,
475 Destroy,
476 Sacrifice,
477 Exile,
478 Bounce,
479 Mill,
480 Discard,
481 Counter,
482 Tap,
483 Untap,
484 Copy,
485 Buff,
486 Debuff,
487 Heal,
488 LoseLife,
489 Reveal,
490 Draw,
491 Fetch,
492 GainControl,
493 Fight,
494 Attach,
495 Attack,
496 Block,
497 Hostile,
498 Friendly,
499}
500
501#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
502#[serde(rename_all = "camelCase")]
503#[ts(export, export_to = "game/index.ts")]
504pub struct PlaymatSettings {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 #[ts(optional)]
507 pub opacity: Option<f32>,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
509 #[ts(optional)]
510 pub texture: Option<f32>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
512 #[ts(optional)]
513 pub border_width: Option<f32>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 #[ts(optional)]
516 pub border_color: Option<String>,
517 #[serde(default, skip_serializing_if = "Option::is_none")]
518 #[ts(optional)]
519 pub fit: Option<String>,
520 #[serde(default, skip_serializing_if = "Option::is_none")]
521 #[ts(optional)]
522 pub offset_x: Option<f32>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
524 #[ts(optional)]
525 pub offset_y: Option<f32>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
527 #[ts(optional)]
528 pub zoom: Option<f32>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
530 #[ts(optional)]
531 pub blur: Option<f32>,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 #[ts(optional)]
534 pub brightness: Option<f32>,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
536 #[ts(optional)]
537 pub color: Option<String>,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
541#[ts(export, export_to = "game/index.ts")]
542pub enum GameFormat {
543 Any,
544 Standard,
545 Pioneer,
546 Modern,
547 Legacy,
548 Vintage,
549 Pauper,
550 Premodern,
551 Commander,
552 Brawl,
553 Oathbreaker,
554 Draft,
555 Sealed,
556}
557
558#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
559#[ts(export, export_to = "game/index.ts")]
560pub enum EngineKind {
561 #[default]
562 Manabrew,
563 Forge,
564 Ironsmith,
565}