feature updates
This commit is contained in:
798
scripts/game/abilities/AbilitySystem.gd
Normal file
798
scripts/game/abilities/AbilitySystem.gd
Normal file
@@ -0,0 +1,798 @@
|
||||
class_name AbilitySystem
|
||||
extends Node
|
||||
|
||||
## AbilitySystem - Central coordinator for ability processing
|
||||
## Loads processed abilities and handles trigger matching and effect resolution
|
||||
|
||||
signal ability_triggered(source: CardInstance, ability: Dictionary)
|
||||
signal effect_resolved(effect: Dictionary, targets: Array)
|
||||
signal targeting_required(effect: Dictionary, valid_targets: Array, callback: Callable)
|
||||
signal targeting_completed(effect: Dictionary, selected_targets: Array)
|
||||
signal choice_modal_required(effect: Dictionary, modes: Array, callback: Callable)
|
||||
signal optional_effect_prompt(player_index: int, effect: Dictionary, description: String, callback: Callable)
|
||||
|
||||
const ABILITIES_PATH = "res://data/abilities_processed.json"
|
||||
|
||||
# Loaded ability data
|
||||
var _abilities: Dictionary = {} # card_id -> Array of parsed abilities
|
||||
var _version: String = ""
|
||||
var _stats: Dictionary = {}
|
||||
|
||||
# Sub-systems
|
||||
var trigger_matcher: TriggerMatcher
|
||||
var effect_resolver: EffectResolver
|
||||
var target_selector: TargetSelector
|
||||
var field_effect_manager: FieldEffectManager
|
||||
var condition_checker: ConditionChecker
|
||||
|
||||
# UI Reference
|
||||
var choice_modal: ChoiceModal = null
|
||||
|
||||
# Effect resolution stack
|
||||
var _pending_effects: Array = []
|
||||
var _is_resolving: bool = false
|
||||
var _waiting_for_choice: bool = false
|
||||
var _waiting_for_optional: bool = false
|
||||
|
||||
# Connected game state
|
||||
var _game_state = null # GameState reference
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_load_abilities()
|
||||
_init_subsystems()
|
||||
|
||||
|
||||
func _init_subsystems() -> void:
|
||||
trigger_matcher = TriggerMatcher.new()
|
||||
effect_resolver = EffectResolver.new()
|
||||
target_selector = TargetSelector.new()
|
||||
field_effect_manager = FieldEffectManager.new()
|
||||
condition_checker = ConditionChecker.new()
|
||||
|
||||
# Wire ConditionChecker to subsystems that need it
|
||||
effect_resolver.condition_checker = condition_checker
|
||||
trigger_matcher.condition_checker = condition_checker
|
||||
|
||||
# Connect effect resolver signals
|
||||
effect_resolver.effect_completed.connect(_on_effect_completed)
|
||||
effect_resolver.choice_required.connect(_on_choice_required)
|
||||
|
||||
|
||||
func _load_abilities() -> void:
|
||||
if not FileAccess.file_exists(ABILITIES_PATH):
|
||||
push_warning("AbilitySystem: No processed abilities found at " + ABILITIES_PATH)
|
||||
push_warning("Run: python tools/ability_processor.py")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(ABILITIES_PATH, FileAccess.READ)
|
||||
if not file:
|
||||
push_error("AbilitySystem: Failed to open " + ABILITIES_PATH)
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(file.get_as_text())
|
||||
file.close()
|
||||
|
||||
if error != OK:
|
||||
push_error("AbilitySystem: Failed to parse abilities JSON: " + json.get_error_message())
|
||||
return
|
||||
|
||||
var data = json.get_data()
|
||||
_version = data.get("version", "unknown")
|
||||
_stats = data.get("statistics", {})
|
||||
_abilities = data.get("abilities", {})
|
||||
|
||||
print("AbilitySystem: Loaded v%s - %d cards, %d abilities (%d high confidence)" % [
|
||||
_version,
|
||||
_stats.get("total_cards", 0),
|
||||
_stats.get("total_abilities", 0),
|
||||
_stats.get("parsed_high", 0)
|
||||
])
|
||||
|
||||
|
||||
## Connect to a game state to listen for events
|
||||
func connect_to_game(game_state) -> void:
|
||||
if _game_state:
|
||||
_disconnect_from_game()
|
||||
|
||||
_game_state = game_state
|
||||
|
||||
# Connect to game events that can trigger abilities
|
||||
game_state.card_played.connect(_on_card_played)
|
||||
game_state.summon_cast.connect(_on_summon_cast)
|
||||
game_state.attack_declared.connect(_on_attack_declared)
|
||||
game_state.block_declared.connect(_on_block_declared)
|
||||
game_state.forward_broken.connect(_on_forward_broken)
|
||||
game_state.damage_dealt.connect(_on_damage_dealt)
|
||||
game_state.card_moved.connect(_on_card_moved)
|
||||
game_state.combat_resolved.connect(_on_combat_resolved)
|
||||
|
||||
# Turn manager signals
|
||||
if game_state.turn_manager:
|
||||
game_state.turn_manager.phase_changed.connect(_on_phase_changed)
|
||||
game_state.turn_manager.turn_started.connect(_on_turn_started)
|
||||
game_state.turn_manager.turn_ended.connect(_on_turn_ended)
|
||||
|
||||
print("AbilitySystem: Connected to GameState")
|
||||
|
||||
|
||||
func _disconnect_from_game() -> void:
|
||||
if not _game_state:
|
||||
return
|
||||
|
||||
# Disconnect all signals
|
||||
if _game_state.card_played.is_connected(_on_card_played):
|
||||
_game_state.card_played.disconnect(_on_card_played)
|
||||
if _game_state.summon_cast.is_connected(_on_summon_cast):
|
||||
_game_state.summon_cast.disconnect(_on_summon_cast)
|
||||
if _game_state.attack_declared.is_connected(_on_attack_declared):
|
||||
_game_state.attack_declared.disconnect(_on_attack_declared)
|
||||
if _game_state.block_declared.is_connected(_on_block_declared):
|
||||
_game_state.block_declared.disconnect(_on_block_declared)
|
||||
if _game_state.forward_broken.is_connected(_on_forward_broken):
|
||||
_game_state.forward_broken.disconnect(_on_forward_broken)
|
||||
if _game_state.damage_dealt.is_connected(_on_damage_dealt):
|
||||
_game_state.damage_dealt.disconnect(_on_damage_dealt)
|
||||
if _game_state.card_moved.is_connected(_on_card_moved):
|
||||
_game_state.card_moved.disconnect(_on_card_moved)
|
||||
|
||||
_game_state = null
|
||||
|
||||
|
||||
## Get parsed abilities for a card
|
||||
func get_abilities(card_id: String) -> Array:
|
||||
return _abilities.get(card_id, [])
|
||||
|
||||
|
||||
## Check if a card has parsed abilities
|
||||
func has_abilities(card_id: String) -> bool:
|
||||
return _abilities.has(card_id) and _abilities[card_id].size() > 0
|
||||
|
||||
|
||||
## Get a specific parsed ability
|
||||
func get_ability(card_id: String, ability_index: int) -> Dictionary:
|
||||
var abilities = get_abilities(card_id)
|
||||
if ability_index >= 0 and ability_index < abilities.size():
|
||||
return abilities[ability_index]
|
||||
return {}
|
||||
|
||||
|
||||
## Process a game event and trigger matching abilities
|
||||
func process_event(event_type: String, event_data: Dictionary) -> void:
|
||||
if not _game_state:
|
||||
return
|
||||
|
||||
var triggered = trigger_matcher.find_triggered_abilities(
|
||||
event_type, event_data, _game_state, _abilities
|
||||
)
|
||||
|
||||
for trigger_info in triggered:
|
||||
_queue_ability(trigger_info)
|
||||
|
||||
|
||||
## Queue an ability for resolution
|
||||
func _queue_ability(trigger_info: Dictionary) -> void:
|
||||
var source = trigger_info.source as CardInstance
|
||||
var ability = trigger_info.ability as Dictionary
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
if not parsed or not parsed.has("effects"):
|
||||
return
|
||||
|
||||
# Check if ability has a cost that needs to be paid
|
||||
var cost = parsed.get("cost", {})
|
||||
if not cost.is_empty() and source and _game_state:
|
||||
var player = _game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
# Validate cost
|
||||
var validation = _validate_ability_cost(cost, source, player)
|
||||
if not validation.valid:
|
||||
# Cannot pay cost - emit signal and skip ability
|
||||
ability_cost_failed.emit(source, ability, validation.reason)
|
||||
push_warning("AbilitySystem: Cannot pay cost for ability - %s" % validation.reason)
|
||||
return
|
||||
|
||||
# Pay the cost
|
||||
_pay_ability_cost(cost, source, player)
|
||||
|
||||
ability_triggered.emit(source, ability)
|
||||
|
||||
# Add effects to pending stack (LIFO for proper resolution order)
|
||||
var effects = parsed.get("effects", [])
|
||||
for i in range(effects.size() - 1, -1, -1):
|
||||
_pending_effects.push_front({
|
||||
"effect": effects[i],
|
||||
"source": source,
|
||||
"controller": source.controller_index,
|
||||
"ability": ability,
|
||||
"event_data": trigger_info.get("event_data", {})
|
||||
})
|
||||
|
||||
# Start resolving if not already
|
||||
if not _is_resolving:
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Resolve the next pending effect
|
||||
func _resolve_next_effect() -> void:
|
||||
if _pending_effects.is_empty():
|
||||
_is_resolving = false
|
||||
return
|
||||
|
||||
_is_resolving = true
|
||||
var pending = _pending_effects[0]
|
||||
var effect = pending.effect
|
||||
var source = pending.source
|
||||
|
||||
# Check if effect is optional and we haven't prompted yet
|
||||
if effect.get("optional", false) and not pending.get("optional_prompted", false):
|
||||
_waiting_for_optional = true
|
||||
pending["optional_prompted"] = true # Mark as prompted to avoid re-prompting
|
||||
|
||||
# Determine which player should decide
|
||||
var player_index = source.controller_index if source else 0
|
||||
|
||||
# Build description from effect
|
||||
var description = _build_effect_description(effect)
|
||||
|
||||
# Emit signal for UI to handle
|
||||
optional_effect_prompt.emit(player_index, effect, description, _on_optional_effect_choice)
|
||||
return # Wait for callback
|
||||
|
||||
# Check if effect needs targeting
|
||||
if _effect_needs_targeting(effect):
|
||||
var valid_targets = target_selector.get_valid_targets(
|
||||
effect.get("target", {}), source, _game_state
|
||||
)
|
||||
|
||||
if valid_targets.is_empty():
|
||||
# No valid targets, skip effect
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
return
|
||||
|
||||
# Request target selection from player
|
||||
targeting_required.emit(effect, valid_targets, _on_targets_selected)
|
||||
# Wait for targeting_completed signal
|
||||
else:
|
||||
# Resolve immediately
|
||||
_execute_effect(pending)
|
||||
|
||||
|
||||
## Execute an effect with its targets
|
||||
func _execute_effect(pending: Dictionary) -> void:
|
||||
var effect = pending.effect
|
||||
var source = pending.source
|
||||
var targets = pending.get("targets", [])
|
||||
|
||||
effect_resolver.resolve(effect, source, targets, _game_state)
|
||||
|
||||
|
||||
## Called when effect resolution completes
|
||||
func _on_effect_completed(effect: Dictionary, targets: Array) -> void:
|
||||
effect_resolved.emit(effect, targets)
|
||||
|
||||
if not _pending_effects.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Called when player selects targets
|
||||
func _on_targets_selected(targets: Array) -> void:
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
_pending_effects[0]["targets"] = targets
|
||||
targeting_completed.emit(_pending_effects[0].effect, targets)
|
||||
_execute_effect(_pending_effects[0])
|
||||
|
||||
|
||||
## Check if effect requires player targeting
|
||||
func _effect_needs_targeting(effect: Dictionary) -> bool:
|
||||
if not effect.has("target"):
|
||||
return false
|
||||
var target = effect.target
|
||||
return target.get("type") == "CHOOSE"
|
||||
|
||||
|
||||
## Called when player responds to optional effect prompt
|
||||
func _on_optional_effect_choice(accepted: bool) -> void:
|
||||
_waiting_for_optional = false
|
||||
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
if accepted:
|
||||
# Player chose to execute the optional effect
|
||||
# Continue with normal resolution (targeting or execution)
|
||||
var pending = _pending_effects[0]
|
||||
var effect = pending.effect
|
||||
|
||||
if _effect_needs_targeting(effect):
|
||||
var source = pending.source
|
||||
var valid_targets = target_selector.get_valid_targets(
|
||||
effect.get("target", {}), source, _game_state
|
||||
)
|
||||
|
||||
if valid_targets.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
return
|
||||
|
||||
targeting_required.emit(effect, valid_targets, _on_targets_selected)
|
||||
else:
|
||||
_execute_effect(pending)
|
||||
else:
|
||||
# Player declined the optional effect - skip it
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Build a human-readable description of an effect for prompts
|
||||
func _build_effect_description(effect: Dictionary) -> String:
|
||||
var effect_type = str(effect.get("type", "")).to_upper()
|
||||
var amount = effect.get("amount", 0)
|
||||
|
||||
match effect_type:
|
||||
"DRAW":
|
||||
var count = effect.get("amount", 1)
|
||||
return "Draw %d card%s" % [count, "s" if count > 1 else ""]
|
||||
"DAMAGE":
|
||||
return "Deal %d damage" % amount
|
||||
"POWER_MOD":
|
||||
var sign = "+" if amount >= 0 else ""
|
||||
return "Give %s%d power" % [sign, amount]
|
||||
"DULL":
|
||||
return "Dull a Forward"
|
||||
"ACTIVATE":
|
||||
return "Activate a card"
|
||||
"BREAK":
|
||||
return "Break a card"
|
||||
"RETURN":
|
||||
return "Return a card to hand"
|
||||
"SEARCH":
|
||||
return "Search your deck"
|
||||
"DISCARD":
|
||||
var count = effect.get("amount", 1)
|
||||
return "Discard %d card%s" % [count, "s" if count > 1 else ""]
|
||||
_:
|
||||
# Use the original_text if available
|
||||
if effect.has("original_text"):
|
||||
return effect.original_text
|
||||
return "Use this effect"
|
||||
|
||||
|
||||
## Called when EffectResolver encounters a CHOOSE_MODE effect
|
||||
func _on_choice_required(effect: Dictionary, modes: Array) -> void:
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
var pending = _pending_effects[0]
|
||||
var source = pending.get("source") as CardInstance
|
||||
|
||||
# Check for enhanced condition (e.g., "If you have 5+ Ifrit, select 3 instead")
|
||||
var select_count = effect.get("select_count", 1)
|
||||
var select_up_to = effect.get("select_up_to", false)
|
||||
|
||||
var enhanced = effect.get("enhanced_condition", {})
|
||||
if not enhanced.is_empty() and _check_enhanced_condition(enhanced, source):
|
||||
select_count = enhanced.get("select_count", select_count)
|
||||
select_up_to = enhanced.get("select_up_to", select_up_to)
|
||||
|
||||
# If we have a ChoiceModal, use it
|
||||
if choice_modal:
|
||||
_waiting_for_choice = true
|
||||
_handle_modal_choice_async(effect, modes, select_count, select_up_to, source)
|
||||
else:
|
||||
# No UI available - auto-select first N modes
|
||||
push_warning("AbilitySystem: No ChoiceModal available, auto-selecting first mode(s)")
|
||||
var auto_selected: Array = []
|
||||
for i in range(min(select_count, modes.size())):
|
||||
auto_selected.append(i)
|
||||
_on_modes_selected(effect, modes, auto_selected, source)
|
||||
|
||||
|
||||
## Handle modal choice asynchronously
|
||||
func _handle_modal_choice_async(
|
||||
effect: Dictionary,
|
||||
modes: Array,
|
||||
select_count: int,
|
||||
select_up_to: bool,
|
||||
source: CardInstance
|
||||
) -> void:
|
||||
var selected = await choice_modal.show_choices(
|
||||
"", # Title is generated by ChoiceModal
|
||||
modes,
|
||||
select_count,
|
||||
select_up_to,
|
||||
false # Not cancellable for mandatory abilities
|
||||
)
|
||||
|
||||
_waiting_for_choice = false
|
||||
_on_modes_selected(effect, modes, selected, source)
|
||||
|
||||
|
||||
## Cached regex for enhanced condition parsing
|
||||
var _enhanced_count_regex: RegEx = null
|
||||
|
||||
|
||||
## Check if enhanced condition is met
|
||||
func _check_enhanced_condition(condition: Dictionary, source: CardInstance) -> bool:
|
||||
var description = condition.get("description", "").to_lower()
|
||||
|
||||
# Parse "if you have X or more [Card Name] in your Break Zone"
|
||||
if "break zone" in description:
|
||||
# Initialize regex once (lazy)
|
||||
if _enhanced_count_regex == null:
|
||||
_enhanced_count_regex = RegEx.new()
|
||||
_enhanced_count_regex.compile("(\\d+) or more")
|
||||
|
||||
var match_result = _enhanced_count_regex.search(description)
|
||||
if match_result:
|
||||
var required_count = int(match_result.get_string(1))
|
||||
|
||||
# Check break zone for matching cards
|
||||
if _game_state and source:
|
||||
var player = _game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
# Count matching cards in break zone
|
||||
var break_zone_count = 0
|
||||
for card in player.break_zone.get_cards():
|
||||
# Simple name matching (description contains card name pattern)
|
||||
if card.card_data and card.card_data.name.to_lower() in description:
|
||||
break_zone_count += 1
|
||||
|
||||
return break_zone_count >= required_count
|
||||
|
||||
return false
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COST VALIDATION AND PAYMENT
|
||||
# =============================================================================
|
||||
|
||||
## Signal emitted when cost cannot be paid
|
||||
signal ability_cost_failed(source: CardInstance, ability: Dictionary, reason: String)
|
||||
|
||||
|
||||
## Validate that a player can pay the cost for an ability
|
||||
## Returns true if cost can be paid, false otherwise
|
||||
func _validate_ability_cost(
|
||||
cost: Dictionary,
|
||||
source: CardInstance,
|
||||
player
|
||||
) -> Dictionary:
|
||||
var result = {"valid": true, "reason": ""}
|
||||
|
||||
if cost.is_empty():
|
||||
return result
|
||||
|
||||
# Check CP cost
|
||||
var cp_cost = cost.get("cp", 0)
|
||||
var element = cost.get("element", "")
|
||||
|
||||
if cp_cost > 0:
|
||||
if element and element != "" and element.to_upper() != "ANY":
|
||||
# Specific element required
|
||||
var element_enum = Enums.element_from_string(element)
|
||||
if player.cp_pool.get_cp(element_enum) < cp_cost:
|
||||
result.valid = false
|
||||
result.reason = "Not enough %s CP (need %d, have %d)" % [
|
||||
element, cp_cost, player.cp_pool.get_cp(element_enum)
|
||||
]
|
||||
return result
|
||||
else:
|
||||
# Any element
|
||||
if player.cp_pool.get_total_cp() < cp_cost:
|
||||
result.valid = false
|
||||
result.reason = "Not enough CP (need %d, have %d)" % [
|
||||
cp_cost, player.cp_pool.get_total_cp()
|
||||
]
|
||||
return result
|
||||
|
||||
# Check discard cost
|
||||
var discard = cost.get("discard", 0)
|
||||
if discard > 0:
|
||||
if player.hand.get_count() < discard:
|
||||
result.valid = false
|
||||
result.reason = "Not enough cards in hand to discard (need %d, have %d)" % [
|
||||
discard, player.hand.get_count()
|
||||
]
|
||||
return result
|
||||
|
||||
# Check dull self cost
|
||||
var dull_self = cost.get("dull_self", false)
|
||||
if dull_self and source:
|
||||
if source.is_dull():
|
||||
result.valid = false
|
||||
result.reason = "Card is already dulled"
|
||||
return result
|
||||
|
||||
# Check specific card discard
|
||||
var specific_discard = cost.get("specific_discard", "")
|
||||
if specific_discard != "":
|
||||
# Player must have a card with this name in hand
|
||||
var has_card = false
|
||||
for card in player.hand.get_cards():
|
||||
if card.card_data and card.card_data.name.to_lower() == specific_discard.to_lower():
|
||||
has_card = true
|
||||
break
|
||||
if not has_card:
|
||||
result.valid = false
|
||||
result.reason = "Must discard a card named '%s'" % specific_discard
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Pay the cost for an ability
|
||||
## Returns true if cost was paid successfully
|
||||
func _pay_ability_cost(
|
||||
cost: Dictionary,
|
||||
source: CardInstance,
|
||||
player
|
||||
) -> bool:
|
||||
if cost.is_empty():
|
||||
return true
|
||||
|
||||
# Pay CP cost
|
||||
var cp_cost = cost.get("cp", 0)
|
||||
var element = cost.get("element", "")
|
||||
|
||||
if cp_cost > 0:
|
||||
if element and element != "" and element.to_upper() != "ANY":
|
||||
# Spend specific element CP
|
||||
var element_enum = Enums.element_from_string(element)
|
||||
player.cp_pool.add_cp(element_enum, -cp_cost)
|
||||
else:
|
||||
# Spend from any element (generic)
|
||||
var remaining = cp_cost
|
||||
for elem in Enums.Element.values():
|
||||
var available = player.cp_pool.get_cp(elem)
|
||||
if available > 0:
|
||||
var to_spend = mini(available, remaining)
|
||||
player.cp_pool.add_cp(elem, -to_spend)
|
||||
remaining -= to_spend
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
# Pay dull self cost
|
||||
var dull_self = cost.get("dull_self", false)
|
||||
if dull_self and source:
|
||||
source.dull()
|
||||
|
||||
# Note: Discard costs are handled through separate UI interaction
|
||||
# The discard selection would be queued as a separate effect
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Called when player selects mode(s)
|
||||
func _on_modes_selected(
|
||||
effect: Dictionary,
|
||||
modes: Array,
|
||||
selected_indices: Array,
|
||||
source: CardInstance
|
||||
) -> void:
|
||||
# Queue the effects from selected modes
|
||||
for index in selected_indices:
|
||||
if index >= 0 and index < modes.size():
|
||||
var mode = modes[index]
|
||||
var mode_effects = mode.get("effects", [])
|
||||
|
||||
for mode_effect in mode_effects:
|
||||
_pending_effects.push_back({
|
||||
"effect": mode_effect,
|
||||
"source": source,
|
||||
"controller": source.controller_index if source else 0,
|
||||
"ability": effect,
|
||||
"event_data": {}
|
||||
})
|
||||
|
||||
# Remove the CHOOSE_MODE effect from pending and continue
|
||||
if not _pending_effects.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Event Handlers
|
||||
# =============================================================================
|
||||
|
||||
func _on_card_played(card: CardInstance, player_index: int) -> void:
|
||||
# Register field abilities
|
||||
if card.is_forward() or card.is_backup():
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
field_effect_manager.register_field_abilities(card, card_abilities)
|
||||
|
||||
# Trigger enters field events
|
||||
process_event("ENTERS_FIELD", {
|
||||
"card": card,
|
||||
"player": player_index,
|
||||
"zone_from": Enums.ZoneType.HAND,
|
||||
"zone_to": Enums.ZoneType.FIELD_FORWARDS if card.is_forward() else Enums.ZoneType.FIELD_BACKUPS
|
||||
})
|
||||
|
||||
|
||||
func _on_summon_cast(card: CardInstance, player_index: int) -> void:
|
||||
process_event("SUMMON_CAST", {
|
||||
"card": card,
|
||||
"player": player_index
|
||||
})
|
||||
|
||||
|
||||
func _on_attack_declared(attacker: CardInstance) -> void:
|
||||
process_event("ATTACKS", {
|
||||
"card": attacker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_block_declared(blocker: CardInstance) -> void:
|
||||
if not _game_state or not _game_state.turn_manager:
|
||||
return
|
||||
|
||||
var attacker = _game_state.turn_manager.current_attacker
|
||||
|
||||
process_event("BLOCKS", {
|
||||
"card": blocker,
|
||||
"attacker": attacker,
|
||||
"player": blocker.controller_index
|
||||
})
|
||||
|
||||
if attacker:
|
||||
process_event("IS_BLOCKED", {
|
||||
"card": attacker,
|
||||
"blocker": blocker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_forward_broken(card: CardInstance) -> void:
|
||||
# Unregister field abilities
|
||||
field_effect_manager.unregister_field_abilities(card)
|
||||
|
||||
process_event("LEAVES_FIELD", {
|
||||
"card": card,
|
||||
"zone_from": Enums.ZoneType.FIELD_FORWARDS,
|
||||
"zone_to": Enums.ZoneType.BREAK
|
||||
})
|
||||
|
||||
|
||||
func _on_damage_dealt(player_index: int, amount: int, cards: Array) -> void:
|
||||
process_event("DAMAGE_DEALT_TO_PLAYER", {
|
||||
"player": player_index,
|
||||
"amount": amount,
|
||||
"cards": cards
|
||||
})
|
||||
|
||||
# Check for EX BURST triggers on damage cards
|
||||
for card in cards:
|
||||
if card.card_data and card.card_data.has_ex_burst:
|
||||
_trigger_ex_burst(card, player_index)
|
||||
|
||||
|
||||
func _on_card_moved(card: CardInstance, from_zone: Enums.ZoneType, to_zone: Enums.ZoneType) -> void:
|
||||
# Handle zone changes
|
||||
if to_zone == Enums.ZoneType.BREAK:
|
||||
field_effect_manager.unregister_field_abilities(card)
|
||||
|
||||
|
||||
func _on_combat_resolved(attacker: CardInstance, blocker: CardInstance) -> void:
|
||||
if not blocker:
|
||||
# Unblocked attack
|
||||
process_event("DEALS_DAMAGE_TO_OPPONENT", {
|
||||
"card": attacker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
else:
|
||||
# Blocked combat
|
||||
process_event("DEALS_DAMAGE", {
|
||||
"card": attacker,
|
||||
"target": blocker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
process_event("DEALS_DAMAGE", {
|
||||
"card": blocker,
|
||||
"target": attacker,
|
||||
"player": blocker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_phase_changed(phase: Enums.TurnPhase) -> void:
|
||||
var event_type = ""
|
||||
match phase:
|
||||
Enums.TurnPhase.ACTIVE:
|
||||
event_type = "START_OF_ACTIVE_PHASE"
|
||||
Enums.TurnPhase.DRAW:
|
||||
event_type = "START_OF_DRAW_PHASE"
|
||||
Enums.TurnPhase.MAIN_1:
|
||||
event_type = "START_OF_MAIN_PHASE"
|
||||
Enums.TurnPhase.ATTACK:
|
||||
event_type = "START_OF_ATTACK_PHASE"
|
||||
Enums.TurnPhase.MAIN_2:
|
||||
event_type = "START_OF_MAIN_PHASE_2"
|
||||
Enums.TurnPhase.END:
|
||||
event_type = "START_OF_END_PHASE"
|
||||
|
||||
if event_type:
|
||||
process_event(event_type, {
|
||||
"player": _game_state.turn_manager.current_player_index if _game_state else 0
|
||||
})
|
||||
|
||||
|
||||
func _on_turn_started(player_index: int, turn_number: int) -> void:
|
||||
process_event("START_OF_TURN", {
|
||||
"player": player_index,
|
||||
"turn_number": turn_number
|
||||
})
|
||||
|
||||
|
||||
func _on_turn_ended(player_index: int) -> void:
|
||||
process_event("END_OF_TURN", {
|
||||
"player": player_index
|
||||
})
|
||||
|
||||
|
||||
## Trigger EX BURST for a damage card
|
||||
func _trigger_ex_burst(card: CardInstance, damaged_player: int) -> void:
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
|
||||
for ability in card_abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("is_ex_burst", false):
|
||||
# Queue the EX BURST ability
|
||||
_queue_ability({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": {
|
||||
"player": damaged_player,
|
||||
"trigger_type": "EX_BURST"
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
## Get power modifier from field effects for a card
|
||||
func get_field_power_modifier(card: CardInstance) -> int:
|
||||
return field_effect_manager.get_power_modifiers(card, _game_state)
|
||||
|
||||
|
||||
## Check if a card has a field-granted keyword
|
||||
func has_field_keyword(card: CardInstance, keyword: String) -> bool:
|
||||
return field_effect_manager.has_keyword(card, keyword, _game_state)
|
||||
|
||||
|
||||
## Check if a card has field-granted protection
|
||||
func has_field_protection(card: CardInstance, protection_type: String) -> bool:
|
||||
return field_effect_manager.has_protection(card, protection_type, _game_state)
|
||||
|
||||
|
||||
## Get all granted keywords for a card from field effects
|
||||
func get_field_keywords(card: CardInstance) -> Array:
|
||||
return field_effect_manager.get_granted_keywords(card, _game_state)
|
||||
|
||||
|
||||
## Trigger EX BURST on a specific card (called by EffectResolver)
|
||||
func trigger_ex_burst_on_card(card: CardInstance) -> void:
|
||||
if not card or not card.card_data:
|
||||
return
|
||||
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
|
||||
for ability in card_abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("is_ex_burst", false):
|
||||
_queue_ability({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": {
|
||||
"trigger_type": "EX_BURST_TRIGGERED"
|
||||
}
|
||||
})
|
||||
break
|
||||
219
scripts/game/abilities/CardFilter.gd
Normal file
219
scripts/game/abilities/CardFilter.gd
Normal file
@@ -0,0 +1,219 @@
|
||||
class_name CardFilter
|
||||
extends RefCounted
|
||||
|
||||
## CardFilter - Shared card filtering utility used by EffectResolver, FieldEffectManager, and TargetSelector
|
||||
##
|
||||
## This utility provides a unified way to filter cards based on various criteria
|
||||
## including element, job, category, cost, power, card type, and state.
|
||||
|
||||
|
||||
## Check if a card matches a filter dictionary
|
||||
static func matches_filter(card: CardInstance, filter: Dictionary, source: CardInstance = null) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
if filter.is_empty():
|
||||
return true
|
||||
|
||||
# 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
|
||||
|
||||
# Job filter
|
||||
if filter.has("job"):
|
||||
var job_filter = str(filter.job).to_lower()
|
||||
var card_job = str(card.card_data.job).to_lower() if card.card_data.job else ""
|
||||
if job_filter != card_job:
|
||||
return false
|
||||
|
||||
# Category filter
|
||||
if filter.has("category"):
|
||||
if not _has_category(card, str(filter.category).to_upper()):
|
||||
return false
|
||||
|
||||
# Cost filters
|
||||
if not _matches_cost_filter(card, filter):
|
||||
return false
|
||||
|
||||
# Power filters
|
||||
if not _matches_power_filter(card, filter):
|
||||
return false
|
||||
|
||||
# Card name filter
|
||||
if filter.has("card_name"):
|
||||
var name_filter = str(filter.card_name).to_lower()
|
||||
var card_name = str(card.card_data.name).to_lower() if card.card_data.name else ""
|
||||
if name_filter != card_name:
|
||||
return false
|
||||
if filter.has("name"):
|
||||
var name_filter = str(filter.name).to_lower()
|
||||
var card_name = str(card.card_data.name).to_lower() if card.card_data.name else ""
|
||||
if name_filter != card_name:
|
||||
return false
|
||||
|
||||
# Card type filter
|
||||
if filter.has("card_type"):
|
||||
if not _matches_type(card, str(filter.card_type)):
|
||||
return false
|
||||
|
||||
# State filters
|
||||
if filter.has("is_dull"):
|
||||
var card_dull = card.is_dull() if card.has_method("is_dull") else card.is_dull
|
||||
if card_dull != filter.is_dull:
|
||||
return false
|
||||
if filter.has("is_active"):
|
||||
var card_active = card.is_active() if card.has_method("is_active") else not card.is_dull
|
||||
if card_active != filter.is_active:
|
||||
return false
|
||||
|
||||
# Exclude self
|
||||
if filter.get("exclude_self", false) and source != null and card == source:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Count cards that match a filter
|
||||
static func count_matching(cards: Array, filter: Dictionary, source: CardInstance = null) -> int:
|
||||
var count = 0
|
||||
for card in cards:
|
||||
if matches_filter(card, filter, source):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
## Get all cards that match a filter
|
||||
static func get_matching(cards: Array, filter: Dictionary, source: CardInstance = null) -> Array:
|
||||
var matching: Array = []
|
||||
for card in cards:
|
||||
if matches_filter(card, filter, source):
|
||||
matching.append(card)
|
||||
return matching
|
||||
|
||||
|
||||
## Get the highest power among cards (optionally filtered)
|
||||
static func get_highest_power(cards: Array, filter: Dictionary = {}, source: CardInstance = null) -> int:
|
||||
var highest = 0
|
||||
for card in cards:
|
||||
if filter.is_empty() or matches_filter(card, filter, source):
|
||||
var power = card.get_power() if card.has_method("get_power") else 0
|
||||
if power > highest:
|
||||
highest = power
|
||||
return highest
|
||||
|
||||
|
||||
## Get the lowest power among cards (optionally filtered)
|
||||
static func get_lowest_power(cards: Array, filter: Dictionary = {}, source: CardInstance = null) -> int:
|
||||
var lowest = -1
|
||||
for card in cards:
|
||||
if filter.is_empty() or matches_filter(card, filter, source):
|
||||
var power = card.get_power() if card.has_method("get_power") else 0
|
||||
if lowest == -1 or power < lowest:
|
||||
lowest = power
|
||||
return lowest if lowest != -1 else 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRIVATE HELPER METHODS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Check if a card has a specific category
|
||||
static func _has_category(card: CardInstance, category_filter: String) -> bool:
|
||||
# Check card's category field
|
||||
if card.card_data.has("category") and card.card_data.category:
|
||||
if category_filter in str(card.card_data.category).to_upper():
|
||||
return true
|
||||
# Check categories array if present
|
||||
if card.card_data.has("categories"):
|
||||
for cat in card.card_data.categories:
|
||||
if category_filter in str(cat).to_upper():
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Check if a card matches cost filter criteria
|
||||
static func _matches_cost_filter(card: CardInstance, filter: Dictionary) -> bool:
|
||||
var card_cost = card.card_data.cost
|
||||
|
||||
# Exact cost filter
|
||||
if filter.has("cost") and not filter.has("cost_comparison"):
|
||||
if card_cost != int(filter.cost):
|
||||
return false
|
||||
|
||||
# Min/max style cost filters (from TargetSelector)
|
||||
if filter.has("cost_min") and card_cost < int(filter.cost_min):
|
||||
return false
|
||||
if filter.has("cost_max") and card_cost > int(filter.cost_max):
|
||||
return false
|
||||
|
||||
# Cost comparison filter
|
||||
if filter.has("cost_comparison") and filter.has("cost_value"):
|
||||
var target_cost = int(filter.cost_value)
|
||||
match str(filter.cost_comparison).to_upper():
|
||||
"LTE":
|
||||
if card_cost > target_cost:
|
||||
return false
|
||||
"GTE":
|
||||
if card_cost < target_cost:
|
||||
return false
|
||||
"EQ":
|
||||
if card_cost != target_cost:
|
||||
return false
|
||||
"LT":
|
||||
if card_cost >= target_cost:
|
||||
return false
|
||||
"GT":
|
||||
if card_cost <= target_cost:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card matches type filter
|
||||
static func _matches_type(card: CardInstance, type_filter: String) -> bool:
|
||||
match type_filter.to_upper():
|
||||
"FORWARD":
|
||||
return card.is_forward()
|
||||
"BACKUP":
|
||||
return card.is_backup()
|
||||
"SUMMON":
|
||||
return card.is_summon()
|
||||
"MONSTER":
|
||||
return card.is_monster()
|
||||
"CHARACTER":
|
||||
return card.is_forward() or card.is_backup()
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card matches power filter criteria
|
||||
static func _matches_power_filter(card: CardInstance, filter: Dictionary) -> bool:
|
||||
var power = card.get_power() if card.has_method("get_power") else card.card_data.power
|
||||
|
||||
# Min/max style power filters
|
||||
if filter.has("power_min") and power < int(filter.power_min):
|
||||
return false
|
||||
if filter.has("power_max") and power > int(filter.power_max):
|
||||
return false
|
||||
|
||||
# Comparison style power filter
|
||||
if filter.has("power_comparison") and filter.has("power_value"):
|
||||
var target = int(filter.power_value)
|
||||
match str(filter.power_comparison).to_upper():
|
||||
"LTE":
|
||||
if power > target:
|
||||
return false
|
||||
"GTE":
|
||||
if power < target:
|
||||
return false
|
||||
"LT":
|
||||
if power >= target:
|
||||
return false
|
||||
"GT":
|
||||
if power <= target:
|
||||
return false
|
||||
|
||||
return true
|
||||
510
scripts/game/abilities/ConditionChecker.gd
Normal file
510
scripts/game/abilities/ConditionChecker.gd
Normal file
@@ -0,0 +1,510 @@
|
||||
class_name ConditionChecker
|
||||
extends RefCounted
|
||||
|
||||
## Centralized condition evaluation for all ability types
|
||||
## Handles conditions like "If you control X", "If you have received Y damage", etc.
|
||||
|
||||
|
||||
## Main evaluation entry point
|
||||
## Returns true if condition is met, false otherwise
|
||||
func evaluate(condition: Dictionary, context: Dictionary) -> bool:
|
||||
if condition.is_empty():
|
||||
return true # Empty condition = unconditional
|
||||
|
||||
var condition_type = condition.get("type", "")
|
||||
|
||||
match condition_type:
|
||||
"CONTROL_CARD":
|
||||
return _check_control_card(condition, context)
|
||||
"CONTROL_COUNT":
|
||||
return _check_control_count(condition, context)
|
||||
"DAMAGE_RECEIVED":
|
||||
return _check_damage_received(condition, context)
|
||||
"BREAK_ZONE_COUNT":
|
||||
return _check_break_zone_count(condition, context)
|
||||
"CARD_IN_ZONE":
|
||||
return _check_card_in_zone(condition, context)
|
||||
"FORWARD_STATE":
|
||||
return _check_forward_state(condition, context)
|
||||
"COST_COMPARISON":
|
||||
return _check_cost_comparison(condition, context)
|
||||
"POWER_COMPARISON":
|
||||
return _check_power_comparison(condition, context)
|
||||
"ELEMENT_MATCH":
|
||||
return _check_element_match(condition, context)
|
||||
"CARD_TYPE_MATCH":
|
||||
return _check_card_type_match(condition, context)
|
||||
"JOB_MATCH":
|
||||
return _check_job_match(condition, context)
|
||||
"CATEGORY_MATCH":
|
||||
return _check_category_match(condition, context)
|
||||
"AND":
|
||||
return _check_and(condition, context)
|
||||
"OR":
|
||||
return _check_or(condition, context)
|
||||
"NOT":
|
||||
return _check_not(condition, context)
|
||||
_:
|
||||
push_warning("ConditionChecker: Unknown condition type '%s'" % condition_type)
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONTROL CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_control_card(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_name = condition.get("card_name", "")
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
# Check all field cards for the player
|
||||
var field_cards = _get_field_cards(game_state, player)
|
||||
for card in field_cards:
|
||||
if card and card.card_data and card.card_data.name == card_name:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _check_control_count(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_type = condition.get("card_type", "")
|
||||
var element = condition.get("element", "")
|
||||
var job = condition.get("job", "")
|
||||
var category = condition.get("category", "")
|
||||
var comparison = condition.get("comparison", "GTE")
|
||||
var value = condition.get("value", 1)
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var count = 0
|
||||
var field_cards = _get_field_cards(game_state, player)
|
||||
|
||||
for card in field_cards:
|
||||
if not card or not card.card_data:
|
||||
continue
|
||||
|
||||
var matches = true
|
||||
|
||||
# Check card type filter
|
||||
if card_type != "" and not _matches_card_type(card, card_type):
|
||||
matches = false
|
||||
|
||||
# Check element filter
|
||||
if element != "" and not _matches_element(card, element):
|
||||
matches = false
|
||||
|
||||
# Check job filter
|
||||
if job != "" and not _matches_job(card, job):
|
||||
matches = false
|
||||
|
||||
# Check category filter
|
||||
if category != "" and not _matches_category(card, category):
|
||||
matches = false
|
||||
|
||||
if matches:
|
||||
count += 1
|
||||
|
||||
return _compare(count, comparison, value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DAMAGE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_damage_received(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var comparison = condition.get("comparison", "GTE")
|
||||
var value = condition.get("value", 1)
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var damage = _get_player_damage(game_state, player)
|
||||
return _compare(damage, comparison, value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ZONE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_break_zone_count(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_name = condition.get("card_name", "")
|
||||
var card_names: Array = condition.get("card_names", [])
|
||||
if card_name != "" and card_name not in card_names:
|
||||
card_names.append(card_name)
|
||||
|
||||
var comparison = condition.get("comparison", "GTE")
|
||||
var value = condition.get("value", 1)
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var count = 0
|
||||
var break_zone = _get_break_zone(game_state, player)
|
||||
|
||||
for card in break_zone:
|
||||
if not card or not card.card_data:
|
||||
continue
|
||||
|
||||
# If no specific names, count all
|
||||
if card_names.is_empty():
|
||||
count += 1
|
||||
elif card.card_data.name in card_names:
|
||||
count += 1
|
||||
|
||||
return _compare(count, comparison, value)
|
||||
|
||||
|
||||
func _check_card_in_zone(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var zone = condition.get("zone", "") # "HAND", "DECK", "BREAK_ZONE", "REMOVED"
|
||||
var card_name = condition.get("card_name", "")
|
||||
var card_type = condition.get("card_type", "")
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var zone_cards: Array = []
|
||||
match zone:
|
||||
"HAND":
|
||||
zone_cards = _get_hand(game_state, player)
|
||||
"DECK":
|
||||
zone_cards = _get_deck(game_state, player)
|
||||
"BREAK_ZONE":
|
||||
zone_cards = _get_break_zone(game_state, player)
|
||||
"REMOVED":
|
||||
zone_cards = _get_removed_zone(game_state, player)
|
||||
"FIELD":
|
||||
zone_cards = _get_field_cards(game_state, player)
|
||||
|
||||
for card in zone_cards:
|
||||
if not card or not card.card_data:
|
||||
continue
|
||||
|
||||
var matches = true
|
||||
if card_name != "" and card.card_data.name != card_name:
|
||||
matches = false
|
||||
if card_type != "" and not _matches_card_type(card, card_type):
|
||||
matches = false
|
||||
|
||||
if matches:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CARD STATE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_forward_state(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var state = condition.get("state", "") # "DULL", "ACTIVE", "DAMAGED"
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target:
|
||||
return false
|
||||
|
||||
match state:
|
||||
"DULL":
|
||||
return target.is_dull if target.has_method("get") or "is_dull" in target else false
|
||||
"ACTIVE":
|
||||
return not target.is_dull if "is_dull" in target else false
|
||||
"DAMAGED":
|
||||
if "current_power" in target and target.card_data:
|
||||
return target.current_power < target.card_data.power
|
||||
"FROZEN":
|
||||
return target.is_frozen if "is_frozen" in target else false
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _check_cost_comparison(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var comparison = condition.get("comparison", "LTE")
|
||||
var value = condition.get("value", 0)
|
||||
var compare_to = condition.get("compare_to", "") # "SELF_COST", "VALUE", or empty for value
|
||||
var target = context.get("target_card")
|
||||
var source = context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
var target_cost = target.card_data.cost
|
||||
var compare_value = value
|
||||
|
||||
if compare_to == "SELF_COST" and source and source.card_data:
|
||||
compare_value = source.card_data.cost
|
||||
|
||||
return _compare(target_cost, comparison, compare_value)
|
||||
|
||||
|
||||
func _check_power_comparison(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var comparison = condition.get("comparison", "LTE")
|
||||
var value = condition.get("value", 0)
|
||||
var compare_to = condition.get("compare_to", "") # "SELF_POWER", "VALUE"
|
||||
var target = context.get("target_card")
|
||||
var source = context.get("source_card")
|
||||
|
||||
if not target:
|
||||
return false
|
||||
|
||||
var target_power = target.current_power if "current_power" in target else 0
|
||||
var compare_value = value
|
||||
|
||||
if compare_to == "SELF_POWER" and source:
|
||||
compare_value = source.current_power if "current_power" in source else 0
|
||||
|
||||
return _compare(target_power, comparison, compare_value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CARD ATTRIBUTE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_element_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var element = condition.get("element", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
return _matches_element(target, element)
|
||||
|
||||
|
||||
func _check_card_type_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_type = condition.get("card_type", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target:
|
||||
return false
|
||||
|
||||
return _matches_card_type(target, card_type)
|
||||
|
||||
|
||||
func _check_job_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var job = condition.get("job", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
return _matches_job(target, job)
|
||||
|
||||
|
||||
func _check_category_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var category = condition.get("category", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
return _matches_category(target, category)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LOGICAL OPERATORS
|
||||
# =============================================================================
|
||||
|
||||
func _check_and(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var conditions: Array = condition.get("conditions", [])
|
||||
for sub_condition in conditions:
|
||||
if not evaluate(sub_condition, context):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _check_or(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var conditions: Array = condition.get("conditions", [])
|
||||
for sub_condition in conditions:
|
||||
if evaluate(sub_condition, context):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _check_not(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var inner: Dictionary = condition.get("condition", {})
|
||||
return not evaluate(inner, context)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _compare(actual: int, comparison: String, expected: int) -> bool:
|
||||
match comparison:
|
||||
"EQ":
|
||||
return actual == expected
|
||||
"NEQ":
|
||||
return actual != expected
|
||||
"GT":
|
||||
return actual > expected
|
||||
"GTE":
|
||||
return actual >= expected
|
||||
"LT":
|
||||
return actual < expected
|
||||
"LTE":
|
||||
return actual <= expected
|
||||
return false
|
||||
|
||||
|
||||
func _matches_card_type(card, card_type: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var type_upper = card_type.to_upper()
|
||||
var card_type_value = card.card_data.type
|
||||
|
||||
# Handle string or enum type
|
||||
if card_type_value is String:
|
||||
return card_type_value.to_upper() == type_upper
|
||||
|
||||
# Handle Enums.CardType enum
|
||||
match type_upper:
|
||||
"FORWARD":
|
||||
return card_type_value == Enums.CardType.FORWARD
|
||||
"BACKUP":
|
||||
return card_type_value == Enums.CardType.BACKUP
|
||||
"SUMMON":
|
||||
return card_type_value == Enums.CardType.SUMMON
|
||||
"MONSTER":
|
||||
return card_type_value == Enums.CardType.MONSTER
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _matches_element(card, element: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var element_upper = element.to_upper()
|
||||
var card_element = card.card_data.element
|
||||
|
||||
if card_element is String:
|
||||
return card_element.to_upper() == element_upper
|
||||
|
||||
# Handle Enums.Element enum
|
||||
match element_upper:
|
||||
"FIRE":
|
||||
return card_element == Enums.Element.FIRE
|
||||
"ICE":
|
||||
return card_element == Enums.Element.ICE
|
||||
"WIND":
|
||||
return card_element == Enums.Element.WIND
|
||||
"EARTH":
|
||||
return card_element == Enums.Element.EARTH
|
||||
"LIGHTNING":
|
||||
return card_element == Enums.Element.LIGHTNING
|
||||
"WATER":
|
||||
return card_element == Enums.Element.WATER
|
||||
"LIGHT":
|
||||
return card_element == Enums.Element.LIGHT
|
||||
"DARK":
|
||||
return card_element == Enums.Element.DARK
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _matches_job(card, job: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var card_job = card.card_data.get("job", "")
|
||||
if card_job is String:
|
||||
return card_job.to_lower() == job.to_lower()
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _matches_category(card, category: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var card_categories = card.card_data.get("categories", [])
|
||||
if card_categories is Array:
|
||||
for cat in card_categories:
|
||||
if cat is String and cat.to_lower() == category.to_lower():
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME STATE ACCESSORS
|
||||
# These abstract away the game state interface for flexibility
|
||||
# =============================================================================
|
||||
|
||||
func _get_field_cards(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_field_cards"):
|
||||
return game_state.get_field_cards(player)
|
||||
elif game_state.has_method("get_player_field"):
|
||||
return game_state.get_player_field(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "field" in p:
|
||||
return p.field
|
||||
return []
|
||||
|
||||
|
||||
func _get_player_damage(game_state, player: int) -> int:
|
||||
if game_state.has_method("get_player_damage"):
|
||||
return game_state.get_player_damage(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "damage" in p:
|
||||
return p.damage
|
||||
return 0
|
||||
|
||||
|
||||
func _get_break_zone(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_break_zone"):
|
||||
return game_state.get_break_zone(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "break_zone" in p:
|
||||
return p.break_zone
|
||||
return []
|
||||
|
||||
|
||||
func _get_hand(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_hand"):
|
||||
return game_state.get_hand(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "hand" in p:
|
||||
return p.hand
|
||||
return []
|
||||
|
||||
|
||||
func _get_deck(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_deck"):
|
||||
return game_state.get_deck(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "deck" in p:
|
||||
return p.deck
|
||||
return []
|
||||
|
||||
|
||||
func _get_removed_zone(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_removed_zone"):
|
||||
return game_state.get_removed_zone(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "removed_zone" in p:
|
||||
return p.removed_zone
|
||||
return []
|
||||
1807
scripts/game/abilities/EffectResolver.gd
Normal file
1807
scripts/game/abilities/EffectResolver.gd
Normal file
File diff suppressed because it is too large
Load Diff
681
scripts/game/abilities/FieldEffectManager.gd
Normal file
681
scripts/game/abilities/FieldEffectManager.gd
Normal file
@@ -0,0 +1,681 @@
|
||||
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
|
||||
174
scripts/game/abilities/TargetSelector.gd
Normal file
174
scripts/game/abilities/TargetSelector.gd
Normal file
@@ -0,0 +1,174 @@
|
||||
class_name TargetSelector
|
||||
extends RefCounted
|
||||
|
||||
## TargetSelector - Validates and provides target options for effects
|
||||
|
||||
|
||||
## Get all valid targets for an effect's target specification
|
||||
func get_valid_targets(
|
||||
target_spec: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
if target_spec.is_empty():
|
||||
return []
|
||||
|
||||
var candidates: Array = []
|
||||
|
||||
var zone = str(target_spec.get("zone", "FIELD")).to_upper()
|
||||
var owner = str(target_spec.get("owner", "ANY")).to_upper()
|
||||
var filter = target_spec.get("filter", {})
|
||||
var target_type = str(target_spec.get("type", "CHOOSE")).to_upper()
|
||||
|
||||
# Handle SELF and ALL targets specially
|
||||
if target_type == "SELF":
|
||||
return [source]
|
||||
elif target_type == "ALL":
|
||||
return _get_all_matching(owner, zone, filter, source, game_state)
|
||||
|
||||
# Collect candidates from appropriate zones
|
||||
match zone:
|
||||
"FIELD":
|
||||
candidates = _get_field_cards(owner, source, game_state)
|
||||
"HAND":
|
||||
candidates = _get_hand_cards(owner, source, game_state)
|
||||
"BREAK_ZONE", "BREAK":
|
||||
candidates = _get_break_zone_cards(owner, source, game_state)
|
||||
"DECK":
|
||||
candidates = _get_deck_cards(owner, source, game_state)
|
||||
_:
|
||||
# Default to field
|
||||
candidates = _get_field_cards(owner, source, game_state)
|
||||
|
||||
# Apply filters using CardFilter utility
|
||||
return CardFilter.get_matching(candidates, filter, source)
|
||||
|
||||
|
||||
## Get all cards matching filter (for "ALL" target type)
|
||||
func _get_all_matching(
|
||||
owner: String,
|
||||
zone: String,
|
||||
filter: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var candidates = _get_field_cards(owner, source, game_state)
|
||||
return CardFilter.get_matching(candidates, filter, source)
|
||||
|
||||
|
||||
## Get cards from field
|
||||
func _get_field_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var cards: Array = []
|
||||
|
||||
match owner:
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
cards.append_array(_get_player_field_cards(player))
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - source.controller_index)
|
||||
if opponent:
|
||||
cards.append_array(_get_player_field_cards(opponent))
|
||||
"ANY", _:
|
||||
for player in game_state.players:
|
||||
cards.append_array(_get_player_field_cards(player))
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Get all field cards for a player
|
||||
func _get_player_field_cards(player) -> Array:
|
||||
var cards: Array = []
|
||||
cards.append_array(player.field_forwards.get_cards())
|
||||
cards.append_array(player.field_backups.get_cards())
|
||||
return cards
|
||||
|
||||
|
||||
## Get cards from hand
|
||||
func _get_hand_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var cards: Array = []
|
||||
var player_index = source.controller_index
|
||||
|
||||
if owner == "OPPONENT":
|
||||
player_index = 1 - player_index
|
||||
|
||||
var player = game_state.get_player(player_index)
|
||||
if player:
|
||||
cards.append_array(player.hand.get_cards())
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Get cards from break zone
|
||||
func _get_break_zone_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var cards: Array = []
|
||||
var player_index = source.controller_index
|
||||
|
||||
if owner == "OPPONENT":
|
||||
player_index = 1 - player_index
|
||||
|
||||
var player = game_state.get_player(player_index)
|
||||
if player:
|
||||
cards.append_array(player.break_zone.get_cards())
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Get cards from deck
|
||||
func _get_deck_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
# Usually not directly targetable, used for search effects
|
||||
var cards: Array = []
|
||||
var player_index = source.controller_index
|
||||
|
||||
if owner == "OPPONENT":
|
||||
player_index = 1 - player_index
|
||||
|
||||
var player = game_state.get_player(player_index)
|
||||
if player:
|
||||
cards.append_array(player.deck.get_cards())
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Validate that a set of targets meets the target specification requirements
|
||||
func validate_targets(
|
||||
targets: Array,
|
||||
target_spec: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var target_type = str(target_spec.get("type", "CHOOSE")).to_upper()
|
||||
|
||||
# Check count requirements
|
||||
if target_spec.has("count"):
|
||||
var required = int(target_spec.count)
|
||||
if targets.size() != required:
|
||||
return false
|
||||
elif target_spec.has("count_up_to"):
|
||||
var max_count = int(target_spec.count_up_to)
|
||||
if targets.size() > max_count:
|
||||
return false
|
||||
|
||||
# Validate each target is valid
|
||||
var valid_targets = get_valid_targets(target_spec, source, game_state)
|
||||
for target in targets:
|
||||
if target not in valid_targets:
|
||||
return false
|
||||
|
||||
return true
|
||||
233
scripts/game/abilities/TriggerMatcher.gd
Normal file
233
scripts/game/abilities/TriggerMatcher.gd
Normal file
@@ -0,0 +1,233 @@
|
||||
class_name TriggerMatcher
|
||||
extends RefCounted
|
||||
|
||||
## TriggerMatcher - Matches game events to ability triggers
|
||||
## Scans all cards on field for abilities that trigger from the given event
|
||||
|
||||
## Reference to ConditionChecker for evaluating trigger conditions
|
||||
var condition_checker: ConditionChecker = null
|
||||
|
||||
|
||||
## Find all abilities that should trigger for a given event
|
||||
func find_triggered_abilities(
|
||||
event_type: String,
|
||||
event_data: Dictionary,
|
||||
game_state,
|
||||
all_abilities: Dictionary
|
||||
) -> Array:
|
||||
var triggered = []
|
||||
|
||||
# Check abilities on all cards in play
|
||||
for player in game_state.players:
|
||||
# Check forwards
|
||||
for card in player.field_forwards.get_cards():
|
||||
var card_abilities = all_abilities.get(card.card_data.id, [])
|
||||
triggered.append_array(_check_card_abilities(card, card_abilities, event_type, event_data, game_state))
|
||||
|
||||
# Check backups
|
||||
for card in player.field_backups.get_cards():
|
||||
var card_abilities = all_abilities.get(card.card_data.id, [])
|
||||
triggered.append_array(_check_card_abilities(card, card_abilities, event_type, event_data, game_state))
|
||||
|
||||
return triggered
|
||||
|
||||
|
||||
## Check all abilities on a card for triggers
|
||||
func _check_card_abilities(
|
||||
card: CardInstance,
|
||||
abilities: Array,
|
||||
event_type: String,
|
||||
event_data: Dictionary,
|
||||
game_state
|
||||
) -> Array:
|
||||
var triggered = []
|
||||
|
||||
for ability in abilities:
|
||||
if _matches_trigger(ability, event_type, event_data, card, game_state):
|
||||
triggered.append({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": event_data
|
||||
})
|
||||
|
||||
return triggered
|
||||
|
||||
|
||||
## Check if an ability's trigger matches the event
|
||||
func _matches_trigger(
|
||||
ability: Dictionary,
|
||||
event_type: String,
|
||||
event_data: Dictionary,
|
||||
source_card: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.is_empty():
|
||||
return false
|
||||
|
||||
# Only AUTO abilities have triggers
|
||||
if parsed.get("type") != "AUTO":
|
||||
return false
|
||||
|
||||
var trigger = parsed.get("trigger", {})
|
||||
if trigger.is_empty():
|
||||
return false
|
||||
|
||||
# Check event type matches
|
||||
var trigger_event = trigger.get("event", "")
|
||||
if not _event_matches(trigger_event, event_type):
|
||||
return false
|
||||
|
||||
# Check source filter
|
||||
var trigger_source = trigger.get("source", "ANY")
|
||||
if not _source_matches(trigger_source, event_data, source_card, game_state):
|
||||
return false
|
||||
|
||||
# Check additional trigger filters
|
||||
if trigger.has("source_filter"):
|
||||
var filter = trigger.source_filter
|
||||
var event_card = event_data.get("card")
|
||||
if event_card and not _matches_card_filter(event_card, filter):
|
||||
return false
|
||||
|
||||
# Check trigger condition (if present)
|
||||
var trigger_condition = trigger.get("condition", {})
|
||||
if not trigger_condition.is_empty() and condition_checker:
|
||||
var context = {
|
||||
"source_card": source_card,
|
||||
"target_card": event_data.get("card"),
|
||||
"game_state": game_state,
|
||||
"player_id": source_card.controller_index if source_card else 0,
|
||||
"event_data": event_data
|
||||
}
|
||||
if not condition_checker.evaluate(trigger_condition, context):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if event type matches trigger event
|
||||
func _event_matches(trigger_event: String, actual_event: String) -> bool:
|
||||
# Direct match
|
||||
if trigger_event == actual_event:
|
||||
return true
|
||||
|
||||
# Handle variations
|
||||
match trigger_event:
|
||||
"ENTERS_FIELD":
|
||||
return actual_event in ["ENTERS_FIELD", "CARD_PLAYED"]
|
||||
"LEAVES_FIELD":
|
||||
return actual_event in ["LEAVES_FIELD", "FORWARD_BROKEN", "CARD_BROKEN"]
|
||||
"DEALS_DAMAGE":
|
||||
return actual_event in ["DEALS_DAMAGE", "DEALS_DAMAGE_TO_OPPONENT", "DEALS_DAMAGE_TO_FORWARD"]
|
||||
"DEALS_DAMAGE_TO_OPPONENT":
|
||||
return actual_event == "DEALS_DAMAGE_TO_OPPONENT"
|
||||
"DEALS_DAMAGE_TO_FORWARD":
|
||||
return actual_event == "DEALS_DAMAGE_TO_FORWARD"
|
||||
"BLOCKS_OR_IS_BLOCKED":
|
||||
return actual_event in ["BLOCKS", "IS_BLOCKED"]
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Check if source matches trigger requirements
|
||||
func _source_matches(
|
||||
trigger_source: String,
|
||||
event_data: Dictionary,
|
||||
source_card: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var event_card = event_data.get("card")
|
||||
|
||||
match trigger_source:
|
||||
"SELF":
|
||||
# Trigger source must be this card
|
||||
return event_card == source_card
|
||||
"CONTROLLER":
|
||||
# Trigger source must be controlled by same player
|
||||
if event_card:
|
||||
return event_card.controller_index == source_card.controller_index
|
||||
var event_player = event_data.get("player", -1)
|
||||
return event_player == source_card.controller_index
|
||||
"OPPONENT":
|
||||
# Trigger source must be controlled by opponent
|
||||
if event_card:
|
||||
return event_card.controller_index != source_card.controller_index
|
||||
var event_player = event_data.get("player", -1)
|
||||
return event_player != source_card.controller_index and event_player >= 0
|
||||
"ANY", _:
|
||||
# Any source triggers
|
||||
return true
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card matches a filter
|
||||
func _matches_card_filter(card: CardInstance, filter: Dictionary) -> 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
|
||||
|
||||
# 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"):
|
||||
if card.card_data.cost < filter.cost_min:
|
||||
return false
|
||||
if filter.has("cost_max"):
|
||||
if card.card_data.cost > filter.cost_max:
|
||||
return false
|
||||
if filter.has("cost"):
|
||||
if card.card_data.cost != filter.cost:
|
||||
return false
|
||||
|
||||
# Power filters
|
||||
if filter.has("power_min"):
|
||||
if card.get_power() < filter.power_min:
|
||||
return false
|
||||
if filter.has("power_max"):
|
||||
if card.get_power() > filter.power_max:
|
||||
return false
|
||||
|
||||
# State filters
|
||||
if filter.has("is_dull"):
|
||||
if card.is_dull() != filter.is_dull:
|
||||
return false
|
||||
if filter.has("is_active"):
|
||||
if card.is_active() != filter.is_active:
|
||||
return false
|
||||
|
||||
# Name filter
|
||||
if filter.has("name"):
|
||||
if card.card_data.name != filter.name:
|
||||
return false
|
||||
|
||||
# Category filter
|
||||
if filter.has("category"):
|
||||
if card.card_data.category != filter.category:
|
||||
return false
|
||||
|
||||
# Job filter
|
||||
if filter.has("job"):
|
||||
if card.card_data.job != filter.job:
|
||||
return false
|
||||
|
||||
return true
|
||||
Reference in New Issue
Block a user