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