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