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