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 ring_level: i32,
165 pub speed: i32,
166}
167
168#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
169#[serde(rename_all = "camelCase", default)]
170#[ts(export, export_to = "game/index.ts")]
171pub struct CardIdentity {
172 pub name: String,
173 pub set_code: String,
174 pub card_number: String,
175 pub is_token: bool,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 #[ts(optional)]
178 pub token_script: Option<TokenScript>,
179}
180
181#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
182#[serde(rename_all = "camelCase", default)]
183#[ts(export, export_to = "game/index.ts")]
184pub struct ClassLevelDto {
185 pub level: i32,
186 pub oracle: String,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 #[ts(optional)]
189 pub cost: Option<String>,
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
193#[serde(rename_all = "camelCase", default)]
194#[ts(export, export_to = "game/index.ts")]
195pub struct SagaChapterDto {
196 pub chapters: Vec<i32>,
197 pub oracle: String,
198}
199
200#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
201#[serde(rename_all = "camelCase", default)]
202#[ts(export, export_to = "game/index.ts")]
203pub struct CardDto {
204 pub id: String,
205 pub identity: CardIdentity,
206 pub color: String,
207 pub mana_cost: String,
208 pub cmc: i32,
209 pub types: Vec<String>,
210 pub subtypes: Vec<String>,
211 pub supertypes: Vec<String>,
212 pub power: Option<String>,
213 pub toughness: Option<String>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 #[ts(optional)]
216 pub base_power: Option<i32>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 #[ts(optional)]
219 pub base_toughness: Option<i32>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 #[ts(optional)]
222 pub final_chapter: Option<i32>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 #[ts(optional)]
225 pub class_level: Option<i32>,
226 pub class_levels: Vec<ClassLevelDto>,
227 pub saga_chapters: Vec<SagaChapterDto>,
228 pub text: String,
229 pub controller_id: String,
230 pub owner_id: String,
231 pub tapped: bool,
232 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
233 pub is_crewed: bool,
234 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
235 pub is_attacking: bool,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 #[ts(optional)]
238 pub attacking_player_id: Option<String>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 #[ts(optional)]
241 pub attack_target_id: Option<String>,
242 pub keywords: Vec<String>,
243 #[ts(type = "Record<string, number>")]
246 pub counters: BTreeMap<String, u32>,
247 pub damage: i32,
248 pub summoning_sick: bool,
249 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
250 pub is_copy: bool,
251 pub is_double_faced: bool,
252 pub is_transformed: bool,
253 pub is_face_down: bool,
254 pub is_bestowed: bool,
255 pub phased_out: bool,
256 pub exerted: bool,
257 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
258 pub is_ring_bearer: bool,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 #[ts(optional)]
261 pub attached_to: Option<String>,
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
263 pub attachment_ids: Vec<String>,
264 #[serde(default, skip_serializing_if = "Vec::is_empty")]
266 pub merged_card_ids: Vec<String>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 #[ts(optional)]
269 pub flashback_cost: Option<String>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 #[ts(optional)]
272 pub kicker_cost: Option<String>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 #[ts(optional)]
275 pub effective_mana_cost: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 #[ts(optional)]
278 pub madness_cost: Option<String>,
279 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
280 pub is_madness_exiled: bool,
281 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
282 pub is_plotted: bool,
283 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
284 pub is_warp_exiled: bool,
285 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
286 pub foil: bool,
287 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
288 pub would_die_in_combat: bool,
289}
290
291#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
292#[serde(rename_all = "camelCase", default)]
293#[ts(export, export_to = "game/index.ts")]
294pub struct StackObjectDto {
295 pub id: String,
296 pub source_id: String,
297 pub controller_id: String,
298 pub owner_id: String,
299 pub identity: CardIdentity,
300 pub text: String,
301 pub is_permanent_spell: bool,
302 pub is_casting: bool,
303 pub is_double_faced: bool,
304 pub face_index: u8,
305 pub targets: Vec<TargetRef>,
306}
307
308#[cfg(test)]
309mod stack_object_tests {
310 use super::{CardIdentity, StackObjectDto};
311
312 #[test]
313 fn serializes_card_face_fields() {
314 let stack_object = StackObjectDto {
315 id: "stack-1".into(),
316 source_id: "card-1".into(),
317 controller_id: "player-0".into(),
318 owner_id: "player-1".into(),
319 identity: CardIdentity::default(),
320 text: String::new(),
321 is_permanent_spell: true,
322 is_casting: true,
323 is_double_faced: true,
324 face_index: 1,
325 targets: Vec::new(),
326 };
327
328 let value = serde_json::to_value(stack_object).unwrap();
329
330 assert_eq!(value["ownerId"], "player-1");
331 assert_eq!(value["isDoubleFaced"], true);
332 assert_eq!(value["faceIndex"], 1);
333 }
334
335 #[test]
336 fn defaults_missing_card_face_fields() {
337 let value = serde_json::json!({
338 "id": "stack-1",
339 "sourceId": "card-1",
340 "controllerId": "player-0"
341 });
342
343 let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
344
345 assert!(stack_object.owner_id.is_empty());
346 assert!(!stack_object.is_double_faced);
347 assert_eq!(stack_object.face_index, 0);
348 }
349}
350
351#[derive(
352 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
353)]
354#[serde(rename_all = "camelCase")]
355#[ts(export, export_to = "game/index.ts")]
356pub enum TargetingIntent {
357 #[default]
358 Damage,
359 Destroy,
360 Sacrifice,
361 Exile,
362 Bounce,
363 Mill,
364 Discard,
365 Counter,
366 Tap,
367 Untap,
368 Copy,
369 Buff,
370 Debuff,
371 Heal,
372 LoseLife,
373 Reveal,
374 Draw,
375 Fetch,
376 GainControl,
377 Fight,
378 Attach,
379 Attack,
380 Block,
381 Hostile,
382 Friendly,
383}
384
385#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
386#[serde(rename_all = "camelCase")]
387#[ts(export, export_to = "game/index.ts")]
388pub struct PlaymatSettings {
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 #[ts(optional)]
391 pub opacity: Option<f32>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 #[ts(optional)]
394 pub texture: Option<f32>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 #[ts(optional)]
397 pub border_width: Option<f32>,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 #[ts(optional)]
400 pub border_color: Option<String>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 #[ts(optional)]
403 pub fit: Option<String>,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 #[ts(optional)]
406 pub offset_x: Option<f32>,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
408 #[ts(optional)]
409 pub offset_y: Option<f32>,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 #[ts(optional)]
412 pub zoom: Option<f32>,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 #[ts(optional)]
415 pub blur: Option<f32>,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 #[ts(optional)]
418 pub brightness: Option<f32>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 #[ts(optional)]
421 pub color: Option<String>,
422}