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 pub is_permanent_spell: bool,
322 pub is_casting: bool,
323 pub is_double_faced: bool,
324 pub face_index: u8,
325 pub targets: Vec<TargetRef>,
326}
327
328#[cfg(test)]
329mod stack_object_tests {
330 use super::{CardIdentity, StackObjectDto};
331
332 #[test]
333 fn serializes_card_face_fields() {
334 let stack_object = StackObjectDto {
335 id: "stack-1".into(),
336 source_id: "card-1".into(),
337 controller_id: "player-0".into(),
338 owner_id: "player-1".into(),
339 identity: CardIdentity::default(),
340 text: String::new(),
341 is_permanent_spell: true,
342 is_casting: true,
343 is_double_faced: true,
344 face_index: 1,
345 targets: Vec::new(),
346 };
347
348 let value = serde_json::to_value(stack_object).unwrap();
349
350 assert_eq!(value["ownerId"], "player-1");
351 assert_eq!(value["isDoubleFaced"], true);
352 assert_eq!(value["faceIndex"], 1);
353 }
354
355 #[test]
356 fn defaults_missing_card_face_fields() {
357 let value = serde_json::json!({
358 "id": "stack-1",
359 "sourceId": "card-1",
360 "controllerId": "player-0"
361 });
362
363 let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
364
365 assert!(stack_object.owner_id.is_empty());
366 assert!(!stack_object.is_double_faced);
367 assert_eq!(stack_object.face_index, 0);
368 }
369}
370
371#[derive(
372 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
373)]
374#[serde(rename_all = "camelCase")]
375#[ts(export, export_to = "game/index.ts")]
376pub enum TargetingIntent {
377 #[default]
378 Damage,
379 Destroy,
380 Sacrifice,
381 Exile,
382 Bounce,
383 Mill,
384 Discard,
385 Counter,
386 Tap,
387 Untap,
388 Copy,
389 Buff,
390 Debuff,
391 Heal,
392 LoseLife,
393 Reveal,
394 Draw,
395 Fetch,
396 GainControl,
397 Fight,
398 Attach,
399 Attack,
400 Block,
401 Hostile,
402 Friendly,
403}
404
405#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
406#[serde(rename_all = "camelCase")]
407#[ts(export, export_to = "game/index.ts")]
408pub struct PlaymatSettings {
409 #[serde(default, skip_serializing_if = "Option::is_none")]
410 #[ts(optional)]
411 pub opacity: Option<f32>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 #[ts(optional)]
414 pub texture: Option<f32>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 #[ts(optional)]
417 pub border_width: Option<f32>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 #[ts(optional)]
420 pub border_color: Option<String>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 #[ts(optional)]
423 pub fit: Option<String>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 #[ts(optional)]
426 pub offset_x: Option<f32>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 #[ts(optional)]
429 pub offset_y: Option<f32>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 #[ts(optional)]
432 pub zoom: Option<f32>,
433 #[serde(default, skip_serializing_if = "Option::is_none")]
434 #[ts(optional)]
435 pub blur: Option<f32>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 #[ts(optional)]
438 pub brightness: Option<f32>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 #[ts(optional)]
441 pub color: Option<String>,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
445#[ts(export, export_to = "game/index.ts")]
446pub enum GameFormat {
447 Any,
448 Standard,
449 Pioneer,
450 Modern,
451 Legacy,
452 Vintage,
453 Pauper,
454 Premodern,
455 Commander,
456 Brawl,
457 Oathbreaker,
458 Draft,
459 Sealed,
460}
461
462#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
463#[ts(export, export_to = "game/index.ts")]
464pub enum EngineKind {
465 #[default]
466 Manabrew,
467 Forge,
468 Ironsmith,
469}