1808 lines
50 KiB
GDScript
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)
|