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

1808 lines
50 KiB
GDScript

class_name EffectResolver
extends RefCounted
## EffectResolver - Executes parsed effects on game state
## Handles all 58+ effect types from the ability processor
signal effect_completed(effect: Dictionary, targets: Array)
signal targeting_required(effect: Dictionary, valid_targets: Array, count: int)
signal choice_required(effect: Dictionary, options: Array)
## Reference to ConditionChecker for evaluating conditions
var condition_checker: ConditionChecker = null
## Resolve an effect
func resolve(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var effect_type = str(effect.get("type", "")).to_upper()
match effect_type:
# === CORE EFFECTS (Most Common) ===
"DAMAGE":
_resolve_damage(effect, source, targets, game_state)
"DRAW":
_resolve_draw(effect, source, game_state)
"BREAK":
_resolve_break(effect, targets, game_state)
"DULL":
_resolve_dull(effect, targets, game_state)
"ACTIVATE":
_resolve_activate(effect, targets, game_state)
"POWER_MOD":
_resolve_power_mod(effect, source, targets, game_state)
"SEARCH":
_resolve_search(effect, source, game_state)
"PLAY":
_resolve_play(effect, source, targets, game_state)
"RETURN":
_resolve_return(effect, targets, game_state)
"DISCARD":
_resolve_discard(effect, source, targets, game_state)
# === KEYWORD & ABILITY EFFECTS ===
"ABILITY_GRANT":
_resolve_ability_grant(effect, source, targets, game_state)
"KEYWORD":
_resolve_keyword(effect, source, targets, game_state)
"REMOVE_ABILITY":
_resolve_remove_ability(effect, targets, game_state)
# === ZONE MOVEMENT ===
"RETRIEVE":
_resolve_retrieve(effect, source, targets, game_state)
"REMOVE_FROM_GAME":
_resolve_remove_from_game(effect, targets, game_state)
"PUT_INTO_BREAK_ZONE":
_resolve_put_into_break_zone(effect, targets, game_state)
"ADD_TO_HAND":
_resolve_add_to_hand(effect, source, targets, game_state)
"PUT_ON_FIELD":
_resolve_put_on_field(effect, source, targets, game_state)
"PUT_INTO_DECK":
_resolve_put_into_deck(effect, source, targets, game_state)
"REVEAL_AND_ADD":
_resolve_reveal_and_add(effect, source, game_state)
# === CARD STATE EFFECTS ===
"FREEZE":
_resolve_freeze(effect, targets, game_state)
"UNFREEZE":
_resolve_unfreeze(effect, targets, game_state)
"ADD_COUNTER":
_resolve_add_counter(effect, targets, game_state)
"REMOVE_COUNTER":
_resolve_remove_counter(effect, targets, game_state)
"TRANSFORM":
_resolve_transform(effect, source, targets, game_state)
# === COMBAT EFFECTS ===
"MUTUAL_DAMAGE":
_resolve_mutual_damage(effect, source, targets, game_state)
"LETHAL":
_resolve_lethal(effect, targets, game_state)
"CANT_ATTACK":
_resolve_cant_attack(effect, targets, game_state)
"CANT_BLOCK":
_resolve_cant_block(effect, targets, game_state)
"MUST_ATTACK":
_resolve_must_attack(effect, targets, game_state)
"MUST_BLOCK":
_resolve_must_block(effect, targets, game_state)
"UNBLOCKABLE":
_resolve_unblockable(effect, source, targets, game_state)
"BRAVE":
_resolve_brave(effect, source, targets, game_state)
"HASTE":
_resolve_haste(effect, source, targets, game_state)
"FIRST_STRIKE":
_resolve_first_strike(effect, source, targets, game_state)
# === PREVENTION & PROTECTION ===
"PREVENT":
_resolve_prevent(effect, source, targets, game_state)
"PROTECTION":
_resolve_protection(effect, source, targets, game_state)
"DAMAGE_IMMUNITY":
_resolve_damage_immunity(effect, targets, game_state)
"BREAK_IMMUNITY":
_resolve_break_immunity(effect, targets, game_state)
"SELECTION_IMMUNITY":
_resolve_selection_immunity(effect, targets, game_state)
# === COST & RESOURCE EFFECTS ===
"REDUCE_COST":
_resolve_reduce_cost(effect, source, game_state)
"INCREASE_COST":
_resolve_increase_cost(effect, targets, game_state)
"GENERATE_CP":
_resolve_generate_cp(effect, source, game_state)
"PAY_COST":
_resolve_pay_cost(effect, source, game_state)
# === DECK MANIPULATION ===
"SHUFFLE":
_resolve_shuffle(effect, source, game_state)
"LOOK_AT_TOP":
_resolve_look_at_top(effect, source, game_state)
"REORDER_TOP":
_resolve_reorder_top(effect, source, targets, game_state)
"MILL":
_resolve_mill(effect, source, game_state)
# === CHOICE & MODAL EFFECTS ===
"CHOOSE":
_resolve_choose(effect, source, targets, game_state)
"CHOOSE_MODE":
_resolve_choose_mode(effect, source, game_state)
# === SPECIAL EFFECTS ===
"COPY":
_resolve_copy(effect, source, targets, game_state)
"CANCEL":
_resolve_cancel(effect, targets, game_state)
"TAKE_CONTROL":
_resolve_take_control(effect, source, targets, game_state)
"SWAP":
_resolve_swap(effect, targets, game_state)
"TAKE_EXTRA_TURN":
_resolve_take_extra_turn(effect, source, game_state)
"WIN_GAME":
_resolve_win_game(effect, source, game_state)
# === DAMAGE TO PLAYER ===
"DAMAGE_TO_OPPONENT":
_resolve_damage_to_opponent(effect, source, game_state)
"DAMAGE_TO_CONTROLLER":
_resolve_damage_to_controller(effect, source, game_state)
# === HEALING & RECOVERY ===
"HEAL":
_resolve_heal(effect, source, targets, game_state)
"REMOVE_DAMAGE":
_resolve_remove_damage(effect, targets, game_state)
# === CONTINUOUS EFFECTS (Handled by FieldEffectManager) ===
"DAMAGE_MODIFIER":
# Continuous - registered with FieldEffectManager
pass
# === PROPERTY MODIFICATION ===
"MODIFY":
_resolve_modify(effect, targets, game_state)
# === PERMISSION / CONDITIONAL ===
"PERMISSION":
_resolve_permission(effect, source, game_state)
"CONDITIONAL_EFFECT":
_resolve_conditional_effect(effect, source, targets, game_state)
"CONDITIONAL":
_resolve_conditional(effect, source, targets, game_state)
"CHAINED_EFFECT":
_resolve_chained_effect(effect, source, targets, game_state)
"SCALING_EFFECT":
_resolve_scaling_effect(effect, source, targets, game_state)
"TRIGGER_EX_BURST":
_resolve_trigger_ex_burst(effect, targets, game_state)
# === FIELD EFFECT TYPES (registered, not resolved) ===
# These are registered with FieldEffectManager when card enters field
"BLOCK_IMMUNITY", "RESTRICTION", "COST_REDUCTION", "COST_INCREASE", \
"TAUNT", "CONDITIONAL_FIELD", "GAIN_ELEMENTS", "HAS_ALL_JOBS", \
"DAMAGE_PREVENTION", "DAMAGE_PREVENTION_CONDITIONAL", "ENTERS_DULL", \
"PARTY_ANY_ELEMENT", "CAST_RESTRICTION", "CAST_RESTRICTION_CP_SOURCE", \
"CAST_RESTRICTION_CP_TYPE", "CAST_REQUIREMENT", "MULTI_ATTACK", \
"SUPPRESS_EX_BURST", "SUPPRESS_AUTO_TRIGGERS", "SUPPRESS_ACTION_ABILITIES", \
"ALLOW_MULTIPLE", "ALSO_NAMED", "ALSO_TYPE", "PLAY_RESTRICTION", \
"PRODUCE_CP", "PRODUCE_ANY_CP", "COST_ANY_ELEMENT", "ACTION_HASTE", \
"HASTE_LIKE", "MODAL":
# These are continuous effects registered by FieldEffectManager
pass
# === DAMAGE SCALING (deal X damage for each Y) ===
"DAMAGE_SCALING":
_resolve_damage_scaling(effect, source, targets, game_state)
# === OPPONENT MAY PLAY ===
"OPPONENT_MAY", "OPPONENT_MAY_PLAY":
_resolve_opponent_may(effect, source, game_state)
# === UNKNOWN / UNPARSED ===
"UNKNOWN":
push_warning("EffectResolver: Unknown effect type for: " + str(effect))
_:
push_warning("EffectResolver: Unhandled effect type: " + effect_type)
effect_completed.emit(effect, targets)
## Deal damage to targets
func _resolve_damage(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var amount = _resolve_damage_amount(effect, source, game_state)
for target in targets:
if target is CardInstance and target.is_forward():
var broken = target.apply_damage(amount)
if broken:
var player = game_state.get_player(target.controller_index)
if player:
player.break_card(target)
## Resolve damage amount, handling special values like HIGHEST_CONTROLLED_POWER
func _resolve_damage_amount(effect: Dictionary, source: CardInstance, game_state) -> int:
var amount = effect.get("amount", 0)
# Check for special dynamic amount values
if amount is String:
var amount_filter = effect.get("amount_filter", {})
match amount.to_upper():
"HIGHEST_CONTROLLED_POWER":
return _get_highest_power(source.controller_index, game_state, amount_filter)
"LOWEST_CONTROLLED_POWER":
return _get_lowest_power(source.controller_index, game_state, amount_filter)
"HIGHEST_OPPONENT_POWER":
return _get_highest_power(1 - source.controller_index, game_state, amount_filter)
"HIGHEST_CONTROLLED_COST":
return _get_highest_cost(source.controller_index, game_state, amount_filter)
_:
push_warning("EffectResolver: Unknown damage amount type: " + amount)
return 0
return int(amount)
## Draw cards
func _resolve_draw(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var amount = effect.get("amount", 1)
var target = effect.get("target", {})
var target_owner = target.get("type", "CONTROLLER")
var player_index = source.controller_index
if target_owner == "OPPONENT":
player_index = 1 - player_index
var player = game_state.get_player(player_index)
if player:
player.draw_cards(amount)
## Break targets
func _resolve_break(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
var player = game_state.get_player(target.controller_index)
if player:
player.break_card(target)
## Dull targets
func _resolve_dull(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
target.dull()
## Activate targets
func _resolve_activate(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
target.activate()
## Apply power modifier
func _resolve_power_mod(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var amount = effect.get("amount", 0)
var duration = effect.get("duration", "END_OF_TURN")
# Determine targets
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
# Add to temporary power modifiers
target.power_modifiers.append(amount)
## Search deck for cards
func _resolve_search(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
# Search requires UI interaction - for now just log
var count = effect.get("count", 1)
var filter = effect.get("filter", {})
var destination = effect.get("destination", "HAND")
print("EffectResolver: Search for %d cards matching %s -> %s" % [count, filter, destination])
# TODO: Implement search UI and card selection
## Play card from zone
func _resolve_play(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var modifiers = effect.get("modifiers", {})
var enters_dull = modifiers.get("enters_dull", false)
for target in targets:
if target is CardInstance:
var player = game_state.get_player(source.controller_index)
if player:
# Move card to field
# This is simplified - full implementation needs zone management
if target.is_forward():
player.hand.remove_card(target)
player.field_forwards.add_card(target)
target.entered_field()
if enters_dull:
target.dull()
elif target.is_backup():
player.hand.remove_card(target)
player.field_backups.add_card(target)
target.entered_field()
if enters_dull:
target.dull()
## Return cards to hand
func _resolve_return(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
var player = game_state.get_player(target.owner_index)
if player:
# Remove from field
if target.is_forward() and player.field_forwards.has_card(target):
player.field_forwards.remove_card(target)
player.hand.add_card(target)
elif target.is_backup() and player.field_backups.has_card(target):
player.field_backups.remove_card(target)
player.hand.add_card(target)
## Force discard
func _resolve_discard(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var amount = effect.get("amount", 1)
var target = effect.get("target", {})
var target_owner = target.get("type", "OPPONENT")
var player_index = source.controller_index
if target_owner == "OPPONENT":
player_index = 1 - player_index
# Discard requires UI interaction for card selection
print("EffectResolver: Player %d discards %d cards" % [player_index, amount])
# TODO: Implement discard UI
## Grant temporary ability
func _resolve_ability_grant(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var ability_name = effect.get("ability", "")
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.temporary_abilities.append(ability_name)
## Apply prevention effect
func _resolve_prevent(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var action = effect.get("action", "")
var duration = effect.get("duration", "END_OF_TURN")
# Prevention effects need state tracking on cards
# For now, log what should happen
print("EffectResolver: Prevent %s on targets until %s" % [action, duration])
# TODO: Implement prevention state tracking
## Freeze targets (prevent activation)
func _resolve_freeze(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
target.set_frozen(true)
## Unfreeze targets
func _resolve_unfreeze(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
target.set_frozen(false)
# =============================================================================
# KEYWORD & ABILITY EFFECTS
# =============================================================================
## Grant keyword ability (for triggered/temporary grants, not FIELD)
func _resolve_keyword(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var keyword = str(effect.get("keyword", "")).to_upper()
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.add_temporary_keyword(keyword, duration)
## Remove ability from targets
func _resolve_remove_ability(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var ability_name = effect.get("ability", "ALL")
for target in targets:
if target is CardInstance:
if ability_name == "ALL":
target.remove_all_abilities()
else:
target.remove_ability(ability_name)
# =============================================================================
# ZONE MOVEMENT EFFECTS
# =============================================================================
## Retrieve card from break zone to hand
func _resolve_retrieve(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
var owner_player = game_state.get_player(target.owner_index)
if owner_player and owner_player.break_zone.has_card(target):
owner_player.break_zone.remove_card(target)
owner_player.hand.add_card(target)
## Remove cards from game (exile)
func _resolve_remove_from_game(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
var owner_player = game_state.get_player(target.owner_index)
if owner_player:
owner_player.remove_card_from_game(target)
## Put cards into break zone
func _resolve_put_into_break_zone(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
var owner_player = game_state.get_player(target.owner_index)
if owner_player:
owner_player.put_card_in_break_zone(target)
## Add cards to hand (from various zones)
func _resolve_add_to_hand(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var destination_owner = effect.get("destination_owner", "OWNER")
for target in targets:
if target is CardInstance:
var player_index = target.owner_index
if destination_owner == "CONTROLLER":
player_index = source.controller_index
elif destination_owner == "OPPONENT":
player_index = 1 - source.controller_index
var player = game_state.get_player(player_index)
if player:
player.add_card_to_hand(target)
## Put card onto field from another zone
func _resolve_put_on_field(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var enters_dull = effect.get("enters_dull", false)
var enters_active = effect.get("enters_active", false)
for target in targets:
if target is CardInstance:
var player = game_state.get_player(source.controller_index)
if player:
player.put_card_on_field(target, enters_dull)
if enters_active:
target.activate()
## Put cards into deck
func _resolve_put_into_deck(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var position = effect.get("position", "BOTTOM") # TOP, BOTTOM, SHUFFLE
for target in targets:
if target is CardInstance:
var owner_player = game_state.get_player(target.owner_index)
if owner_player:
owner_player.put_card_in_deck(target, position)
## Reveal top cards and add matching to hand
func _resolve_reveal_and_add(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var count = effect.get("count", 5)
var filter = effect.get("filter", {})
var add_count = effect.get("add_count", 1)
var player = game_state.get_player(source.controller_index)
if player:
player.reveal_top_and_add(count, filter, add_count)
# =============================================================================
# CARD STATE EFFECTS
# =============================================================================
## Add counters to cards
func _resolve_add_counter(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var counter_type = effect.get("counter_type", "GENERIC")
var amount = effect.get("amount", 1)
for target in targets:
if target is CardInstance:
target.add_counters(counter_type, amount)
## Remove counters from cards
func _resolve_remove_counter(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var counter_type = effect.get("counter_type", "GENERIC")
var amount = effect.get("amount", 1)
for target in targets:
if target is CardInstance:
target.remove_counters(counter_type, amount)
## Transform card into another card
func _resolve_transform(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var into = effect.get("into", {})
for target in targets:
if target is CardInstance:
target.transform(into)
# =============================================================================
# COMBAT EFFECTS
# =============================================================================
## Deal damage equal to target's power to itself (mutual/reflected damage)
func _resolve_mutual_damage(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance and target.is_forward():
var damage = target.get_power()
var broken = target.apply_damage(damage)
if broken:
var player = game_state.get_player(target.controller_index)
if player:
player.break_card(target)
## Apply lethal effect (break regardless of power)
func _resolve_lethal(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
var player = game_state.get_player(target.controller_index)
if player:
player.break_card(target)
## Prevent targets from attacking
func _resolve_cant_attack(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_restriction("CANT_ATTACK", duration)
## Prevent targets from blocking
func _resolve_cant_block(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_restriction("CANT_BLOCK", duration)
## Force targets to attack if able
func _resolve_must_attack(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_requirement("MUST_ATTACK", duration)
## Force targets to block if able
func _resolve_must_block(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_requirement("MUST_BLOCK", duration)
## Make attacker unblockable
func _resolve_unblockable(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.add_temporary_keyword("UNBLOCKABLE", duration)
## Grant Brave
func _resolve_brave(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.add_temporary_keyword("BRAVE", duration)
## Grant Haste
func _resolve_haste(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.add_temporary_keyword("HASTE", duration)
## Grant First Strike
func _resolve_first_strike(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.add_temporary_keyword("FIRST_STRIKE", duration)
# =============================================================================
# PREVENTION & PROTECTION EFFECTS
# =============================================================================
## Protection from damage/effects
func _resolve_protection(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var from = effect.get("from", "ALL")
var duration = effect.get("duration", "END_OF_TURN")
var affected = targets if targets.size() > 0 else [source]
for target in affected:
if target is CardInstance:
target.add_protection(from, duration)
## Damage immunity
func _resolve_damage_immunity(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_protection("DAMAGE", duration)
## Break immunity
func _resolve_break_immunity(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_protection("BREAK", duration)
## Selection immunity (can't be chosen)
func _resolve_selection_immunity(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var from = effect.get("from", "OPPONENT")
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
target.add_protection("SELECTION_" + from, duration)
# =============================================================================
# COST & RESOURCE EFFECTS
# =============================================================================
## Reduce cost of playing cards
func _resolve_reduce_cost(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var amount = effect.get("amount", 1)
var filter = effect.get("filter", {})
var duration = effect.get("duration", "END_OF_TURN")
var player = game_state.get_player(source.controller_index)
if player:
player.add_cost_modifier(-amount, filter, duration)
## Increase cost of playing cards
func _resolve_increase_cost(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var amount = effect.get("amount", 1)
var filter = effect.get("filter", {})
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
var player = game_state.get_player(target.controller_index)
if player:
player.add_cost_modifier(amount, filter, duration)
## Generate CP
func _resolve_generate_cp(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var element = effect.get("element", "ANY")
var amount = effect.get("amount", 1)
var player = game_state.get_player(source.controller_index)
if player:
player.add_cp(element, amount)
## Pay cost (as part of ability)
func _resolve_pay_cost(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var cost_type = effect.get("cost_type", "CP")
var amount = effect.get("amount", 0)
var player = game_state.get_player(source.controller_index)
if player:
match cost_type:
"CP":
player.spend_cp(amount)
"DISCARD":
pass # Handled by separate discard selection
"DULL_SELF":
source.dull()
# =============================================================================
# DECK MANIPULATION EFFECTS
# =============================================================================
## Shuffle deck
func _resolve_shuffle(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var target_owner = effect.get("owner", "CONTROLLER")
var player_index = source.controller_index
if target_owner == "OPPONENT":
player_index = 1 - player_index
var player = game_state.get_player(player_index)
if player:
player.deck.shuffle()
## Look at top cards of deck
func _resolve_look_at_top(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var count = effect.get("count", 1)
var player = game_state.get_player(source.controller_index)
if player:
var cards = player.deck.peek_top(count)
# This emits a signal for UI to handle revealing
game_state.emit_signal("cards_revealed", source.controller_index, cards)
## Reorder top cards of deck
func _resolve_reorder_top(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var player = game_state.get_player(source.controller_index)
if player:
# targets contains the cards in the desired order
player.deck.reorder_top(targets)
## Mill cards from deck to break zone
func _resolve_mill(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var count = effect.get("amount", 1)
var target_owner = effect.get("owner", "OPPONENT")
var player_index = source.controller_index
if target_owner == "OPPONENT":
player_index = 1 - player_index
var player = game_state.get_player(player_index)
if player:
player.mill(count)
# =============================================================================
# CHOICE & MODAL EFFECTS
# =============================================================================
## Handle choose effect (user selects targets)
func _resolve_choose(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
# If targets already provided, process the chosen effect
if targets.size() > 0:
var chosen_effect = effect.get("effect", {})
if not chosen_effect.is_empty():
resolve(chosen_effect, source, targets, game_state)
## Handle modal choice (choose one of several effects)
func _resolve_choose_mode(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var modes = effect.get("modes", [])
# Emit signal for UI to present choices
choice_required.emit(effect, modes)
# =============================================================================
# SPECIAL EFFECTS
# =============================================================================
## Copy an ability or card
func _resolve_copy(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var copy_type = effect.get("copy_type", "ABILITY")
for target in targets:
if target is CardInstance:
if copy_type == "ABILITY":
source.copy_abilities_from(target)
elif copy_type == "STATS":
source.copy_stats_from(target)
elif copy_type == "CARD":
source.become_copy_of(target)
## Cancel an ability or summon on the stack
func _resolve_cancel(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is Dictionary: # Stack item
game_state.cancel_stack_item(target)
## Take control of target
func _resolve_take_control(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var duration = effect.get("duration", "PERMANENT")
for target in targets:
if target is CardInstance:
game_state.transfer_control(target, source.controller_index, duration)
## Swap two cards or values
func _resolve_swap(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var swap_type = effect.get("swap_type", "POSITION")
if targets.size() >= 2:
var card_a = targets[0]
var card_b = targets[1]
match swap_type:
"POSITION":
game_state.swap_positions(card_a, card_b)
"POWER":
var power_a = card_a.base_power
var power_b = card_b.base_power
card_a.set_base_power(power_b)
card_b.set_base_power(power_a)
"CONTROL":
game_state.swap_control(card_a, card_b)
## Take an extra turn
func _resolve_take_extra_turn(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var player = game_state.get_player(source.controller_index)
if player:
game_state.add_extra_turn(source.controller_index)
## Win the game
func _resolve_win_game(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var condition = effect.get("condition", "UNCONDITIONAL")
# Check if condition is met
if condition == "UNCONDITIONAL":
game_state.declare_winner(source.controller_index)
# =============================================================================
# DAMAGE TO PLAYER EFFECTS
# =============================================================================
## Deal damage directly to opponent (damage zone)
func _resolve_damage_to_opponent(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var amount = effect.get("amount", 1)
var opponent_index = 1 - source.controller_index
var opponent = game_state.get_player(opponent_index)
if opponent:
opponent.take_damage(amount)
## Deal damage directly to controller
func _resolve_damage_to_controller(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var amount = effect.get("amount", 1)
var player = game_state.get_player(source.controller_index)
if player:
player.take_damage(amount)
# =============================================================================
# HEALING & RECOVERY EFFECTS
# =============================================================================
## Heal/restore damage
func _resolve_heal(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var amount = effect.get("amount", 1)
for target in targets:
if target is CardInstance:
target.heal_damage(amount)
## Remove all damage from targets
func _resolve_remove_damage(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
target.remove_all_damage()
# =============================================================================
# PROPERTY MODIFICATION EFFECTS
# =============================================================================
## Modify card properties (element, job, etc.)
func _resolve_modify(
effect: Dictionary,
targets: Array,
game_state
) -> void:
var property = effect.get("property", "")
var new_value = effect.get("new_element", effect.get("job", ""))
var duration = effect.get("duration", "END_OF_TURN")
for target in targets:
if target is CardInstance:
match property:
"ELEMENT":
target.set_temporary_element(new_value, duration)
"JOB":
target.set_temporary_job(new_value, duration)
# =============================================================================
# PERMISSION & CONDITIONAL EFFECTS
# =============================================================================
## Grant permission to perform action
func _resolve_permission(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var action = effect.get("action", "")
var duration = effect.get("duration", "END_OF_TURN")
var player = game_state.get_player(source.controller_index)
if player:
match action:
"CAST":
# Allow casting from non-standard zone
player.add_cast_permission(effect, duration)
## Resolve conditional effect wrapper (old format)
func _resolve_conditional_effect(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var condition = effect.get("condition", "")
# Check if condition is met using legacy check
var condition_met = _check_condition_legacy(condition, effect, source, game_state)
if condition_met:
# Execute the wrapped effect
var inner_effect = effect.get("effect", {})
if not inner_effect.is_empty():
resolve(inner_effect, source, targets, game_state)
## Resolve CONDITIONAL effect (new format with condition object)
func _resolve_conditional(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var condition = effect.get("condition", {})
# Build context for condition evaluation
var context = _build_condition_context(source, targets, game_state)
# Use ConditionChecker if available
var condition_met = false
if condition_checker:
condition_met = condition_checker.evaluate(condition, context)
else:
# Fallback to legacy check if no ConditionChecker
condition_met = _check_condition_legacy(condition.get("type", ""), effect, source, game_state)
if condition_met:
# Execute then_effects
var then_effects: Array = effect.get("then_effects", [])
for sub_effect in then_effects:
resolve(sub_effect, source, targets, game_state)
else:
# Execute else_effects (if any)
var else_effects: Array = effect.get("else_effects", [])
for sub_effect in else_effects:
resolve(sub_effect, source, targets, game_state)
## Resolve CHAINED_EFFECT - "If you do so" patterns
func _resolve_chained_effect(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var primary_effect = effect.get("primary_effect", {})
var chain_effects: Array = effect.get("chain_effects", [])
if primary_effect.is_empty():
return
# Execute primary effect and track if it succeeded
var primary_result = _execute_with_tracking(primary_effect, source, targets, game_state)
# Only execute chain effects if primary succeeded
if primary_result.get("success", false):
for chain_effect in chain_effects:
resolve(chain_effect, source, targets, game_state)
## Resolve SCALING_EFFECT - "for each X" patterns
func _resolve_scaling_effect(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var base_effect = effect.get("base_effect", {})
var scale_by = str(effect.get("scale_by", "")).to_upper()
var scale_filter = effect.get("scale_filter", {})
var multiplier = effect.get("multiplier", 1)
if base_effect.is_empty():
return
# Determine the scale value (with optional filter)
var scale_value = _get_scale_value(scale_by, source, game_state, scale_filter)
# Create scaled effect
var scaled_effect = base_effect.duplicate(true)
# Apply scaling based on effect type
if scaled_effect.has("amount"):
scaled_effect["amount"] = scale_value * multiplier
# Resolve the scaled effect
resolve(scaled_effect, source, targets, game_state)
## Execute an effect and track success for chaining
func _execute_with_tracking(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> Dictionary:
var effect_type = str(effect.get("type", "")).to_upper()
var result = {"success": false}
# Different effects have different success criteria
match effect_type:
"DISCARD":
# Discard succeeds if player had cards to discard
var target_info = effect.get("target", {})
var player_index = source.controller_index
if target_info.get("type", "") == "OPPONENT":
player_index = 1 - player_index
var player = game_state.get_player(player_index) if game_state else null
if player and player.hand.size() > 0:
resolve(effect, source, targets, game_state)
result["success"] = true
"DULL":
# Dull succeeds if there were targets to dull
if targets.size() > 0:
resolve(effect, source, targets, game_state)
result["success"] = true
elif effect.get("target", {}).get("type") == "SELF" and source:
resolve(effect, source, [source], game_state)
result["success"] = true
"PAY_COST":
# Pay cost succeeds if cost could be paid
# For now, assume success - actual cost checking would be more complex
resolve(effect, source, targets, game_state)
result["success"] = true
"SEARCH":
# Search generally succeeds (even if no cards found, the search happened)
resolve(effect, source, targets, game_state)
result["success"] = true
"DRAW":
# Draw succeeds if player has cards in deck
var player = game_state.get_player(source.controller_index) if game_state else null
if player and player.deck.size() > 0:
resolve(effect, source, targets, game_state)
result["success"] = true
_:
# Default: resolve and assume success
resolve(effect, source, targets, game_state)
result["success"] = true
return result
## Get scale value based on scale_by type (with optional filter)
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()
# 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)
"MONSTERS_CONTROLLED", "MONSTERS":
cards_to_count = _get_monsters_for_owner(owner, player_index, game_state)
_:
push_warning("EffectResolver: Unknown scale_by type: " + scale_by)
return 0
# If no filter, just return count
if 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
# =============================================================================
## Get forwards for specified owner
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 []
"ANY", _:
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
## Get backups for specified owner
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 []
"ANY", _:
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
## Get all field cards for specified owner
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
## Get hand cards for specified owner
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 []
"ANY", _:
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
## Get break zone cards for specified owner
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 []
"ANY", _:
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
## Get damage for specified owner
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
"ANY", _:
var total = 0
for p in game_state.players:
if p and "damage" in p:
total += p.damage
return total
## Get monster cards for specified owner
func _get_monsters_for_owner(owner: String, player_index: int, game_state) -> Array:
# Monsters are typically on the field like forwards
var all_field = _get_field_cards_for_owner(owner, player_index, game_state)
var monsters = []
for card in all_field:
if card.is_monster():
monsters.append(card)
return monsters
# =============================================================================
# HIGHEST/LOWEST VALUE QUERIES
# =============================================================================
## Get highest power among forwards controlled by player (with optional filter)
func _get_highest_power(player_index: int, game_state, filter: Dictionary = {}) -> int:
var forwards = _get_forwards_for_owner("CONTROLLER", player_index, game_state)
return CardFilter.get_highest_power(forwards, filter)
## Get lowest power among forwards controlled by player (with optional filter)
func _get_lowest_power(player_index: int, game_state, filter: Dictionary = {}) -> int:
var forwards = _get_forwards_for_owner("CONTROLLER", player_index, game_state)
return CardFilter.get_lowest_power(forwards, filter)
## Get highest cost among forwards controlled by player (with optional filter)
func _get_highest_cost(player_index: int, game_state, filter: Dictionary = {}) -> int:
var forwards = _get_forwards_for_owner("CONTROLLER", player_index, game_state)
var highest = 0
for card in forwards:
if filter.is_empty() or CardFilter.matches_filter(card, filter):
var cost = card.card_data.cost if card.card_data else 0
if cost > highest:
highest = cost
return highest
## Get lowest cost among forwards controlled by player (with optional filter)
func _get_lowest_cost(player_index: int, game_state, filter: Dictionary = {}) -> int:
var forwards = _get_forwards_for_owner("CONTROLLER", player_index, game_state)
var lowest = -1
for card in forwards:
if filter.is_empty() or CardFilter.matches_filter(card, filter):
var cost = card.card_data.cost if card.card_data else 0
if lowest == -1 or cost < lowest:
lowest = cost
return lowest if lowest != -1 else 0
## Get forward with highest cost controlled by player (for targeting)
func _get_forward_with_highest_cost(player_index: int, game_state, filter: Dictionary = {}) -> CardInstance:
var forwards = _get_forwards_for_owner("CONTROLLER", player_index, game_state)
var highest_cost = -1
var highest_card: CardInstance = null
for card in forwards:
if filter.is_empty() or CardFilter.matches_filter(card, filter):
var cost = card.card_data.cost if card.card_data else 0
if cost > highest_cost:
highest_cost = cost
highest_card = card
return highest_card
## Get forward with lowest cost controlled by player (for targeting)
func _get_forward_with_lowest_cost(player_index: int, game_state, filter: Dictionary = {}) -> CardInstance:
var forwards = _get_forwards_for_owner("CONTROLLER", player_index, game_state)
var lowest_cost = -1
var lowest_card: CardInstance = null
for card in forwards:
if filter.is_empty() or CardFilter.matches_filter(card, filter):
var cost = card.card_data.cost if card.card_data else 0
if lowest_cost == -1 or cost < lowest_cost:
lowest_cost = cost
lowest_card = card
return lowest_card
## Build context dictionary for ConditionChecker
func _build_condition_context(
source: CardInstance,
targets: Array,
game_state
) -> Dictionary:
return {
"source_card": source,
"target_card": targets[0] if targets.size() > 0 else null,
"targets": targets,
"game_state": game_state,
"player_id": source.controller_index if source else 0
}
## Check if a condition is met (legacy format)
func _check_condition_legacy(
condition: String,
effect: Dictionary,
source: CardInstance,
game_state
) -> bool:
match condition:
"CONTROL_COUNT":
var required = effect.get("count", 0)
var player = game_state.get_player(source.controller_index)
if player:
var count = player.field_forwards.size() + player.field_backups.size()
return count >= required
"COST_COMPARISON":
# Would need more context about what we're comparing
return true
_:
# Unknown condition, assume true
return true
## Trigger EX BURST on a card
func _resolve_trigger_ex_burst(
effect: Dictionary,
targets: Array,
game_state
) -> void:
for target in targets:
if target is CardInstance:
# Signal AbilitySystem to trigger the card's EX BURST
var tree = Engine.get_main_loop()
if tree and tree.root and tree.root.has_node("AbilitySystem"):
var ability_system = tree.root.get_node("AbilitySystem")
ability_system.trigger_ex_burst_on_card(target)
# =============================================================================
# DAMAGE SCALING EFFECTS
# =============================================================================
## Deal X damage for each Y you control
func _resolve_damage_scaling(
effect: Dictionary,
source: CardInstance,
targets: Array,
game_state
) -> void:
var damage_per = effect.get("damage_per", 1000)
var count_filter = effect.get("count_filter", "")
# Count matching cards
var count = _count_matching_cards(count_filter, source, game_state)
var total_damage = damage_per * count
for target in targets:
if target is CardInstance and target.is_forward():
var broken = target.apply_damage(total_damage)
if broken:
var player = game_state.get_player(target.controller_index)
if player:
player.break_card(target)
## Count cards matching a filter description
func _count_matching_cards(
filter_text: String,
source: CardInstance,
game_state
) -> int:
var player = game_state.get_player(source.controller_index)
if not player:
return 0
var count = 0
var filter_lower = filter_text.to_lower()
# Count forwards
for card in player.field_forwards.get_cards():
if _card_matches_text_filter(card, filter_lower):
count += 1
# Count backups
for card in player.field_backups.get_cards():
if _card_matches_text_filter(card, filter_lower):
count += 1
return count
## Check if card matches a text filter
func _card_matches_text_filter(card: CardInstance, filter_text: String) -> bool:
if not card or not card.card_data:
return false
# Check job
if "job" in filter_text:
var job_match = filter_text.find("job ")
if job_match >= 0:
var job_name = filter_text.substr(job_match + 4).split(" ")[0]
if job_name.to_lower() in card.card_data.job.to_lower():
return true
# Check card name
if "card name" in filter_text:
var name_start = filter_text.find("card name ")
if name_start >= 0:
var card_name = filter_text.substr(name_start + 10).split(" ")[0]
if card_name.to_lower() in card.card_data.name.to_lower():
return true
# Check category
if "category" in filter_text:
if card.card_data.category.to_lower() in filter_text:
return true
# Check element
for element in card.get_elements():
if Enums.element_to_string(element).to_lower() in filter_text:
return true
# Check card type
if card.is_forward() and "forward" in filter_text:
return true
if card.is_backup() and "backup" in filter_text:
return true
return false
# =============================================================================
# OPPONENT MAY EFFECTS
# =============================================================================
## Handle "opponent may play X" effects
func _resolve_opponent_may(
effect: Dictionary,
source: CardInstance,
game_state
) -> void:
var action = effect.get("action", "")
# Signal that opponent has an optional action
var opponent_index = 1 - source.controller_index
var opponent = game_state.get_player(opponent_index)
if opponent:
# This would trigger UI for opponent to make a choice
game_state.emit_signal("opponent_may_action", opponent_index, effect)