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