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 set_host_card_id(&mut self, id: CardId) {
185 self.host_card_id = Some(id);
186 }
187
188 pub fn host_card<'a>(&self, game: &'a GameState) -> &'a Card {
189 game.card(self.host_card_id())
190 }
191
192 pub fn host_controller(&self, game: &GameState) -> PlayerId {
193 self.host_card(game).controller
194 }
195
196 pub fn is_keyword(&self, kw: Keyword) -> bool {
199 self.keyword
200 .as_ref()
201 .map(|current| current.get_keyword() == kw)
202 .unwrap_or(false)
203 }
204
205 pub fn get_keyword(&self) -> Option<&KeywordInterface> {
206 self.keyword.as_ref()
207 }
208
209 pub fn set_keyword(&mut self, kw: KeywordInterface) {
210 self.keyword = Some(kw);
211 }
212
213 pub fn is_embalm(&self) -> bool {
214 self.is_keyword(Keyword::Embalm)
215 }
216
217 pub fn is_eternalize(&self) -> bool {
218 self.is_keyword(Keyword::Eternalize)
219 }
220
221 pub fn is_secondary(&self) -> bool {
224 self.get_param_or_default("Secondary", "False") == "True"
225 }
226
227 pub fn is_class_ability(&self) -> bool {
228 self.has_param("ClassLevel")
229 }
230
231 pub fn is_class_level_n_ability(&self, level: i32) -> bool {
232 let raw = self.get_param_or_default("ClassLevel", "0");
233 let numeric = if raw.chars().all(|c| c.is_ascii_digit()) {
234 raw
235 } else {
236 &raw[2..]
238 };
239 numeric.parse::<i32>().map(|n| n == level).unwrap_or(false)
240 }
241
242 pub fn is_mana_ability(&self) -> bool {
244 false
245 }
246
247 pub fn matches_valid_with_player(
254 &self,
255 target: &MatchValidTarget<'_>,
256 valids: &[&str],
257 src_card: &Card,
258 src_player: PlayerId,
259 ) -> bool {
260 match target {
261 MatchValidTarget::Card(card) => {
262 let selector = crate::parsing::cached_compiled_selector(&valids.join(","));
263 valid_filter::matches_valid_card_selector(&selector, card, src_card)
264 }
265 MatchValidTarget::Player(player) => {
266 valid_filter::matches_valid_player(&valids.join(","), *player, src_player)
267 }
268 MatchValidTarget::GameObj(obj) => {
269 let owned: Vec<String> = valids.iter().map(|s| s.to_string()).collect();
270 obj.is_valid(&owned, src_player, src_card, self)
271 }
272 MatchValidTarget::Iter(items) => items
273 .iter()
274 .any(|item| self.matches_valid_with_player(item, valids, src_card, src_player)),
275 MatchValidTarget::Str(s) => valids.contains(s),
276 MatchValidTarget::LossReason(reason) => valids.iter().any(|v| {
277 GameLossReason::smart_value_of(v)
278 .map(|parsed| parsed == *reason)
279 .unwrap_or(false)
280 }),
281 MatchValidTarget::PlanarDice => {
282 unimplemented!("port: PlanarDice — planechase not implemented")
283 }
284 }
285 }
286
287 pub fn matches_compiled_valid_with_player(
288 &self,
289 target: &MatchValidTarget<'_>,
290 selector: &CompiledSelector,
291 src_card: &Card,
292 src_player: PlayerId,
293 ) -> bool {
294 match target {
295 MatchValidTarget::Card(card) => {
296 valid_filter::matches_valid_card_selector(selector, card, src_card)
297 }
298 MatchValidTarget::Player(player) => {
299 valid_filter::matches_valid_player_selector(selector, *player, src_player)
300 }
301 MatchValidTarget::GameObj(obj) => {
302 let owned: Vec<String> = selector
303 .alternatives
304 .iter()
305 .map(|alternative| alternative.raw.clone())
306 .collect();
307 obj.is_valid(&owned, src_player, src_card, self)
308 }
309 MatchValidTarget::Iter(items) => items.iter().any(|item| {
310 self.matches_compiled_valid_with_player(item, selector, src_card, src_player)
311 }),
312 MatchValidTarget::Str(s) => selector
313 .alternatives
314 .iter()
315 .any(|alternative| alternative.raw == *s),
316 MatchValidTarget::LossReason(reason) => {
317 selector.alternatives.iter().any(|alternative| {
318 GameLossReason::smart_value_of(&alternative.raw)
319 .map(|parsed| parsed == *reason)
320 .unwrap_or(false)
321 })
322 }
323 MatchValidTarget::PlanarDice => {
324 unimplemented!("port: PlanarDice — planechase not implemented")
325 }
326 }
327 }
328
329 pub fn set_suppressed(&mut self, supp: bool) {
332 self.suppressed = supp;
333 }
334
335 pub fn is_suppressed(&self) -> bool {
336 self.suppressed
337 }
338
339 pub fn get_card_view(&self) -> ! {
343 unimplemented!("port: CardView — UI layer, not in Rust engine")
344 }
345
346 fn get_svar_fallback(&self, name: Option<&str>) -> Vec<&dyn HasSVars> {
351 let mut result: Vec<&dyn HasSVars> = Vec::new();
352
353 if let Some(kw) = self.keyword.as_ref() {
354 if let Some(st) = kw.get_static() {
355 let include = match name {
357 None => true,
358 Some(n) => kw.get_original().contains(n),
359 };
360 if include {
361 result.push(st);
362 }
363 }
364 }
365 result
366 }
367
368 fn find_svar(&self, name: &str) -> Option<&dyn HasSVars> {
369 self.get_svar_fallback(Some(name))
370 .into_iter()
371 .find(|src| HasSVars::has_svar(*src, name))
372 }
373
374 pub fn get_svar_int(&self, name: &str) -> Option<i32> {
375 let value = HasSVars::get_svar(self, name)?;
376 value.parse::<i32>().ok()
377 }
378
379 pub fn get_all_svars(&self) -> HashMap<String, String> {
382 let mut res: HashMap<String, String> = HashMap::new();
383 for src in self.get_svar_fallback(None) {
384 for (k, v) in HasSVars::get_svars(src) {
385 res.insert(k.clone(), v.clone());
386 }
387 }
388 for (k, v) in &self.svars {
389 res.insert(k.clone(), v.clone());
390 }
391 res
392 }
393
394 pub fn set_card_state(&mut self, state: &CardState) {
397 self.card_state_name = Some(state.get_state_name());
398 for (key, value) in HasSVars::get_svars(state) {
399 self.svars
400 .entry(key.clone())
401 .or_insert_with(|| value.clone());
402 }
403 }
404
405 pub fn get_card_state_name(&self) -> Option<CardStateName> {
406 self.card_state_name
407 }
408
409 pub fn get_host_name<'a>(
414 &'a self,
415 game: &'a GameState,
416 node: &'a CardTraitBase,
417 ) -> HostName<'a> {
418 if node.is_intrinsic() {
419 if let Some(_state_name) = node.card_state_name {
423 unimplemented!(
424 "port: Card::get_current_state_name — required by CardTraitBase::get_host_name"
425 );
426 }
427 }
428 HostName::Card(node.host_card(game))
429 }
430
431 pub fn is_copied_trait(&self) -> bool {
432 false
433 }
434
435 pub fn get_changed_text_colors(&self) -> HashMap<String, String> {
438 combine_changed_map(
439 &self.intrinsic_changed_text_colors,
440 &self.changed_text_colors,
441 )
442 }
443
444 pub fn get_changed_text_types(&self) -> HashMap<String, String> {
445 combine_changed_map(&self.intrinsic_changed_text_types, &self.changed_text_types)
446 }
447
448 pub fn changed_text_pairs(&self) -> Vec<(String, String)> {
452 self.changed_text_colors
453 .iter()
454 .chain(self.changed_text_types.iter())
455 .map(|(from, to)| (from.clone(), to.clone()))
456 .collect()
457 }
458
459 pub fn change_text_intrinsic(
460 &mut self,
461 color_map: HashMap<String, String>,
462 type_map: HashMap<String, String>,
463 ) {
464 self.intrinsic_changed_text_colors = color_map.clone();
465 self.intrinsic_changed_text_types = type_map.clone();
466
467 let color_tree: BTreeMap<String, String> = color_map.into_iter().collect();
468 let type_tree: BTreeMap<String, String> = type_map.into_iter().collect();
469
470 let keys: Vec<String> = self.map_params.keys().cloned().collect();
471 for key in keys {
472 let Some(value) = self.original_map_params.get(&key).cloned() else {
473 continue;
474 };
475 let new_value = if NO_CHANGE_KEYS.contains(&key.as_str()) {
476 continue;
477 } else if DESCRIPTIVE_KEYS.contains(&key.as_str()) {
478 Some(apply_text_change_effects(
479 &value,
480 true,
481 &color_tree,
482 &type_tree,
483 ))
484 } else if self.svars.contains_key(&value) {
485 continue;
487 } else {
488 Some(apply_text_change_effects(
489 &value,
490 false,
491 &color_tree,
492 &type_tree,
493 ))
494 };
495
496 if let Some(nv) = new_value {
497 self.map_params.insert(key, nv);
498 }
499 }
500 self.original_map_params = self.map_params.clone();
502 }
503
504 pub fn change_text(&mut self) {
505 unimplemented!(
511 "port: Card::get_changed_text_color_words / _type_words — \
512 required by CardTraitBase::change_text"
513 );
514 }
515
516 pub fn copy_helper(&self, copy: &mut CardTraitBase, host: Card) {
519 self.copy_helper_with_text(copy, host, false);
520 }
521
522 pub fn copy_helper_with_text(
523 &self,
524 copy: &mut CardTraitBase,
525 host: Card,
526 keep_text_changes: bool,
527 ) {
528 copy.original_map_params = self.original_map_params.clone();
529 copy.map_params = if keep_text_changes {
530 self.map_params.clone()
531 } else {
532 self.original_map_params.clone()
533 };
534 copy.set_svars(self.svars.clone());
535 copy.card_state_name = self.card_state_name;
536 copy.host_card_id = Some(host.id);
538 copy.keyword = self.keyword.clone();
539 }
540
541 pub fn get_trigger_remembered(&self) -> Vec<AbilityValue> {
548 Vec::new()
549 }
550}
551
552fn combine_changed_map(
555 input: &HashMap<String, String>,
556 output: &HashMap<String, String>,
557) -> HashMap<String, String> {
558 if input.is_empty() {
559 return output.clone();
560 }
561 if output.is_empty() {
562 return input.clone();
563 }
564 let mut result = output.clone();
565 for (k, v) in input {
566 let replacement = output.get(v).cloned().unwrap_or_else(|| v.clone());
567 result.insert(k.clone(), replacement);
568 }
569 result
570}
571
572impl Identifiable for CardTraitBase {
573 fn id(&self) -> i32 {
574 self.id
575 }
576}
577
578impl HasSVars for CardTraitBase {
579 fn get_svar(&self, name: &str) -> Option<&str> {
580 if let Some(v) = self.svars.get(name) {
581 return Some(v.as_str());
582 }
583 if let Some(kw) = self.keyword.as_ref() {
591 if let Some(st) = kw.get_static() {
592 if kw.get_original().contains(name) {
593 if let Some(v) = HasSVars::get_svar(st, name) {
594 return Some(v);
595 }
596 }
597 }
598 }
599 None
600 }
601
602 fn has_svar(&self, name: &str) -> bool {
603 self.svars.contains_key(name) || self.find_svar(name).is_some()
604 }
605
606 fn set_svar(&mut self, name: String, value: String) {
607 self.svars.insert(name, value);
608 }
609
610 fn set_svars(&mut self, new_svars: HashMap<String, String>) {
611 self.svars = new_svars;
612 }
613
614 fn get_svars(&self) -> &HashMap<String, String> {
615 &self.svars
616 }
617
618 fn remove_svar(&mut self, var: &str) {
619 self.svars.remove(var);
620 }
621}
622
623impl GameObject for CardTraitBase {}
624
625pub trait CardTrait {
633 fn base(&self) -> &CardTraitBase;
636
637 fn resolve_source_player(&self, src_card: &Card) -> PlayerId {
642 src_card.controller
643 }
644
645 fn matches_valid(
647 &self,
648 target: &MatchValidTarget<'_>,
649 valids: &[&str],
650 src_card: Option<&Card>,
651 ) -> bool {
652 let Some(src) = src_card else {
653 return false;
654 };
655 let player = self.resolve_source_player(src);
656 self.base()
657 .matches_valid_with_player(target, valids, src, player)
658 }
659
660 fn matches_compiled_valid(
661 &self,
662 target: &MatchValidTarget<'_>,
663 selector: &CompiledSelector,
664 src_card: Option<&Card>,
665 ) -> bool {
666 let Some(src) = src_card else {
667 return false;
668 };
669 let player = self.resolve_source_player(src);
670 self.base()
671 .matches_compiled_valid_with_player(target, selector, src, player)
672 }
673
674 fn matches_valid_param(
675 &self,
676 param: &str,
677 target: &MatchValidTarget<'_>,
678 src_card: Option<&Card>,
679 ) -> bool {
680 let b = self.base();
681 let invert_key = format!("Invert{}", param);
682 let invert = b.has_param(&invert_key);
683 if b.has_param(param) {
684 let raw = b.get_param(param).unwrap_or("");
685 let parts: Vec<&str> = raw.split(',').collect();
686 if !self.matches_valid(target, &parts, src_card) {
687 return invert;
688 }
689 }
690 !invert
691 }
692
693 fn matches_valid_card(&self, expr: &str, card: &Card, source: &Card) -> bool {
698 let parts: Vec<&str> = expr.split(',').collect();
699 self.matches_valid(&MatchValidTarget::Card(card), &parts, Some(source))
700 }
701
702 fn matches_compiled_valid_card(
703 &self,
704 selector: &CompiledSelector,
705 card: &Card,
706 source: &Card,
707 ) -> bool {
708 self.matches_compiled_valid(&MatchValidTarget::Card(card), selector, Some(source))
709 }
710
711 fn matches_valid_player(&self, expr: &str, player: PlayerId, source: &Card) -> bool {
714 let parts: Vec<&str> = expr.split(',').collect();
715 self.matches_valid(&MatchValidTarget::Player(player), &parts, Some(source))
716 }
717
718 fn matches_compiled_valid_player(
719 &self,
720 selector: &CompiledSelector,
721 player: PlayerId,
722 source: &Card,
723 ) -> bool {
724 self.matches_compiled_valid(&MatchValidTarget::Player(player), selector, Some(source))
725 }
726}
727
728impl CardTrait for CardTraitBase {
729 fn base(&self) -> &CardTraitBase {
730 self
731 }
732}
733
734pub trait CardTraitIrOwner {
746 type Ir;
747
748 fn ir(&self) -> &Self::Ir;
749
750 fn card_trait_requirements(&self) -> &CardTraitRequirementsIr;
751
752 fn meets_card_trait_requirements(
753 &self,
754 game: &GameState,
755 source: &Card,
756 svar_source: &dyn HasSVars,
757 ) -> bool {
758 self.card_trait_requirements()
759 .meets(game, source, svar_source)
760 }
761}