1use std::collections::{BTreeMap, HashMap};
16
17use serde::{Deserialize, Serialize};
18
19use forge_foundation::CardStateName;
20
21use crate::ability::ability_utils::apply_text_change_effects;
22use crate::card::card_state::CardState;
23use crate::card::valid_filter::{self, CardTraitRequirementsIr};
24use crate::card::Card;
25use crate::core::{HasSVars, Identifiable};
26use crate::event::AbilityValue;
27use crate::game::GameState;
28use crate::game_object::GameObject;
29use crate::ids::{CardId, PlayerId};
30use crate::keyword::keyword_instance::Keyword;
31use crate::keyword::keyword_interface::KeywordInterface;
32use crate::parsing::CompiledSelector;
33use crate::player::GameLossReason;
34
35const DESCRIPTIVE_KEYS: &[&str] = &[
37 "Description",
38 "SpellDescription",
39 "StackDescription",
40 "TriggerDescription",
41 "ChangeTypeDesc",
42 "ValidTgtsDesc",
43];
44
45const NO_CHANGE_KEYS: &[&str] = &[
47 "TokenScript",
48 "NewName",
49 "DefinedName",
50 "ChooseFromList",
51 "AddAbility",
52];
53
54pub enum MatchValidTarget<'a> {
56 Card(&'a Card),
57 Player(PlayerId),
58 GameObj(&'a dyn GameObject),
59 Iter(&'a [MatchValidTarget<'a>]),
60 Str(&'a str),
61 LossReason(GameLossReason),
62 PlanarDice,
64}
65
66pub enum HostName<'a> {
70 State(CardStateName),
71 Card(&'a Card),
72}
73
74#[derive(Debug, Default, Serialize, Deserialize)]
75pub struct CardTraitBase {
76 id: i32,
77 #[serde(skip)]
78 host_card_id: Option<CardId>,
79 #[serde(skip)]
80 card_state_name: Option<CardStateName>,
81 #[serde(skip)]
82 keyword: Option<KeywordInterface>,
83
84 original_map_params: HashMap<String, String>,
85 map_params: HashMap<String, String>,
86
87 intrinsic: bool,
88 suppressed: bool,
89
90 svars: HashMap<String, String>,
91
92 intrinsic_changed_text_colors: HashMap<String, String>,
93 intrinsic_changed_text_types: HashMap<String, String>,
94 changed_text_colors: HashMap<String, String>,
95 changed_text_types: HashMap<String, String>,
96}
97
98impl Clone for CardTraitBase {
99 fn clone(&self) -> Self {
100 Self {
101 id: self.id,
102 host_card_id: self.host_card_id,
103 card_state_name: self.card_state_name,
104 keyword: None,
105 original_map_params: self.original_map_params.clone(),
106 map_params: self.map_params.clone(),
107 intrinsic: self.intrinsic,
108 suppressed: self.suppressed,
109 svars: self.svars.clone(),
110 intrinsic_changed_text_colors: self.intrinsic_changed_text_colors.clone(),
111 intrinsic_changed_text_types: self.intrinsic_changed_text_types.clone(),
112 changed_text_colors: self.changed_text_colors.clone(),
113 changed_text_types: self.changed_text_types.clone(),
114 }
115 }
116}
117
118impl CardTraitBase {
119 pub fn set_id(&mut self, id: i32) {
120 self.id = id;
121 }
122
123 pub fn get_map_params(&self) -> &HashMap<String, String> {
126 &self.map_params
127 }
128
129 pub fn get_param_or_default<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
130 self.map_params
131 .get(key)
132 .map(String::as_str)
133 .unwrap_or(default)
134 }
135
136 pub fn get_param(&self, key: &str) -> Option<&str> {
137 self.map_params.get(key).map(String::as_str)
138 }
139
140 pub fn get_original_param(&self, key: &str) -> Option<&str> {
141 self.original_map_params.get(key).map(String::as_str)
142 }
143
144 pub fn has_param(&self, key: &str) -> bool {
145 self.map_params.contains_key(key)
146 }
147
148 pub fn put_param(&mut self, key: String, value: String) -> Option<String> {
149 self.map_params.insert(key, value)
150 }
151
152 pub fn remove_param(&mut self, key: &str) {
153 self.map_params.remove(key);
154 }
155
156 pub fn get_original_map_params(&self) -> &HashMap<String, String> {
157 &self.original_map_params
158 }
159
160 pub fn set_map_params(&mut self, params: HashMap<String, String>) {
163 self.original_map_params = params.clone();
164 self.map_params = params;
165 }
166
167 pub fn is_intrinsic(&self) -> bool {
170 self.intrinsic
171 }
172
173 pub fn set_intrinsic(&mut self, i: bool) {
174 self.intrinsic = i;
175 }
176
177 pub fn host_card_id(&self) -> CardId {
180 self.host_card_id
181 .expect("CardTraitBase host_card_id must be bound before use")
182 }
183
184 pub fn get_host_card_id(&self) -> Option<CardId> {
185 self.host_card_id
186 }
187
188 pub fn set_host_card_id(&mut self, id: CardId) {
189 self.host_card_id = Some(id);
190 }
191
192 pub fn host_card<'a>(&self, game: &'a GameState) -> &'a Card {
193 game.card(self.host_card_id())
194 }
195
196 pub fn host_controller(&self, game: &GameState) -> PlayerId {
197 self.host_card(game).controller
198 }
199
200 pub fn is_keyword(&self, kw: Keyword) -> bool {
203 self.keyword
204 .as_ref()
205 .map(|current| current.get_keyword() == kw)
206 .unwrap_or(false)
207 }
208
209 pub fn get_keyword(&self) -> Option<&KeywordInterface> {
210 self.keyword.as_ref()
211 }
212
213 pub fn set_keyword(&mut self, kw: KeywordInterface) {
214 self.keyword = Some(kw);
215 }
216
217 pub fn is_embalm(&self) -> bool {
218 self.is_keyword(Keyword::Embalm)
219 }
220
221 pub fn is_eternalize(&self) -> bool {
222 self.is_keyword(Keyword::Eternalize)
223 }
224
225 pub fn is_secondary(&self) -> bool {
228 self.get_param_or_default("Secondary", "False") == "True"
229 }
230
231 pub fn is_class_ability(&self) -> bool {
232 self.has_param("ClassLevel")
233 }
234
235 pub fn is_class_level_n_ability(&self, level: i32) -> bool {
236 let raw = self.get_param_or_default("ClassLevel", "0");
237 let numeric = if raw.chars().all(|c| c.is_ascii_digit()) {
238 raw
239 } else {
240 &raw[2..]
242 };
243 numeric.parse::<i32>().map(|n| n == level).unwrap_or(false)
244 }
245
246 pub fn is_mana_ability(&self) -> bool {
248 false
249 }
250
251 pub fn matches_valid_with_player(
258 &self,
259 target: &MatchValidTarget<'_>,
260 valids: &[&str],
261 src_card: &Card,
262 src_player: PlayerId,
263 ) -> bool {
264 match target {
265 MatchValidTarget::Card(card) => {
266 let selector = crate::parsing::cached_compiled_selector(&valids.join(","));
267 valid_filter::matches_valid_card_selector(&selector, card, src_card)
268 }
269 MatchValidTarget::Player(player) => {
270 valid_filter::matches_valid_player(&valids.join(","), *player, src_player)
271 }
272 MatchValidTarget::GameObj(obj) => {
273 let owned: Vec<String> = valids.iter().map(|s| s.to_string()).collect();
274 obj.is_valid(&owned, src_player, src_card, self)
275 }
276 MatchValidTarget::Iter(items) => items
277 .iter()
278 .any(|item| self.matches_valid_with_player(item, valids, src_card, src_player)),
279 MatchValidTarget::Str(s) => valids.contains(s),
280 MatchValidTarget::LossReason(reason) => valids.iter().any(|v| {
281 GameLossReason::smart_value_of(v)
282 .map(|parsed| parsed == *reason)
283 .unwrap_or(false)
284 }),
285 MatchValidTarget::PlanarDice => {
286 unimplemented!("port: PlanarDice — planechase not implemented")
287 }
288 }
289 }
290
291 pub fn matches_compiled_valid_with_player(
292 &self,
293 target: &MatchValidTarget<'_>,
294 selector: &CompiledSelector,
295 src_card: &Card,
296 src_player: PlayerId,
297 ) -> bool {
298 match target {
299 MatchValidTarget::Card(card) => {
300 valid_filter::matches_valid_card_selector(selector, card, src_card)
301 }
302 MatchValidTarget::Player(player) => {
303 valid_filter::matches_valid_player_selector(selector, *player, src_player)
304 }
305 MatchValidTarget::GameObj(obj) => {
306 let owned: Vec<String> = selector
307 .alternatives
308 .iter()
309 .map(|alternative| alternative.raw.clone())
310 .collect();
311 obj.is_valid(&owned, src_player, src_card, self)
312 }
313 MatchValidTarget::Iter(items) => items.iter().any(|item| {
314 self.matches_compiled_valid_with_player(item, selector, src_card, src_player)
315 }),
316 MatchValidTarget::Str(s) => selector
317 .alternatives
318 .iter()
319 .any(|alternative| alternative.raw == *s),
320 MatchValidTarget::LossReason(reason) => {
321 selector.alternatives.iter().any(|alternative| {
322 GameLossReason::smart_value_of(&alternative.raw)
323 .map(|parsed| parsed == *reason)
324 .unwrap_or(false)
325 })
326 }
327 MatchValidTarget::PlanarDice => {
328 unimplemented!("port: PlanarDice — planechase not implemented")
329 }
330 }
331 }
332
333 pub fn set_suppressed(&mut self, supp: bool) {
336 self.suppressed = supp;
337 }
338
339 pub fn is_suppressed(&self) -> bool {
340 self.suppressed
341 }
342
343 pub fn get_card_view(&self) -> ! {
347 unimplemented!("port: CardView — UI layer, not in Rust engine")
348 }
349
350 fn get_svar_fallback(&self, name: Option<&str>) -> Vec<&dyn HasSVars> {
355 let mut result: Vec<&dyn HasSVars> = Vec::new();
356
357 if let Some(kw) = self.keyword.as_ref() {
358 if let Some(st) = kw.get_static() {
359 let include = match name {
361 None => true,
362 Some(n) => kw.get_original().contains(n),
363 };
364 if include {
365 result.push(st);
366 }
367 }
368 }
369 result
370 }
371
372 fn find_svar(&self, name: &str) -> Option<&dyn HasSVars> {
373 self.get_svar_fallback(Some(name))
374 .into_iter()
375 .find(|src| HasSVars::has_svar(*src, name))
376 }
377
378 pub fn get_svar_int(&self, name: &str) -> Option<i32> {
379 let value = HasSVars::get_svar(self, name)?;
380 value.parse::<i32>().ok()
381 }
382
383 pub fn get_all_svars(&self) -> HashMap<String, String> {
386 let mut res: HashMap<String, String> = HashMap::new();
387 for src in self.get_svar_fallback(None) {
388 for (k, v) in HasSVars::get_svars(src) {
389 res.insert(k.clone(), v.clone());
390 }
391 }
392 for (k, v) in &self.svars {
393 res.insert(k.clone(), v.clone());
394 }
395 res
396 }
397
398 pub fn set_card_state(&mut self, state: &CardState) {
401 self.card_state_name = Some(state.get_state_name());
402 for (key, value) in HasSVars::get_svars(state) {
403 self.svars
404 .entry(key.clone())
405 .or_insert_with(|| value.clone());
406 }
407 }
408
409 pub fn get_card_state_name(&self) -> Option<CardStateName> {
410 self.card_state_name
411 }
412
413 pub fn get_host_name<'a>(
418 &'a self,
419 game: &'a GameState,
420 node: &'a CardTraitBase,
421 ) -> HostName<'a> {
422 if node.is_intrinsic() {
423 if let Some(_state_name) = node.card_state_name {
427 unimplemented!(
428 "port: Card::get_current_state_name — required by CardTraitBase::get_host_name"
429 );
430 }
431 }
432 HostName::Card(node.host_card(game))
433 }
434
435 pub fn is_copied_trait(&self) -> bool {
436 false
437 }
438
439 pub fn get_changed_text_colors(&self) -> HashMap<String, String> {
442 combine_changed_map(
443 &self.intrinsic_changed_text_colors,
444 &self.changed_text_colors,
445 )
446 }
447
448 pub fn get_changed_text_types(&self) -> HashMap<String, String> {
449 combine_changed_map(&self.intrinsic_changed_text_types, &self.changed_text_types)
450 }
451
452 pub fn changed_text_pairs(&self) -> Vec<(String, String)> {
456 self.changed_text_colors
457 .iter()
458 .chain(self.changed_text_types.iter())
459 .map(|(from, to)| (from.clone(), to.clone()))
460 .collect()
461 }
462
463 pub fn change_text_intrinsic(
464 &mut self,
465 color_map: HashMap<String, String>,
466 type_map: HashMap<String, String>,
467 ) {
468 self.intrinsic_changed_text_colors = color_map.clone();
469 self.intrinsic_changed_text_types = type_map.clone();
470
471 let color_tree: BTreeMap<String, String> = color_map.into_iter().collect();
472 let type_tree: BTreeMap<String, String> = type_map.into_iter().collect();
473
474 let keys: Vec<String> = self.map_params.keys().cloned().collect();
475 for key in keys {
476 let Some(value) = self.original_map_params.get(&key).cloned() else {
477 continue;
478 };
479 let new_value = if NO_CHANGE_KEYS.contains(&key.as_str()) {
480 continue;
481 } else if DESCRIPTIVE_KEYS.contains(&key.as_str()) {
482 Some(apply_text_change_effects(
483 &value,
484 true,
485 &color_tree,
486 &type_tree,
487 ))
488 } else if self.svars.contains_key(&value) {
489 continue;
491 } else {
492 Some(apply_text_change_effects(
493 &value,
494 false,
495 &color_tree,
496 &type_tree,
497 ))
498 };
499
500 if let Some(nv) = new_value {
501 self.map_params.insert(key, nv);
502 }
503 }
504 self.original_map_params = self.map_params.clone();
506 }
507
508 pub fn change_text(&mut self) {
509 unimplemented!(
515 "port: Card::get_changed_text_color_words / _type_words — \
516 required by CardTraitBase::change_text"
517 );
518 }
519
520 pub fn copy_helper(&self, copy: &mut CardTraitBase, host: Card) {
523 self.copy_helper_with_text(copy, host, false);
524 }
525
526 pub fn copy_helper_with_text(
527 &self,
528 copy: &mut CardTraitBase,
529 host: Card,
530 keep_text_changes: bool,
531 ) {
532 copy.original_map_params = self.original_map_params.clone();
533 copy.map_params = if keep_text_changes {
534 self.map_params.clone()
535 } else {
536 self.original_map_params.clone()
537 };
538 copy.set_svars(self.svars.clone());
539 copy.card_state_name = self.card_state_name;
540 copy.host_card_id = Some(host.id);
542 copy.keyword = self.keyword.clone();
543 }
544
545 pub fn get_trigger_remembered(&self) -> Vec<AbilityValue> {
552 Vec::new()
553 }
554}
555
556fn combine_changed_map(
559 input: &HashMap<String, String>,
560 output: &HashMap<String, String>,
561) -> HashMap<String, String> {
562 if input.is_empty() {
563 return output.clone();
564 }
565 if output.is_empty() {
566 return input.clone();
567 }
568 let mut result = output.clone();
569 for (k, v) in input {
570 let replacement = output.get(v).cloned().unwrap_or_else(|| v.clone());
571 result.insert(k.clone(), replacement);
572 }
573 result
574}
575
576impl Identifiable for CardTraitBase {
577 fn id(&self) -> i32 {
578 self.id
579 }
580}
581
582impl HasSVars for CardTraitBase {
583 fn get_svar(&self, name: &str) -> Option<&str> {
584 if let Some(v) = self.svars.get(name) {
585 return Some(v.as_str());
586 }
587 if let Some(kw) = self.keyword.as_ref() {
595 if let Some(st) = kw.get_static() {
596 if kw.get_original().contains(name) {
597 if let Some(v) = HasSVars::get_svar(st, name) {
598 return Some(v);
599 }
600 }
601 }
602 }
603 None
604 }
605
606 fn has_svar(&self, name: &str) -> bool {
607 self.svars.contains_key(name) || self.find_svar(name).is_some()
608 }
609
610 fn set_svar(&mut self, name: String, value: String) {
611 self.svars.insert(name, value);
612 }
613
614 fn set_svars(&mut self, new_svars: HashMap<String, String>) {
615 self.svars = new_svars;
616 }
617
618 fn get_svars(&self) -> &HashMap<String, String> {
619 &self.svars
620 }
621
622 fn remove_svar(&mut self, var: &str) {
623 self.svars.remove(var);
624 }
625}
626
627impl GameObject for CardTraitBase {}
628
629pub trait CardTrait {
637 fn base(&self) -> &CardTraitBase;
640
641 fn resolve_source_player(&self, src_card: &Card) -> PlayerId {
646 src_card.controller
647 }
648
649 fn matches_valid(
651 &self,
652 target: &MatchValidTarget<'_>,
653 valids: &[&str],
654 src_card: Option<&Card>,
655 ) -> bool {
656 let Some(src) = src_card else {
657 return false;
658 };
659 let player = self.resolve_source_player(src);
660 self.base()
661 .matches_valid_with_player(target, valids, src, player)
662 }
663
664 fn matches_compiled_valid(
665 &self,
666 target: &MatchValidTarget<'_>,
667 selector: &CompiledSelector,
668 src_card: Option<&Card>,
669 ) -> bool {
670 let Some(src) = src_card else {
671 return false;
672 };
673 let player = self.resolve_source_player(src);
674 self.base()
675 .matches_compiled_valid_with_player(target, selector, src, player)
676 }
677
678 fn matches_valid_param(
679 &self,
680 param: &str,
681 target: &MatchValidTarget<'_>,
682 src_card: Option<&Card>,
683 ) -> bool {
684 let b = self.base();
685 let invert_key = format!("Invert{param}");
686 let invert = b.has_param(&invert_key);
687 if b.has_param(param) {
688 let raw = b.get_param(param).unwrap_or("");
689 let parts: Vec<&str> = raw.split(',').collect();
690 if !self.matches_valid(target, &parts, src_card) {
691 return invert;
692 }
693 }
694 !invert
695 }
696
697 fn matches_valid_card(&self, expr: &str, card: &Card, source: &Card) -> bool {
702 let parts: Vec<&str> = expr.split(',').collect();
703 self.matches_valid(&MatchValidTarget::Card(card), &parts, Some(source))
704 }
705
706 fn matches_compiled_valid_card(
707 &self,
708 selector: &CompiledSelector,
709 card: &Card,
710 source: &Card,
711 ) -> bool {
712 self.matches_compiled_valid(&MatchValidTarget::Card(card), selector, Some(source))
713 }
714
715 fn matches_valid_player(&self, expr: &str, player: PlayerId, source: &Card) -> bool {
718 let parts: Vec<&str> = expr.split(',').collect();
719 self.matches_valid(&MatchValidTarget::Player(player), &parts, Some(source))
720 }
721
722 fn matches_compiled_valid_player(
723 &self,
724 selector: &CompiledSelector,
725 player: PlayerId,
726 source: &Card,
727 ) -> bool {
728 self.matches_compiled_valid(&MatchValidTarget::Player(player), selector, Some(source))
729 }
730}
731
732impl CardTrait for CardTraitBase {
733 fn base(&self) -> &CardTraitBase {
734 self
735 }
736}
737
738pub trait CardTraitIrOwner {
750 type Ir;
751
752 fn ir(&self) -> &Self::Ir;
753
754 fn card_trait_requirements(&self) -> &CardTraitRequirementsIr;
755
756 fn meets_card_trait_requirements(
757 &self,
758 game: &GameState,
759 source: &Card,
760 svar_source: &dyn HasSVars,
761 ) -> bool {
762 self.card_trait_requirements()
763 .meets(game, source, svar_source)
764 }
765}