class_name FieldEffectManager extends RefCounted ## FieldEffectManager - Manages continuous FIELD abilities ## Tracks active field effects and calculates their impact on the game state # Active field abilities by source card instance_id var _active_abilities: Dictionary = {} # instance_id -> Array of abilities ## Register field abilities when a card enters the field func register_field_abilities(card: CardInstance, abilities: Array) -> void: var field_abilities: Array = [] for ability in abilities: var parsed = ability.get("parsed", {}) if parsed.get("type") == "FIELD": field_abilities.append({ "ability": ability, "source": card }) if not field_abilities.is_empty(): _active_abilities[card.instance_id] = field_abilities ## Unregister field abilities when a card leaves the field func unregister_field_abilities(card: CardInstance) -> void: _active_abilities.erase(card.instance_id) ## Get total power modifier for a card from all active field effects func get_power_modifiers(card: CardInstance, game_state) -> int: var total_modifier: int = 0 for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "POWER_MOD": if _card_matches_effect_target(card, effect, source, game_state): total_modifier += effect.get("amount", 0) return total_modifier ## Check if a card has a keyword granted by field effects func has_keyword(card: CardInstance, keyword: String, game_state) -> bool: var keyword_upper = keyword.to_upper() for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "KEYWORD": var granted_keyword = str(effect.get("keyword", "")).to_upper() if granted_keyword == keyword_upper: if _card_matches_effect_target(card, effect, source, game_state): return true return false ## Get all keywords granted to a card by field effects func get_granted_keywords(card: CardInstance, game_state) -> Array: var keywords: Array = [] for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "KEYWORD": if _card_matches_effect_target(card, effect, source, game_state): var keyword = effect.get("keyword", "") if keyword and keyword not in keywords: keywords.append(keyword) return keywords ## Check if a card has protection from something via field effects func has_protection(card: CardInstance, protection_type: String, game_state) -> bool: var protection_upper = protection_type.to_upper() for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "PROTECTION": var from = str(effect.get("from", "")).to_upper() if from == protection_upper or from == "ALL": if _card_matches_effect_target(card, effect, source, game_state): return true return false ## Check if a card is affected by a damage modifier func get_damage_modifier(card: CardInstance, game_state) -> int: var total_modifier: int = 0 for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "DAMAGE_MODIFIER": if _card_matches_effect_target(card, effect, source, game_state): total_modifier += effect.get("amount", 0) return total_modifier ## Check if a card matches an effect's target specification func _card_matches_effect_target( card: CardInstance, effect: Dictionary, source: CardInstance, game_state ) -> bool: var target = effect.get("target", {}) if target.is_empty(): # No target specified, assume applies to source only return card == source var target_type = str(target.get("type", "")).to_upper() # Check owner var owner = str(target.get("owner", "ANY")).to_upper() match owner: "CONTROLLER": if card.controller_index != source.controller_index: return false "OPPONENT": if card.controller_index == source.controller_index: return false # "ANY" matches all # Check if applies to self if target_type == "SELF": return card == source # Check if applies to all matching if target_type == "ALL": return _matches_filter(card, target.get("filter", {}), source) # Default check filter return _matches_filter(card, target.get("filter", {}), source) ## Check if a card matches a filter (duplicated from TargetSelector for independence) func _matches_filter( card: CardInstance, filter: Dictionary, source: CardInstance ) -> bool: if filter.is_empty(): return true # Card type filter if filter.has("card_type"): var type_str = str(filter.card_type).to_upper() match type_str: "FORWARD": if not card.is_forward(): return false "BACKUP": if not card.is_backup(): return false "SUMMON": if not card.is_summon(): return false "CHARACTER": if not (card.is_forward() or card.is_backup()): return false # Element filter if filter.has("element"): var element_str = str(filter.element).to_upper() var element = Enums.element_from_string(element_str) if element not in card.get_elements(): return false # Cost filters if filter.has("cost_min") and card.card_data.cost < int(filter.cost_min): return false if filter.has("cost_max") and card.card_data.cost > int(filter.cost_max): return false if filter.has("cost") and card.card_data.cost != int(filter.cost): return false # Power filters if filter.has("power_min") and card.get_power() < int(filter.power_min): return false if filter.has("power_max") and card.get_power() > int(filter.power_max): return false # Name filter if filter.has("name") and card.card_data.name != filter.name: return false # Category filter if filter.has("category") and card.card_data.category != filter.category: return false # Job filter if filter.has("job") and card.card_data.job != filter.job: return false # Exclude self if filter.get("exclude_self", false) and card == source: return false return true ## Get count of active field abilities func get_active_ability_count() -> int: var count = 0 for instance_id in _active_abilities: count += _active_abilities[instance_id].size() return count ## Clear all active abilities (for game reset) func clear_all() -> void: _active_abilities.clear() # ============================================================================= # BLOCK IMMUNITY CHECKS # ============================================================================= ## Check if a card has block immunity (can't be blocked by certain cards) func has_block_immunity(card: CardInstance, potential_blocker: CardInstance, game_state) -> bool: for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var source = ability_data.source if source != card: continue var ability = ability_data.ability var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "BLOCK_IMMUNITY": var condition = effect.get("condition", {}) if _blocker_matches_immunity_condition(potential_blocker, condition, card): return true return false ## Check if blocker matches the immunity condition func _blocker_matches_immunity_condition( blocker: CardInstance, condition: Dictionary, attacker: CardInstance ) -> bool: if condition.is_empty(): return true # Unconditional block immunity var comparison = condition.get("comparison", "") var attribute = condition.get("attribute", "") var value = condition.get("value", 0) var compare_to = condition.get("compare_to", "") var blocker_value = 0 match attribute: "cost": blocker_value = blocker.card_data.cost if blocker.card_data else 0 "power": blocker_value = blocker.get_power() var compare_value = value if compare_to == "SELF_POWER": compare_value = attacker.get_power() match comparison: "GTE": return blocker_value >= compare_value "GT": return blocker_value > compare_value "LTE": return blocker_value <= compare_value "LT": return blocker_value < compare_value "EQ": return blocker_value == compare_value return false # ============================================================================= # ATTACK RESTRICTION CHECKS # ============================================================================= ## Check if a card has attack restrictions func has_attack_restriction(card: CardInstance, game_state) -> bool: for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "RESTRICTION": var restriction = effect.get("restriction", "") if restriction in ["CANNOT_ATTACK", "CANNOT_ATTACK_OR_BLOCK"]: if _card_matches_effect_target(card, effect, source, game_state): return true return false ## Check if a card has block restrictions func has_block_restriction(card: CardInstance, game_state) -> bool: for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "RESTRICTION": var restriction = effect.get("restriction", "") if restriction in ["CANNOT_BLOCK", "CANNOT_ATTACK_OR_BLOCK"]: if _card_matches_effect_target(card, effect, source, game_state): return true return false # ============================================================================= # TAUNT CHECKS (Must be targeted if possible) # ============================================================================= ## Get cards that must be targeted by opponent's abilities if possible func get_taunt_targets(player_index: int, game_state) -> Array: var taunt_cards: Array = [] for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "TAUNT": var target = effect.get("target", {}) if target.get("type") == "SELF": if source.controller_index == player_index: taunt_cards.append(source) return taunt_cards # ============================================================================= # COST MODIFICATION # ============================================================================= ## Get cost modifier for playing a card func get_cost_modifier( card_to_play: CardInstance, playing_player: int, game_state ) -> int: var total_modifier = 0 for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): var effect_type = effect.get("type", "") if effect_type == "COST_REDUCTION": if _cost_effect_applies(effect, card_to_play, playing_player, source, game_state): total_modifier -= effect.get("amount", 0) elif effect_type == "COST_REDUCTION_SCALING": if _cost_effect_applies(effect, card_to_play, playing_player, source, game_state): var reduction = _calculate_scaling_cost_reduction(effect, source, game_state) total_modifier -= reduction elif effect_type == "COST_INCREASE": if _cost_effect_applies(effect, card_to_play, playing_player, source, game_state): total_modifier += effect.get("amount", 0) return total_modifier ## Check if a cost modification effect applies to a card being played func _cost_effect_applies( effect: Dictionary, card: CardInstance, player: int, source: CardInstance, game_state ) -> bool: var for_player = effect.get("for_player", "CONTROLLER") # Check if effect applies to this player match for_player: "CONTROLLER": if player != source.controller_index: return false "OPPONENT": if player == source.controller_index: return false # Check card filter var card_filter = effect.get("card_filter", "") if card_filter and not _card_matches_name_filter(card, card_filter): return false # Check condition var condition = effect.get("condition", {}) if not condition.is_empty(): if not _cost_condition_met(condition, source, game_state): return false return true ## Check if a card name matches a filter func _card_matches_name_filter(card: CardInstance, filter_text: String) -> bool: if not card or not card.card_data: return false var filter_lower = filter_text.to_lower() var card_name = card.card_data.name.to_lower() # Direct name match if card_name in filter_lower or filter_lower in card_name: return true return false ## Check if a cost condition is met func _cost_condition_met(condition: Dictionary, source: CardInstance, game_state) -> bool: if condition.has("control_card_name"): var name_to_find = condition.control_card_name.to_lower() var player = game_state.get_player(source.controller_index) if player: for card in player.field_forwards.get_cards(): if name_to_find in card.card_data.name.to_lower(): return true for card in player.field_backups.get_cards(): if name_to_find in card.card_data.name.to_lower(): return true return false if condition.has("control_category"): var category = condition.control_category.to_lower() var player = game_state.get_player(source.controller_index) if player: for card in player.field_forwards.get_cards(): if category in card.card_data.category.to_lower(): return true for card in player.field_backups.get_cards(): if category in card.card_data.category.to_lower(): return true return false return true # ============================================================================= # SCALING COST REDUCTION # ============================================================================= ## Calculate cost reduction for a COST_REDUCTION_SCALING effect func _calculate_scaling_cost_reduction( effect: Dictionary, source: CardInstance, game_state ) -> int: var reduction_per = effect.get("reduction_per", 1) var scale_by = str(effect.get("scale_by", "")).to_upper() var scale_filter = effect.get("scale_filter", {}) # Get scale value using similar logic to EffectResolver var scale_value = _get_scale_value(scale_by, source, game_state, scale_filter) return scale_value * reduction_per ## Get scale value based on scale_by type (with optional filter) ## Mirrors the logic in EffectResolver for consistency func _get_scale_value( scale_by: String, source: CardInstance, game_state, scale_filter: Dictionary = {} ) -> int: if not source or not game_state: return 0 var player_index = source.controller_index var player = game_state.get_player(player_index) if not player: return 0 # Determine owner from filter (default to CONTROLLER) var owner = scale_filter.get("owner", "CONTROLLER").to_upper() if scale_filter else "CONTROLLER" # Get cards based on scale_by and owner var cards_to_count: Array = [] match scale_by: "DAMAGE_RECEIVED": # Special case - not card-based return _get_damage_for_owner(owner, player_index, game_state) "FORWARDS_CONTROLLED", "FORWARDS": cards_to_count = _get_forwards_for_owner(owner, player_index, game_state) "BACKUPS_CONTROLLED", "BACKUPS": cards_to_count = _get_backups_for_owner(owner, player_index, game_state) "FIELD_CARDS_CONTROLLED", "FIELD_CARDS": cards_to_count = _get_field_cards_for_owner(owner, player_index, game_state) "CARDS_IN_HAND": cards_to_count = _get_hand_for_owner(owner, player_index, game_state) "CARDS_IN_BREAK_ZONE": cards_to_count = _get_break_zone_for_owner(owner, player_index, game_state) "OPPONENT_FORWARDS": cards_to_count = _get_forwards_for_owner("OPPONENT", player_index, game_state) "OPPONENT_BACKUPS": cards_to_count = _get_backups_for_owner("OPPONENT", player_index, game_state) _: push_warning("FieldEffectManager: Unknown scale_by type: " + scale_by) return 0 # If no filter, just return count if not scale_filter or scale_filter.is_empty() or (scale_filter.size() == 1 and scale_filter.has("owner")): return cards_to_count.size() # Apply filter and count matching cards using CardFilter utility return CardFilter.count_matching(cards_to_count, scale_filter) # ============================================================================= # OWNER-BASED ACCESS HELPERS FOR SCALING # ============================================================================= func _get_forwards_for_owner(owner: String, player_index: int, game_state) -> Array: match owner.to_upper(): "CONTROLLER": var player = game_state.get_player(player_index) return player.field_forwards.get_cards() if player and player.field_forwards else [] "OPPONENT": var opponent = game_state.get_player(1 - player_index) return opponent.field_forwards.get_cards() if opponent and opponent.field_forwards else [] _: var all_cards = [] for p in game_state.players: if p and p.field_forwards: all_cards.append_array(p.field_forwards.get_cards()) return all_cards func _get_backups_for_owner(owner: String, player_index: int, game_state) -> Array: match owner.to_upper(): "CONTROLLER": var player = game_state.get_player(player_index) return player.field_backups.get_cards() if player and player.field_backups else [] "OPPONENT": var opponent = game_state.get_player(1 - player_index) return opponent.field_backups.get_cards() if opponent and opponent.field_backups else [] _: var all_cards = [] for p in game_state.players: if p and p.field_backups: all_cards.append_array(p.field_backups.get_cards()) return all_cards func _get_field_cards_for_owner(owner: String, player_index: int, game_state) -> Array: var cards = [] cards.append_array(_get_forwards_for_owner(owner, player_index, game_state)) cards.append_array(_get_backups_for_owner(owner, player_index, game_state)) return cards func _get_hand_for_owner(owner: String, player_index: int, game_state) -> Array: match owner.to_upper(): "CONTROLLER": var player = game_state.get_player(player_index) return player.hand.get_cards() if player and player.hand else [] "OPPONENT": var opponent = game_state.get_player(1 - player_index) return opponent.hand.get_cards() if opponent and opponent.hand else [] _: var all_cards = [] for p in game_state.players: if p and p.hand: all_cards.append_array(p.hand.get_cards()) return all_cards func _get_break_zone_for_owner(owner: String, player_index: int, game_state) -> Array: match owner.to_upper(): "CONTROLLER": var player = game_state.get_player(player_index) return player.break_zone.get_cards() if player and player.break_zone else [] "OPPONENT": var opponent = game_state.get_player(1 - player_index) return opponent.break_zone.get_cards() if opponent and opponent.break_zone else [] _: var all_cards = [] for p in game_state.players: if p and p.break_zone: all_cards.append_array(p.break_zone.get_cards()) return all_cards func _get_damage_for_owner(owner: String, player_index: int, game_state) -> int: match owner.to_upper(): "CONTROLLER": var player = game_state.get_player(player_index) return player.damage if player and "damage" in player else 0 "OPPONENT": var opponent = game_state.get_player(1 - player_index) return opponent.damage if opponent and "damage" in opponent else 0 _: var total = 0 for p in game_state.players: if p and "damage" in p: total += p.damage return total # ============================================================================= # MULTI-ATTACK CHECKS # ============================================================================= ## Get maximum attacks allowed for a card this turn func get_max_attacks(card: CardInstance, game_state) -> int: var max_attacks = 1 # Default is 1 attack per turn for instance_id in _active_abilities: var abilities = _active_abilities[instance_id] for ability_data in abilities: var ability = ability_data.ability var source = ability_data.source var parsed = ability.get("parsed", {}) for effect in parsed.get("effects", []): if effect.get("type") == "MULTI_ATTACK": if _card_matches_effect_target(card, effect, source, game_state): var attack_count = effect.get("attack_count", 1) if attack_count > max_attacks: max_attacks = attack_count return max_attacks