Files
FFCardGame/scripts/game/abilities/FieldEffectManager.gd
2026-02-02 16:28:53 -05:00

682 lines
21 KiB
GDScript

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