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, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
26#[serde(rename_all = "camelCase")]
27#[ts(export, export_to = "game/index.ts")]
28pub struct Mana {
29 pub color: ManaColor,
30 pub amount: i32,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, TS)]
34#[serde(rename_all = "camelCase")]
35#[ts(export, export_to = "game/index.ts")]
36pub enum PlayerCounterKind {
37 Poison,
38 Energy,
39 Experience,
40 Radiation,
41 Ticket,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
45#[serde(rename_all = "camelCase")]
46#[ts(export, export_to = "game/index.ts")]
47pub enum ZoneKind {
48 Battlefield,
49 Hand,
50 Library,
51 Graveyard,
52 Exile,
53 Command,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
57#[serde(rename_all = "camelCase")]
58#[ts(export, export_to = "game/index.ts")]
59pub enum StepKind {
60 #[default]
61 Untap,
62 Upkeep,
63 Draw,
64 Main1,
65 CombatBegin,
66 CombatDeclareAttackers,
67 CombatDeclareBlockers,
68 CombatFirstStrikeDamage,
69 CombatDamage,
70 CombatEnd,
71 Main2,
72 EndOfTurn,
73 Cleanup,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
77#[serde(rename_all = "camelCase")]
78#[ts(export, export_to = "game/index.ts")]
79pub enum DayTime {
80 #[default]
81 Neither,
82 Day,
83 Night,
84}
85
86#[allow(clippy::large_enum_variant)]
87#[derive(Debug, Clone, Serialize, Deserialize, TS)]
88#[serde(
89 tag = "visibility",
90 rename_all = "camelCase",
91 rename_all_fields = "camelCase"
92)]
93#[ts(export, export_to = "game/index.ts")]
94pub enum CardView {
95 Visible(CardDto),
96 Hidden { id: String },
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, TS)]
101#[serde(rename_all = "camelCase")]
102#[ts(export, export_to = "game/index.ts")]
103pub struct ZoneDto {
104 pub zone: ZoneKind,
105 pub owner_id: String,
106 pub cards: Vec<CardView>,
109 pub count: usize,
110}
111
112#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
113#[serde(rename_all = "camelCase")]
114#[ts(export, export_to = "game/index.ts")]
115pub struct GameViewDto {
116 pub game_id: String,
117 pub turn: u32,
118 pub step: StepKind,
119 pub combat_assignments: Vec<CombatAssignmentDto>,
120 pub active_player_id: String,
121 pub priority_player_id: String,
122 pub players: Vec<PlayerDto>,
123 pub zones: Vec<ZoneDto>,
124 pub stack: Vec<StackObjectDto>,
125 pub game_over: bool,
126 pub winner_id: Option<String>,
127 pub monarch_id: Option<String>,
128 pub initiative_holder_id: Option<String>,
129 pub day_time: DayTime,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, TS)]
133#[serde(rename_all = "camelCase")]
134#[ts(export, export_to = "game/index.ts")]
135pub struct CombatAssignmentDto {
136 pub blocker_id: String,
137 pub attacker_id: String,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
141#[serde(rename_all = "camelCase")]
142#[ts(export, export_to = "game/index.ts")]
143pub enum PlayerStatus {
144 #[default]
145 Playing,
146 Lost,
147 Conceded,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, TS)]
151#[serde(rename_all = "camelCase")]
152#[ts(export, export_to = "game/index.ts")]
153pub struct PlayerDto {
154 pub id: String,
155 pub name: String,
156 pub status: PlayerStatus,
157 pub is_human: bool,
158 pub life: i32,
159 pub counters: BTreeMap<PlayerCounterKind, u32>,
160 pub mana_pool: BTreeMap<ManaColor, u32>,
161 #[ts(type = "Record<string, number>")]
162 pub commander_damage: HashMap<String, i32>,
163 pub has_city_blessing: bool,
164 pub has_enduring_story: bool,
165 pub ring_level: i32,
166 pub speed: i32,
167}
168
169#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
170#[serde(rename_all = "camelCase", default)]
171#[ts(export, export_to = "game/index.ts")]
172pub struct CardIdentity {
173 pub name: String,
174 pub set_code: String,
175 pub card_number: String,
176 pub is_token: bool,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 #[ts(optional)]
179 pub token_script: Option<TokenScript>,
180}
181
182#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
183#[serde(rename_all = "camelCase", default)]
184#[ts(export, export_to = "game/index.ts")]
185pub struct ClassLevelDto {
186 pub level: i32,
187 pub oracle: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 #[ts(optional)]
190 pub cost: Option<String>,
191}
192
193#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
194#[serde(rename_all = "camelCase", default)]
195#[ts(export, export_to = "game/index.ts")]
196pub struct SagaChapterDto {
197 pub chapters: Vec<i32>,
198 pub oracle: String,
199}
200
201#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
202#[serde(rename_all = "camelCase", default)]
203#[ts(export, export_to = "game/index.ts")]
204pub struct CardDto {
205 pub id: String,
206 pub identity: CardIdentity,
207 pub color: String,
208 pub mana_cost: String,
209 pub cmc: i32,
210 pub types: Vec<String>,
211 pub subtypes: Vec<String>,
212 pub supertypes: Vec<String>,
213 pub power: Option<String>,
214 pub toughness: Option<String>,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 #[ts(optional)]
217 pub base_power: Option<i32>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 #[ts(optional)]
220 pub base_toughness: Option<i32>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 #[ts(optional)]
223 pub final_chapter: Option<i32>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 #[ts(optional)]
226 pub class_level: Option<i32>,
227 pub class_levels: Vec<ClassLevelDto>,
228 pub saga_chapters: Vec<SagaChapterDto>,
229 pub text: String,
230 pub controller_id: String,
231 pub owner_id: String,
232 pub tapped: bool,
233 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
234 pub is_crewed: bool,
235 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
236 pub is_attacking: bool,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 #[ts(optional)]
239 pub attacking_player_id: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 #[ts(optional)]
242 pub attack_target_id: Option<String>,
243 pub keywords: Vec<String>,
244 #[ts(type = "Record<string, number>")]
247 pub counters: BTreeMap<String, u32>,
248 pub damage: i32,
249 pub summoning_sick: bool,
250 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
251 pub is_copy: bool,
252 pub is_double_faced: bool,
253 pub is_transformed: bool,
254 pub is_face_down: bool,
255 pub is_bestowed: bool,
256 pub phased_out: bool,
257 pub exerted: bool,
258 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
259 pub is_ring_bearer: bool,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 #[ts(optional)]
262 pub attached_to: Option<String>,
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub attachment_ids: Vec<String>,
265 #[serde(default, skip_serializing_if = "Vec::is_empty")]
267 pub merged_card_ids: Vec<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 #[ts(optional)]
270 pub flashback_cost: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 #[ts(optional)]
273 pub kicker_cost: Option<String>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 #[ts(optional)]
276 pub effective_mana_cost: Option<String>,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 #[ts(optional)]
279 pub madness_cost: Option<String>,
280 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
281 pub is_madness_exiled: bool,
282 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
283 pub is_plotted: bool,
284 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
285 pub is_warp_exiled: bool,
286 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
287 pub foil: bool,
288 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
289 pub would_die_in_combat: bool,
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 StackObjectDto {
296 pub id: String,
297 pub source_id: String,
298 pub controller_id: String,
299 pub owner_id: String,
300 pub identity: CardIdentity,
301 pub text: String,
302 pub is_permanent_spell: bool,
303 pub is_casting: bool,
304 pub is_double_faced: bool,
305 pub face_index: u8,
306 pub targets: Vec<TargetRef>,
307}
308
309#[cfg(test)]
310mod stack_object_tests {
311 use super::{CardIdentity, StackObjectDto};
312
313 #[test]
314 fn serializes_card_face_fields() {
315 let stack_object = StackObjectDto {
316 id: "stack-1".into(),
317 source_id: "card-1".into(),
318 controller_id: "player-0".into(),
319 owner_id: "player-1".into(),
320 identity: CardIdentity::default(),
321 text: String::new(),
322 is_permanent_spell: true,
323 is_casting: true,
324 is_double_faced: true,
325 face_index: 1,
326 targets: Vec::new(),
327 };
328
329 let value = serde_json::to_value(stack_object).unwrap();
330
331 assert_eq!(value["ownerId"], "player-1");
332 assert_eq!(value["isDoubleFaced"], true);
333 assert_eq!(value["faceIndex"], 1);
334 }
335
336 #[test]
337 fn defaults_missing_card_face_fields() {
338 let value = serde_json::json!({
339 "id": "stack-1",
340 "sourceId": "card-1",
341 "controllerId": "player-0"
342 });
343
344 let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
345
346 assert!(stack_object.owner_id.is_empty());
347 assert!(!stack_object.is_double_faced);
348 assert_eq!(stack_object.face_index, 0);
349 }
350}
351
352#[derive(
353 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
354)]
355#[serde(rename_all = "camelCase")]
356#[ts(export, export_to = "game/index.ts")]
357pub enum TargetingIntent {
358 #[default]
359 Damage,
360 Destroy,
361 Sacrifice,
362 Exile,
363 Bounce,
364 Mill,
365 Discard,
366 Counter,
367 Tap,
368 Untap,
369 Copy,
370 Buff,
371 Debuff,
372 Heal,
373 LoseLife,
374 Reveal,
375 Draw,
376 Fetch,
377 GainControl,
378 Fight,
379 Attach,
380 Attack,
381 Block,
382 Hostile,
383 Friendly,
384}
385
386#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
387#[serde(rename_all = "camelCase")]
388#[ts(export, export_to = "game/index.ts")]
389pub struct PlaymatSettings {
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 #[ts(optional)]
392 pub opacity: Option<f32>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 #[ts(optional)]
395 pub texture: Option<f32>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 #[ts(optional)]
398 pub border_width: Option<f32>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 #[ts(optional)]
401 pub border_color: Option<String>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 #[ts(optional)]
404 pub fit: Option<String>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
406 #[ts(optional)]
407 pub offset_x: Option<f32>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
409 #[ts(optional)]
410 pub offset_y: Option<f32>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 #[ts(optional)]
413 pub zoom: Option<f32>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 #[ts(optional)]
416 pub blur: Option<f32>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 #[ts(optional)]
419 pub brightness: Option<f32>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 #[ts(optional)]
422 pub color: Option<String>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
426#[ts(export, export_to = "game/index.ts")]
427pub enum GameFormat {
428 Any,
429 Standard,
430 Pioneer,
431 Modern,
432 Legacy,
433 Vintage,
434 Pauper,
435 Commander,
436 Brawl,
437 Oathbreaker,
438 Draft,
439 Sealed,
440}
441
442#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
443#[ts(export, export_to = "game/index.ts")]
444pub enum EngineKind {
445 #[default]
446 Manabrew,
447 Forge,
448 Ironsmith,
449}