Skip to main content

manabrew_protocol/game/
mod.rs

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    Attractions,
72    Junkyard,
73    Sideboard,
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 StepKind {
80    #[default]
81    Untap,
82    Upkeep,
83    Draw,
84    Main1,
85    CombatBegin,
86    CombatDeclareAttackers,
87    CombatDeclareBlockers,
88    CombatFirstStrikeDamage,
89    CombatDamage,
90    CombatEnd,
91    Main2,
92    EndOfTurn,
93    Cleanup,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
97#[serde(rename_all = "camelCase")]
98#[ts(export, export_to = "game/index.ts")]
99pub enum DayTime {
100    #[default]
101    Neither,
102    Day,
103    Night,
104}
105
106#[allow(clippy::large_enum_variant)]
107#[derive(Debug, Clone, Serialize, Deserialize, TS)]
108#[serde(
109    tag = "visibility",
110    rename_all = "camelCase",
111    rename_all_fields = "camelCase"
112)]
113#[ts(export, export_to = "game/index.ts")]
114pub enum CardView {
115    Visible(CardDto),
116    Hidden { id: String },
117}
118
119// One entry per (zone, owner) pair; battlefield cards are bucketed by controller.
120#[derive(Debug, Clone, Serialize, Deserialize, TS)]
121#[serde(rename_all = "camelCase")]
122#[ts(export, export_to = "game/index.ts")]
123pub struct ZoneDto {
124    pub zone: ZoneKind,
125    pub owner_id: String,
126    // Ordered top-first where order is public knowledge
127    // count can be > cards.len if hidden cards are present (library)
128    pub cards: Vec<CardView>,
129    pub count: usize,
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
133#[serde(rename_all = "camelCase")]
134#[ts(export, export_to = "game/index.ts")]
135pub struct GameViewDto {
136    pub game_id: String,
137    pub turn: u32,
138    pub step: StepKind,
139    pub combat_assignments: Vec<CombatAssignmentDto>,
140    pub active_player_id: String,
141    pub priority_player_id: String,
142    pub players: Vec<PlayerDto>,
143    pub zones: Vec<ZoneDto>,
144    pub stack: Vec<StackObjectDto>,
145    pub game_over: bool,
146    pub winner_id: Option<String>,
147    pub monarch_id: Option<String>,
148    pub initiative_holder_id: Option<String>,
149    pub day_time: DayTime,
150    #[ts(optional)]
151    pub active_plane_names: Option<Vec<String>>,
152    #[serde(default)]
153    pub checkpoints: Vec<CheckpointDto>,
154    #[serde(default)]
155    pub restore_vote: Option<RestoreVoteDto>,
156    #[serde(default)]
157    pub snapshot_recording: bool,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, TS)]
161#[serde(rename_all = "camelCase")]
162#[ts(export, export_to = "game/index.ts")]
163pub struct RestoreVoteDto {
164    pub vote_id: u32,
165    pub checkpoint: CheckpointDto,
166    pub requested_by_player_id: String,
167    pub awaiting_player_ids: Vec<String>,
168    pub status: RestoreVoteStatus,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, TS)]
172#[serde(
173    tag = "type",
174    rename_all = "camelCase",
175    rename_all_fields = "camelCase"
176)]
177#[ts(export, export_to = "game/index.ts")]
178pub enum RestoreVoteStatus {
179    Pending,
180    Approved,
181    Declined { player_id: String },
182    Unavailable,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, TS)]
186#[serde(rename_all = "camelCase")]
187#[ts(export, export_to = "game/index.ts")]
188pub struct CheckpointDto {
189    pub checkpoint_id: u32,
190    pub turn: u32,
191    pub step: StepKind,
192    pub active_player_id: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, TS)]
196#[serde(rename_all = "camelCase")]
197#[ts(export, export_to = "game/index.ts")]
198pub struct CombatAssignmentDto {
199    pub blocker_id: String,
200    pub attacker_id: String,
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
204#[serde(rename_all = "camelCase")]
205#[ts(export, export_to = "game/index.ts")]
206pub enum PlayerStatus {
207    #[default]
208    Playing,
209    Lost,
210    Conceded,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, TS)]
214#[serde(rename_all = "camelCase")]
215#[ts(export, export_to = "game/index.ts")]
216pub struct DungeonStateDto {
217    pub name: String,
218    pub room: String,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, TS)]
222#[serde(rename_all = "camelCase")]
223#[ts(export, export_to = "game/index.ts")]
224pub struct PlayerDto {
225    pub id: String,
226    pub name: String,
227    pub status: PlayerStatus,
228    pub is_human: bool,
229    pub life: i32,
230    #[serde(default)]
231    pub max_hand_size: i32,
232    #[serde(default)]
233    pub unlimited_hand_size: bool,
234    #[serde(default)]
235    pub lands_played_this_turn: i32,
236    #[serde(default)]
237    pub max_land_plays_per_turn: i32,
238    #[serde(default)]
239    pub unlimited_land_plays: bool,
240    #[serde(default)]
241    pub cards_drawn_this_turn: i32,
242    #[serde(default)]
243    pub damage_prevention: i32,
244    #[serde(default)]
245    pub is_extra_turn: bool,
246    #[serde(default)]
247    pub extra_turn_count: u32,
248    #[ts(optional)]
249    pub controlled_by: Option<String>,
250    #[serde(default)]
251    pub player_keywords: Vec<String>,
252    #[serde(default)]
253    #[ts(type = "Record<string, number>")]
254    pub commander_casts: HashMap<String, u32>,
255    #[ts(optional)]
256    pub dungeon_state: Option<DungeonStateDto>,
257    #[ts(optional)]
258    pub active_scheme_names: Option<Vec<String>>,
259    #[ts(optional)]
260    pub team_number: Option<i32>,
261    pub counters: BTreeMap<PlayerCounterKind, u32>,
262    pub mana_pool: BTreeMap<ManaColor, u32>,
263    #[ts(type = "Record<string, number>")]
264    pub commander_damage: HashMap<String, i32>,
265    pub has_city_blessing: bool,
266    pub has_enduring_story: bool,
267    pub ring_level: i32,
268    pub speed: i32,
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
272#[serde(rename_all = "camelCase", default)]
273#[ts(export, export_to = "game/index.ts")]
274pub struct CardIdentity {
275    pub name: String,
276    pub set_code: String,
277    pub card_number: String,
278    pub is_token: bool,
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    #[ts(optional)]
281    pub token_script: Option<TokenScript>,
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
285#[serde(rename_all = "camelCase")]
286#[ts(export, export_to = "game/index.ts")]
287pub enum RoomDoor {
288    Left,
289    Right,
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 ClassLevelDto {
296    pub level: i32,
297    pub oracle: String,
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    #[ts(optional)]
300    pub cost: Option<String>,
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
304#[serde(rename_all = "camelCase", default)]
305#[ts(export, export_to = "game/index.ts")]
306pub struct SagaChapterDto {
307    pub chapters: Vec<i32>,
308    pub oracle: String,
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 CardDto {
315    pub id: String,
316    pub identity: CardIdentity,
317    #[serde(
318        default,
319        deserialize_with = "crate::deck_dto::CardCosmetics::deserialize_optional",
320        skip_serializing_if = "Option::is_none"
321    )]
322    #[ts(optional)]
323    pub cosmetics: Option<crate::deck_dto::CardCosmetics>,
324    pub color: String,
325    pub mana_cost: String,
326    pub cmc: i32,
327    pub types: Vec<String>,
328    pub subtypes: Vec<String>,
329    pub supertypes: Vec<String>,
330    pub power: Option<String>,
331    pub toughness: Option<String>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    #[ts(optional)]
334    pub base_power: Option<i32>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    #[ts(optional)]
337    pub base_toughness: Option<i32>,
338    #[serde(default, skip_serializing_if = "Option::is_none")]
339    #[ts(optional)]
340    pub final_chapter: Option<i32>,
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    #[ts(optional)]
343    pub class_level: Option<i32>,
344    pub class_levels: Vec<ClassLevelDto>,
345    pub saga_chapters: Vec<SagaChapterDto>,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    #[ts(optional)]
348    pub unlocked_doors: Option<Vec<RoomDoor>>,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    #[ts(optional)]
351    pub is_solved: Option<bool>,
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    #[ts(optional)]
354    pub is_suspended: Option<bool>,
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    #[ts(optional)]
357    pub is_on_adventure: Option<bool>,
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    #[ts(optional)]
360    pub is_prototyped: Option<bool>,
361    pub text: String,
362    #[serde(default)]
363    pub choices: Vec<CardChoiceDto>,
364    pub controller_id: String,
365    pub owner_id: String,
366    pub tapped: bool,
367    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
368    pub is_crewed: bool,
369    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
370    pub is_attacking: bool,
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    #[ts(optional)]
373    pub attacking_player_id: Option<String>,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    #[ts(optional)]
376    pub attack_target_id: Option<String>,
377    pub keywords: Vec<String>,
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    #[ts(optional)]
380    pub keyword_labels: Option<BTreeMap<String, String>>,
381    #[serde(default, skip_serializing_if = "Vec::is_empty")]
382    pub granted_keywords: Vec<String>,
383    // Keyed by the engine's canonical `CounterType` display form ("P1P1",
384    // "Loyalty", one-off counter names uppercase); both producers must match it.
385    #[ts(type = "Record<string, number>")]
386    pub counters: BTreeMap<String, u32>,
387    pub damage: i32,
388    pub summoning_sick: bool,
389    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
390    pub is_copy: bool,
391    pub is_double_faced: bool,
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    #[ts(optional)]
394    pub face_index: Option<u8>,
395    pub is_transformed: bool,
396    pub is_face_down: bool,
397    pub is_bestowed: bool,
398    pub phased_out: bool,
399    pub exerted: bool,
400    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
401    pub is_ring_bearer: bool,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    #[ts(optional)]
404    pub attached_to: Option<String>,
405    #[serde(default, skip_serializing_if = "Vec::is_empty")]
406    pub attachment_ids: Vec<String>,
407    // Mutate pile: the card ids merged under this top card.
408    #[serde(default, skip_serializing_if = "Vec::is_empty")]
409    pub merged_card_ids: Vec<String>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    #[ts(optional)]
412    pub flashback_cost: Option<String>,
413    #[serde(skip_serializing_if = "Option::is_none")]
414    #[ts(optional)]
415    pub kicker_cost: Option<String>,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    #[ts(optional)]
418    pub effective_mana_cost: Option<String>,
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    #[ts(optional)]
421    pub commander_tax: Option<i32>,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    #[ts(optional)]
424    pub madness_cost: Option<String>,
425    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
426    pub is_madness_exiled: bool,
427    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
428    pub is_plotted: bool,
429    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
430    pub is_warp_exiled: bool,
431    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
432    pub foil: bool,
433    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
434    pub would_die_in_combat: bool,
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    #[ts(optional)]
437    pub zone: Option<ZoneKind>,
438}
439
440#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
441#[serde(rename_all = "camelCase", default)]
442#[ts(export, export_to = "game/index.ts")]
443pub struct StackObjectDto {
444    pub id: String,
445    pub source_id: String,
446    pub controller_id: String,
447    pub owner_id: String,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    #[ts(optional)]
450    pub identity: Option<CardIdentity>,
451    pub text: String,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    #[ts(optional)]
454    pub source_ability_text: Option<String>,
455    pub is_permanent_spell: bool,
456    pub is_casting: bool,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    #[ts(optional)]
459    pub source_card: Option<CardDto>,
460    pub targets: Vec<TargetRef>,
461}
462
463#[derive(
464    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
465)]
466#[serde(rename_all = "camelCase")]
467#[ts(export, export_to = "game/index.ts")]
468pub enum TargetingIntent {
469    #[default]
470    Damage,
471    Destroy,
472    Sacrifice,
473    Exile,
474    Bounce,
475    Mill,
476    Discard,
477    Counter,
478    Tap,
479    Untap,
480    Copy,
481    Buff,
482    Debuff,
483    Heal,
484    LoseLife,
485    Reveal,
486    Draw,
487    Fetch,
488    GainControl,
489    Fight,
490    Attach,
491    Attack,
492    Block,
493    Hostile,
494    Friendly,
495}
496
497#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
498#[serde(rename_all = "camelCase")]
499#[ts(export, export_to = "game/index.ts")]
500pub struct PlaymatSettings {
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    #[ts(optional)]
503    pub opacity: Option<f32>,
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    #[ts(optional)]
506    pub texture: Option<f32>,
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    #[ts(optional)]
509    pub border_width: Option<f32>,
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    #[ts(optional)]
512    pub border_color: Option<String>,
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    #[ts(optional)]
515    pub fit: Option<String>,
516    #[serde(default, skip_serializing_if = "Option::is_none")]
517    #[ts(optional)]
518    pub offset_x: Option<f32>,
519    #[serde(default, skip_serializing_if = "Option::is_none")]
520    #[ts(optional)]
521    pub offset_y: Option<f32>,
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    #[ts(optional)]
524    pub zoom: Option<f32>,
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    #[ts(optional)]
527    pub blur: Option<f32>,
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    #[ts(optional)]
530    pub brightness: Option<f32>,
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    #[ts(optional)]
533    pub color: Option<String>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
537#[ts(export, export_to = "game/index.ts")]
538pub enum GameFormat {
539    Any,
540    Standard,
541    Pioneer,
542    Modern,
543    Legacy,
544    Vintage,
545    Pauper,
546    Premodern,
547    Commander,
548    PauperCommander,
549    Brawl,
550    Oathbreaker,
551    Draft,
552    Sealed,
553}
554
555#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
556#[ts(export, export_to = "game/index.ts")]
557pub enum EngineKind {
558    #[default]
559    Manabrew,
560    Forge,
561    Ironsmith,
562}