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}
150
151#[derive(Debug, Clone, Serialize, Deserialize, TS)]
152#[serde(rename_all = "camelCase")]
153#[ts(export, export_to = "game/index.ts")]
154pub struct CombatAssignmentDto {
155 pub blocker_id: String,
156 pub attacker_id: String,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
160#[serde(rename_all = "camelCase")]
161#[ts(export, export_to = "game/index.ts")]
162pub enum PlayerStatus {
163 #[default]
164 Playing,
165 Lost,
166 Conceded,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, TS)]
170#[serde(rename_all = "camelCase")]
171#[ts(export, export_to = "game/index.ts")]
172pub struct DungeonStateDto {
173 pub name: String,
174 pub room: String,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, TS)]
178#[serde(rename_all = "camelCase")]
179#[ts(export, export_to = "game/index.ts")]
180pub struct PlayerDto {
181 pub id: String,
182 pub name: String,
183 pub status: PlayerStatus,
184 pub is_human: bool,
185 pub life: i32,
186 #[serde(default)]
187 pub max_hand_size: i32,
188 #[serde(default)]
189 pub unlimited_hand_size: bool,
190 #[serde(default)]
191 pub lands_played_this_turn: i32,
192 #[serde(default)]
193 pub max_land_plays_per_turn: i32,
194 #[serde(default)]
195 pub unlimited_land_plays: bool,
196 #[serde(default)]
197 pub cards_drawn_this_turn: i32,
198 #[serde(default)]
199 pub damage_prevention: i32,
200 #[serde(default)]
201 pub is_extra_turn: bool,
202 #[serde(default)]
203 pub extra_turn_count: u32,
204 #[ts(optional)]
205 pub controlled_by: Option<String>,
206 #[serde(default)]
207 pub player_keywords: Vec<String>,
208 #[serde(default)]
209 #[ts(type = "Record<string, number>")]
210 pub commander_casts: HashMap<String, u32>,
211 #[ts(optional)]
212 pub dungeon_state: Option<DungeonStateDto>,
213 #[ts(optional)]
214 pub active_scheme_names: Option<Vec<String>>,
215 #[ts(optional)]
216 pub team_number: Option<i32>,
217 pub counters: BTreeMap<PlayerCounterKind, u32>,
218 pub mana_pool: BTreeMap<ManaColor, u32>,
219 #[ts(type = "Record<string, number>")]
220 pub commander_damage: HashMap<String, i32>,
221 pub has_city_blessing: bool,
222 pub has_enduring_story: bool,
223 pub ring_level: i32,
224 pub speed: i32,
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
228#[serde(rename_all = "camelCase", default)]
229#[ts(export, export_to = "game/index.ts")]
230pub struct CardIdentity {
231 pub name: String,
232 pub set_code: String,
233 pub card_number: String,
234 pub is_token: bool,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 #[ts(optional)]
237 pub token_script: Option<TokenScript>,
238}
239
240#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
241#[serde(rename_all = "camelCase", default)]
242#[ts(export, export_to = "game/index.ts")]
243pub struct ClassLevelDto {
244 pub level: i32,
245 pub oracle: String,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 #[ts(optional)]
248 pub cost: Option<String>,
249}
250
251#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
252#[serde(rename_all = "camelCase", default)]
253#[ts(export, export_to = "game/index.ts")]
254pub struct SagaChapterDto {
255 pub chapters: Vec<i32>,
256 pub oracle: String,
257}
258
259#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
260#[serde(rename_all = "camelCase", default)]
261#[ts(export, export_to = "game/index.ts")]
262pub struct CardDto {
263 pub id: String,
264 pub identity: CardIdentity,
265 pub color: String,
266 pub mana_cost: String,
267 pub cmc: i32,
268 pub types: Vec<String>,
269 pub subtypes: Vec<String>,
270 pub supertypes: Vec<String>,
271 pub power: Option<String>,
272 pub toughness: Option<String>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 #[ts(optional)]
275 pub base_power: Option<i32>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 #[ts(optional)]
278 pub base_toughness: Option<i32>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 #[ts(optional)]
281 pub final_chapter: Option<i32>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 #[ts(optional)]
284 pub class_level: Option<i32>,
285 pub class_levels: Vec<ClassLevelDto>,
286 pub saga_chapters: Vec<SagaChapterDto>,
287 pub text: String,
288 #[serde(default)]
289 pub choices: Vec<CardChoiceDto>,
290 pub controller_id: String,
291 pub owner_id: String,
292 pub tapped: bool,
293 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
294 pub is_crewed: bool,
295 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
296 pub is_attacking: bool,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 #[ts(optional)]
299 pub attacking_player_id: Option<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 #[ts(optional)]
302 pub attack_target_id: Option<String>,
303 pub keywords: Vec<String>,
304 #[ts(type = "Record<string, number>")]
307 pub counters: BTreeMap<String, u32>,
308 pub damage: i32,
309 pub summoning_sick: bool,
310 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311 pub is_copy: bool,
312 pub is_double_faced: bool,
313 pub is_transformed: bool,
314 pub is_face_down: bool,
315 pub is_bestowed: bool,
316 pub phased_out: bool,
317 pub exerted: bool,
318 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
319 pub is_ring_bearer: bool,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 #[ts(optional)]
322 pub attached_to: Option<String>,
323 #[serde(default, skip_serializing_if = "Vec::is_empty")]
324 pub attachment_ids: Vec<String>,
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub merged_card_ids: Vec<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 #[ts(optional)]
330 pub flashback_cost: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 #[ts(optional)]
333 pub kicker_cost: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 #[ts(optional)]
336 pub effective_mana_cost: Option<String>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 #[ts(optional)]
339 pub commander_tax: Option<i32>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 #[ts(optional)]
342 pub madness_cost: Option<String>,
343 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
344 pub is_madness_exiled: bool,
345 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
346 pub is_plotted: bool,
347 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
348 pub is_warp_exiled: bool,
349 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
350 pub foil: bool,
351 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
352 pub would_die_in_combat: bool,
353}
354
355#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
356#[serde(rename_all = "camelCase", default)]
357#[ts(export, export_to = "game/index.ts")]
358pub struct StackObjectDto {
359 pub id: String,
360 pub source_id: String,
361 pub controller_id: String,
362 pub owner_id: String,
363 pub identity: CardIdentity,
364 pub text: String,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 #[ts(optional)]
367 pub source_ability_text: Option<String>,
368 pub is_permanent_spell: bool,
369 pub is_casting: bool,
370 pub is_double_faced: bool,
371 pub face_index: u8,
372 pub targets: Vec<TargetRef>,
373}
374
375#[cfg(test)]
376mod stack_object_tests {
377 use super::{CardIdentity, StackObjectDto};
378
379 #[test]
380 fn serializes_card_face_fields() {
381 let stack_object = StackObjectDto {
382 id: "stack-1".into(),
383 source_id: "card-1".into(),
384 controller_id: "player-0".into(),
385 owner_id: "player-1".into(),
386 identity: CardIdentity::default(),
387 text: String::new(),
388 source_ability_text: Some("When this enters, draw a card.".into()),
389 is_permanent_spell: true,
390 is_casting: true,
391 is_double_faced: true,
392 face_index: 1,
393 targets: Vec::new(),
394 };
395
396 let value = serde_json::to_value(stack_object).unwrap();
397 assert_eq!(value["sourceAbilityText"], "When this enters, draw a card.");
398
399 assert_eq!(value["ownerId"], "player-1");
400 assert_eq!(value["isDoubleFaced"], true);
401 assert_eq!(value["faceIndex"], 1);
402 }
403
404 #[test]
405 fn defaults_missing_card_face_fields() {
406 let value = serde_json::json!({
407 "id": "stack-1",
408 "sourceId": "card-1",
409 "controllerId": "player-0"
410 });
411
412 let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
413
414 assert!(stack_object.owner_id.is_empty());
415 assert!(!stack_object.is_double_faced);
416 assert_eq!(stack_object.face_index, 0);
417 }
418}
419
420#[derive(
421 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
422)]
423#[serde(rename_all = "camelCase")]
424#[ts(export, export_to = "game/index.ts")]
425pub enum TargetingIntent {
426 #[default]
427 Damage,
428 Destroy,
429 Sacrifice,
430 Exile,
431 Bounce,
432 Mill,
433 Discard,
434 Counter,
435 Tap,
436 Untap,
437 Copy,
438 Buff,
439 Debuff,
440 Heal,
441 LoseLife,
442 Reveal,
443 Draw,
444 Fetch,
445 GainControl,
446 Fight,
447 Attach,
448 Attack,
449 Block,
450 Hostile,
451 Friendly,
452}
453
454#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
455#[serde(rename_all = "camelCase")]
456#[ts(export, export_to = "game/index.ts")]
457pub struct PlaymatSettings {
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 #[ts(optional)]
460 pub opacity: Option<f32>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
462 #[ts(optional)]
463 pub texture: Option<f32>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 #[ts(optional)]
466 pub border_width: Option<f32>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 #[ts(optional)]
469 pub border_color: Option<String>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 #[ts(optional)]
472 pub fit: Option<String>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 #[ts(optional)]
475 pub offset_x: Option<f32>,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 #[ts(optional)]
478 pub offset_y: Option<f32>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 #[ts(optional)]
481 pub zoom: Option<f32>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 #[ts(optional)]
484 pub blur: Option<f32>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 #[ts(optional)]
487 pub brightness: Option<f32>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 #[ts(optional)]
490 pub color: Option<String>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
494#[ts(export, export_to = "game/index.ts")]
495pub enum GameFormat {
496 Any,
497 Standard,
498 Pioneer,
499 Modern,
500 Legacy,
501 Vintage,
502 Pauper,
503 Premodern,
504 Commander,
505 Brawl,
506 Oathbreaker,
507 Draft,
508 Sealed,
509}
510
511#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
512#[ts(export, export_to = "game/index.ts")]
513pub enum EngineKind {
514 #[default]
515 Manabrew,
516 Forge,
517 Ironsmith,
518}