feature updates
This commit is contained in:
@@ -25,6 +25,17 @@ var zone_type: Enums.ZoneType = Enums.ZoneType.DECK
|
||||
# Temporary effects (cleared at end of turn)
|
||||
var power_modifiers: Array[int] = []
|
||||
var temporary_abilities: Array = []
|
||||
var temporary_keywords: Dictionary = {} # keyword -> duration
|
||||
var restrictions: Dictionary = {} # restriction_type -> duration
|
||||
var requirements: Dictionary = {} # requirement_type -> duration
|
||||
var protections: Dictionary = {} # protection_type -> duration
|
||||
|
||||
# Counters
|
||||
var counters: Dictionary = {} # counter_type -> count
|
||||
|
||||
# Special states
|
||||
var is_frozen: bool = false
|
||||
var base_power_override: int = -1 # -1 means use card_data.power
|
||||
|
||||
# Turn tracking
|
||||
var turns_on_field: int = 0
|
||||
@@ -43,11 +54,18 @@ func _init(data: CardDatabase.CardData = null, owner: int = 0) -> void:
|
||||
if data:
|
||||
current_power = data.power
|
||||
|
||||
## Get the card's current power (base + modifiers)
|
||||
## Get the card's current power (base + modifiers + field effects)
|
||||
func get_power() -> int:
|
||||
var total = current_power
|
||||
for mod in power_modifiers:
|
||||
total += mod
|
||||
|
||||
# Add field effect modifiers from AbilitySystem
|
||||
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")
|
||||
total += ability_system.get_field_power_modifier(self)
|
||||
|
||||
return max(0, total)
|
||||
|
||||
## Check if this is a Forward
|
||||
@@ -76,8 +94,18 @@ func dull() -> void:
|
||||
|
||||
## Activate this card
|
||||
func activate() -> void:
|
||||
# Frozen cards can't activate during Active Phase (but can be activated by effects)
|
||||
state = Enums.CardState.ACTIVE
|
||||
|
||||
|
||||
## Attempt to activate during Active Phase (respects frozen)
|
||||
func activate_during_active_phase() -> bool:
|
||||
if is_frozen:
|
||||
is_frozen = false # Frozen wears off but card stays dull
|
||||
return false
|
||||
state = Enums.CardState.ACTIVE
|
||||
return true
|
||||
|
||||
## Check if this card can attack
|
||||
func can_attack() -> bool:
|
||||
if not is_forward():
|
||||
@@ -86,19 +114,34 @@ func can_attack() -> bool:
|
||||
return false
|
||||
if attacked_this_turn:
|
||||
return false
|
||||
if has_restriction("CANT_ATTACK"):
|
||||
return false
|
||||
# Must have been on field since start of turn (or have Haste)
|
||||
if turns_on_field < 1 and not has_haste():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Check if this card must attack (if able)
|
||||
func must_attack() -> bool:
|
||||
return has_requirement("MUST_ATTACK")
|
||||
|
||||
|
||||
## Check if this card can block
|
||||
func can_block() -> bool:
|
||||
if not is_forward():
|
||||
return false
|
||||
if is_dull():
|
||||
return false
|
||||
if has_restriction("CANT_BLOCK"):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Check if this card must block (if able)
|
||||
func must_block() -> bool:
|
||||
return has_requirement("MUST_BLOCK")
|
||||
|
||||
## Check if this card can use dull abilities
|
||||
func can_use_dull_ability() -> bool:
|
||||
# Must have been on field since start of turn (or have Haste)
|
||||
@@ -109,14 +152,21 @@ func can_use_dull_ability() -> bool:
|
||||
return false
|
||||
return true
|
||||
|
||||
## Check if card has Haste (from abilities)
|
||||
## Check if card has Haste
|
||||
func has_haste() -> bool:
|
||||
if not card_data:
|
||||
return false
|
||||
# Check explicit has_haste field first
|
||||
if card_data.has_haste:
|
||||
return true
|
||||
# Fallback: search ability text for backwards compatibility
|
||||
for ability in card_data.abilities:
|
||||
if ability.type == Enums.AbilityType.FIELD:
|
||||
if "haste" in ability.effect.to_lower():
|
||||
return true
|
||||
# Check field-granted keywords
|
||||
if _has_field_keyword("HASTE"):
|
||||
return true
|
||||
return false
|
||||
|
||||
## Check if card has Brave (from abilities)
|
||||
@@ -127,6 +177,9 @@ func has_brave() -> bool:
|
||||
if ability.type == Enums.AbilityType.FIELD:
|
||||
if "brave" in ability.effect.to_lower():
|
||||
return true
|
||||
# Check field-granted keywords
|
||||
if _has_field_keyword("BRAVE"):
|
||||
return true
|
||||
return false
|
||||
|
||||
## Check if card has First Strike
|
||||
@@ -137,6 +190,17 @@ func has_first_strike() -> bool:
|
||||
if ability.type == Enums.AbilityType.FIELD:
|
||||
if "first strike" in ability.effect.to_lower():
|
||||
return true
|
||||
# Check field-granted keywords
|
||||
if _has_field_keyword("FIRST_STRIKE"):
|
||||
return true
|
||||
return false
|
||||
|
||||
## Check for field-granted keyword from AbilitySystem
|
||||
func _has_field_keyword(keyword: String) -> bool:
|
||||
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")
|
||||
return ability_system.has_field_keyword(self, keyword)
|
||||
return false
|
||||
|
||||
## Get primary element
|
||||
@@ -191,3 +255,288 @@ func get_display_name() -> String:
|
||||
|
||||
func _to_string() -> String:
|
||||
return "[CardInstance: %s (%s)]" % [get_display_name(), instance_id]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# KEYWORD MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
## Add a temporary keyword
|
||||
func add_temporary_keyword(keyword: String, duration: String = "END_OF_TURN") -> void:
|
||||
temporary_keywords[keyword.to_upper()] = duration
|
||||
|
||||
|
||||
## Check if card has a keyword (from card data, temp, or field effects)
|
||||
func has_keyword(keyword: String) -> bool:
|
||||
var kw_upper = keyword.to_upper()
|
||||
|
||||
# Check temporary keywords
|
||||
if temporary_keywords.has(kw_upper):
|
||||
return true
|
||||
|
||||
# Check field-granted keywords
|
||||
if _has_field_keyword(kw_upper):
|
||||
return true
|
||||
|
||||
# Check card's base keywords
|
||||
if card_data:
|
||||
match kw_upper:
|
||||
"HASTE":
|
||||
return card_data.has_haste
|
||||
"BRAVE":
|
||||
for ability in card_data.abilities:
|
||||
if ability.type == Enums.AbilityType.FIELD and "brave" in ability.effect.to_lower():
|
||||
return true
|
||||
"FIRST_STRIKE":
|
||||
for ability in card_data.abilities:
|
||||
if ability.type == Enums.AbilityType.FIELD and "first strike" in ability.effect.to_lower():
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Remove all abilities
|
||||
func remove_all_abilities() -> void:
|
||||
temporary_abilities.clear()
|
||||
temporary_keywords.clear()
|
||||
|
||||
|
||||
## Remove a specific ability
|
||||
func remove_ability(ability_name: String) -> void:
|
||||
var name_upper = ability_name.to_upper()
|
||||
temporary_abilities.erase(name_upper)
|
||||
temporary_keywords.erase(name_upper)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RESTRICTIONS & REQUIREMENTS
|
||||
# =============================================================================
|
||||
|
||||
## Add a restriction (can't attack, can't block, etc.)
|
||||
func add_restriction(restriction_type: String, duration: String = "END_OF_TURN") -> void:
|
||||
restrictions[restriction_type.to_upper()] = duration
|
||||
|
||||
|
||||
## Check if card has a restriction
|
||||
func has_restriction(restriction_type: String) -> bool:
|
||||
return restrictions.has(restriction_type.to_upper())
|
||||
|
||||
|
||||
## Add a requirement (must attack, must block, etc.)
|
||||
func add_requirement(requirement_type: String, duration: String = "END_OF_TURN") -> void:
|
||||
requirements[requirement_type.to_upper()] = duration
|
||||
|
||||
|
||||
## Check if card has a requirement
|
||||
func has_requirement(requirement_type: String) -> bool:
|
||||
return requirements.has(requirement_type.to_upper())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PROTECTION
|
||||
# =============================================================================
|
||||
|
||||
## Add protection from damage/effects
|
||||
func add_protection(protection_type: String, duration: String = "END_OF_TURN") -> void:
|
||||
protections[protection_type.to_upper()] = duration
|
||||
|
||||
|
||||
## Check if card has protection from something
|
||||
func has_protection(protection_type: String) -> bool:
|
||||
var pt_upper = protection_type.to_upper()
|
||||
|
||||
# Check local protections
|
||||
if protections.has(pt_upper):
|
||||
return true
|
||||
|
||||
# Check for ALL protection
|
||||
if protections.has("ALL"):
|
||||
return true
|
||||
|
||||
# Check field-granted protection
|
||||
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")
|
||||
return ability_system.has_field_protection(self, protection_type)
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FROZEN STATE
|
||||
# =============================================================================
|
||||
|
||||
## Set frozen state
|
||||
func set_frozen(frozen: bool) -> void:
|
||||
is_frozen = frozen
|
||||
|
||||
|
||||
## Check if frozen (doesn't activate during Active Phase)
|
||||
func is_card_frozen() -> bool:
|
||||
return is_frozen
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COUNTERS
|
||||
# =============================================================================
|
||||
|
||||
## Add counters
|
||||
func add_counters(counter_type: String, amount: int = 1) -> void:
|
||||
var ct = counter_type.to_upper()
|
||||
counters[ct] = counters.get(ct, 0) + amount
|
||||
|
||||
|
||||
## Remove counters
|
||||
func remove_counters(counter_type: String, amount: int = 1) -> void:
|
||||
var ct = counter_type.to_upper()
|
||||
counters[ct] = max(0, counters.get(ct, 0) - amount)
|
||||
if counters[ct] == 0:
|
||||
counters.erase(ct)
|
||||
|
||||
|
||||
## Get counter count
|
||||
func get_counter_count(counter_type: String) -> int:
|
||||
return counters.get(counter_type.to_upper(), 0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POWER MANIPULATION
|
||||
# =============================================================================
|
||||
|
||||
## Set base power (for swap/transform effects)
|
||||
func set_base_power(new_power: int) -> void:
|
||||
base_power_override = new_power
|
||||
|
||||
|
||||
## Get base power (respecting override)
|
||||
func get_base_power() -> int:
|
||||
if base_power_override >= 0:
|
||||
return base_power_override
|
||||
return card_data.power if card_data else 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DAMAGE MANIPULATION
|
||||
# =============================================================================
|
||||
|
||||
## Heal damage
|
||||
func heal_damage(amount: int) -> void:
|
||||
damage_received = max(0, damage_received - amount)
|
||||
|
||||
|
||||
## Remove all damage
|
||||
func remove_all_damage() -> void:
|
||||
damage_received = 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COPY & TRANSFORM
|
||||
# =============================================================================
|
||||
|
||||
## Copy abilities from another card
|
||||
func copy_abilities_from(other: CardInstance) -> void:
|
||||
if other and other.card_data:
|
||||
for ability in other.card_data.abilities:
|
||||
temporary_abilities.append(ability)
|
||||
|
||||
|
||||
## Copy stats from another card
|
||||
func copy_stats_from(other: CardInstance) -> void:
|
||||
if other:
|
||||
base_power_override = other.get_base_power()
|
||||
|
||||
|
||||
## Become a copy of another card
|
||||
func become_copy_of(other: CardInstance) -> void:
|
||||
if other:
|
||||
copy_stats_from(other)
|
||||
copy_abilities_from(other)
|
||||
|
||||
|
||||
## Transform into something else
|
||||
func transform(into: Dictionary) -> void:
|
||||
if into.has("power"):
|
||||
base_power_override = int(into.power)
|
||||
if into.has("name"):
|
||||
# Transform name handling would require additional infrastructure
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PROPERTY MODIFICATION
|
||||
# =============================================================================
|
||||
|
||||
# Temporary element/job storage
|
||||
var _temp_element: String = ""
|
||||
var _temp_element_duration: String = ""
|
||||
var _temp_job: String = ""
|
||||
var _temp_job_duration: String = ""
|
||||
|
||||
|
||||
## Set temporary element
|
||||
func set_temporary_element(element: String, duration: String = "END_OF_TURN") -> void:
|
||||
_temp_element = element.to_upper()
|
||||
_temp_element_duration = duration
|
||||
|
||||
|
||||
## Set temporary job
|
||||
func set_temporary_job(job: String, duration: String = "END_OF_TURN") -> void:
|
||||
_temp_job = job
|
||||
_temp_job_duration = duration
|
||||
|
||||
|
||||
## Get current elements (including temporary)
|
||||
func get_current_elements() -> Array:
|
||||
if _temp_element != "":
|
||||
var element = Enums.element_from_string(_temp_element)
|
||||
return [element]
|
||||
return get_elements()
|
||||
|
||||
|
||||
## Get current job (including temporary)
|
||||
func get_current_job() -> String:
|
||||
if _temp_job != "":
|
||||
return _temp_job
|
||||
return card_data.job if card_data else ""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLEANUP
|
||||
# =============================================================================
|
||||
|
||||
## Reset temporary effects at end of turn (extended)
|
||||
func end_turn_cleanup() -> void:
|
||||
power_modifiers.clear()
|
||||
temporary_abilities.clear()
|
||||
damage_received = 0
|
||||
attacked_this_turn = false
|
||||
|
||||
# Clear END_OF_TURN duration effects
|
||||
_clear_duration_effects("END_OF_TURN")
|
||||
|
||||
|
||||
## Clear effects with specific duration
|
||||
func _clear_duration_effects(duration: String) -> void:
|
||||
for key in temporary_keywords.keys():
|
||||
if temporary_keywords[key] == duration:
|
||||
temporary_keywords.erase(key)
|
||||
|
||||
for key in restrictions.keys():
|
||||
if restrictions[key] == duration:
|
||||
restrictions.erase(key)
|
||||
|
||||
for key in requirements.keys():
|
||||
if requirements[key] == duration:
|
||||
requirements.erase(key)
|
||||
|
||||
for key in protections.keys():
|
||||
if protections[key] == duration:
|
||||
protections.erase(key)
|
||||
|
||||
# Clear temporary element/job
|
||||
if _temp_element_duration == duration:
|
||||
_temp_element = ""
|
||||
_temp_element_duration = ""
|
||||
if _temp_job_duration == duration:
|
||||
_temp_job = ""
|
||||
_temp_job_duration = ""
|
||||
|
||||
@@ -41,6 +41,7 @@ enum TurnPhase {
|
||||
|
||||
## Attack Phase Steps
|
||||
enum AttackStep {
|
||||
NONE, # Not in attack phase or between attacks
|
||||
PREPARATION,
|
||||
DECLARATION,
|
||||
BLOCK_DECLARATION,
|
||||
|
||||
@@ -63,6 +63,17 @@ func start_game(first_player: int = -1) -> void:
|
||||
players[1].draw_cards(5)
|
||||
|
||||
game_active = true
|
||||
|
||||
# Connect ability system if available
|
||||
var ability_system = Engine.get_singleton("AbilitySystem")
|
||||
if ability_system == null:
|
||||
# Try getting from scene tree (autoload)
|
||||
var tree = Engine.get_main_loop()
|
||||
if tree and tree.root.has_node("AbilitySystem"):
|
||||
ability_system = tree.root.get_node("AbilitySystem")
|
||||
if ability_system:
|
||||
ability_system.connect_to_game(self)
|
||||
|
||||
turn_manager.start_game(first_player)
|
||||
|
||||
game_started.emit()
|
||||
|
||||
798
scripts/game/abilities/AbilitySystem.gd
Normal file
798
scripts/game/abilities/AbilitySystem.gd
Normal file
@@ -0,0 +1,798 @@
|
||||
class_name AbilitySystem
|
||||
extends Node
|
||||
|
||||
## AbilitySystem - Central coordinator for ability processing
|
||||
## Loads processed abilities and handles trigger matching and effect resolution
|
||||
|
||||
signal ability_triggered(source: CardInstance, ability: Dictionary)
|
||||
signal effect_resolved(effect: Dictionary, targets: Array)
|
||||
signal targeting_required(effect: Dictionary, valid_targets: Array, callback: Callable)
|
||||
signal targeting_completed(effect: Dictionary, selected_targets: Array)
|
||||
signal choice_modal_required(effect: Dictionary, modes: Array, callback: Callable)
|
||||
signal optional_effect_prompt(player_index: int, effect: Dictionary, description: String, callback: Callable)
|
||||
|
||||
const ABILITIES_PATH = "res://data/abilities_processed.json"
|
||||
|
||||
# Loaded ability data
|
||||
var _abilities: Dictionary = {} # card_id -> Array of parsed abilities
|
||||
var _version: String = ""
|
||||
var _stats: Dictionary = {}
|
||||
|
||||
# Sub-systems
|
||||
var trigger_matcher: TriggerMatcher
|
||||
var effect_resolver: EffectResolver
|
||||
var target_selector: TargetSelector
|
||||
var field_effect_manager: FieldEffectManager
|
||||
var condition_checker: ConditionChecker
|
||||
|
||||
# UI Reference
|
||||
var choice_modal: ChoiceModal = null
|
||||
|
||||
# Effect resolution stack
|
||||
var _pending_effects: Array = []
|
||||
var _is_resolving: bool = false
|
||||
var _waiting_for_choice: bool = false
|
||||
var _waiting_for_optional: bool = false
|
||||
|
||||
# Connected game state
|
||||
var _game_state = null # GameState reference
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_load_abilities()
|
||||
_init_subsystems()
|
||||
|
||||
|
||||
func _init_subsystems() -> void:
|
||||
trigger_matcher = TriggerMatcher.new()
|
||||
effect_resolver = EffectResolver.new()
|
||||
target_selector = TargetSelector.new()
|
||||
field_effect_manager = FieldEffectManager.new()
|
||||
condition_checker = ConditionChecker.new()
|
||||
|
||||
# Wire ConditionChecker to subsystems that need it
|
||||
effect_resolver.condition_checker = condition_checker
|
||||
trigger_matcher.condition_checker = condition_checker
|
||||
|
||||
# Connect effect resolver signals
|
||||
effect_resolver.effect_completed.connect(_on_effect_completed)
|
||||
effect_resolver.choice_required.connect(_on_choice_required)
|
||||
|
||||
|
||||
func _load_abilities() -> void:
|
||||
if not FileAccess.file_exists(ABILITIES_PATH):
|
||||
push_warning("AbilitySystem: No processed abilities found at " + ABILITIES_PATH)
|
||||
push_warning("Run: python tools/ability_processor.py")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(ABILITIES_PATH, FileAccess.READ)
|
||||
if not file:
|
||||
push_error("AbilitySystem: Failed to open " + ABILITIES_PATH)
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(file.get_as_text())
|
||||
file.close()
|
||||
|
||||
if error != OK:
|
||||
push_error("AbilitySystem: Failed to parse abilities JSON: " + json.get_error_message())
|
||||
return
|
||||
|
||||
var data = json.get_data()
|
||||
_version = data.get("version", "unknown")
|
||||
_stats = data.get("statistics", {})
|
||||
_abilities = data.get("abilities", {})
|
||||
|
||||
print("AbilitySystem: Loaded v%s - %d cards, %d abilities (%d high confidence)" % [
|
||||
_version,
|
||||
_stats.get("total_cards", 0),
|
||||
_stats.get("total_abilities", 0),
|
||||
_stats.get("parsed_high", 0)
|
||||
])
|
||||
|
||||
|
||||
## Connect to a game state to listen for events
|
||||
func connect_to_game(game_state) -> void:
|
||||
if _game_state:
|
||||
_disconnect_from_game()
|
||||
|
||||
_game_state = game_state
|
||||
|
||||
# Connect to game events that can trigger abilities
|
||||
game_state.card_played.connect(_on_card_played)
|
||||
game_state.summon_cast.connect(_on_summon_cast)
|
||||
game_state.attack_declared.connect(_on_attack_declared)
|
||||
game_state.block_declared.connect(_on_block_declared)
|
||||
game_state.forward_broken.connect(_on_forward_broken)
|
||||
game_state.damage_dealt.connect(_on_damage_dealt)
|
||||
game_state.card_moved.connect(_on_card_moved)
|
||||
game_state.combat_resolved.connect(_on_combat_resolved)
|
||||
|
||||
# Turn manager signals
|
||||
if game_state.turn_manager:
|
||||
game_state.turn_manager.phase_changed.connect(_on_phase_changed)
|
||||
game_state.turn_manager.turn_started.connect(_on_turn_started)
|
||||
game_state.turn_manager.turn_ended.connect(_on_turn_ended)
|
||||
|
||||
print("AbilitySystem: Connected to GameState")
|
||||
|
||||
|
||||
func _disconnect_from_game() -> void:
|
||||
if not _game_state:
|
||||
return
|
||||
|
||||
# Disconnect all signals
|
||||
if _game_state.card_played.is_connected(_on_card_played):
|
||||
_game_state.card_played.disconnect(_on_card_played)
|
||||
if _game_state.summon_cast.is_connected(_on_summon_cast):
|
||||
_game_state.summon_cast.disconnect(_on_summon_cast)
|
||||
if _game_state.attack_declared.is_connected(_on_attack_declared):
|
||||
_game_state.attack_declared.disconnect(_on_attack_declared)
|
||||
if _game_state.block_declared.is_connected(_on_block_declared):
|
||||
_game_state.block_declared.disconnect(_on_block_declared)
|
||||
if _game_state.forward_broken.is_connected(_on_forward_broken):
|
||||
_game_state.forward_broken.disconnect(_on_forward_broken)
|
||||
if _game_state.damage_dealt.is_connected(_on_damage_dealt):
|
||||
_game_state.damage_dealt.disconnect(_on_damage_dealt)
|
||||
if _game_state.card_moved.is_connected(_on_card_moved):
|
||||
_game_state.card_moved.disconnect(_on_card_moved)
|
||||
|
||||
_game_state = null
|
||||
|
||||
|
||||
## Get parsed abilities for a card
|
||||
func get_abilities(card_id: String) -> Array:
|
||||
return _abilities.get(card_id, [])
|
||||
|
||||
|
||||
## Check if a card has parsed abilities
|
||||
func has_abilities(card_id: String) -> bool:
|
||||
return _abilities.has(card_id) and _abilities[card_id].size() > 0
|
||||
|
||||
|
||||
## Get a specific parsed ability
|
||||
func get_ability(card_id: String, ability_index: int) -> Dictionary:
|
||||
var abilities = get_abilities(card_id)
|
||||
if ability_index >= 0 and ability_index < abilities.size():
|
||||
return abilities[ability_index]
|
||||
return {}
|
||||
|
||||
|
||||
## Process a game event and trigger matching abilities
|
||||
func process_event(event_type: String, event_data: Dictionary) -> void:
|
||||
if not _game_state:
|
||||
return
|
||||
|
||||
var triggered = trigger_matcher.find_triggered_abilities(
|
||||
event_type, event_data, _game_state, _abilities
|
||||
)
|
||||
|
||||
for trigger_info in triggered:
|
||||
_queue_ability(trigger_info)
|
||||
|
||||
|
||||
## Queue an ability for resolution
|
||||
func _queue_ability(trigger_info: Dictionary) -> void:
|
||||
var source = trigger_info.source as CardInstance
|
||||
var ability = trigger_info.ability as Dictionary
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
if not parsed or not parsed.has("effects"):
|
||||
return
|
||||
|
||||
# Check if ability has a cost that needs to be paid
|
||||
var cost = parsed.get("cost", {})
|
||||
if not cost.is_empty() and source and _game_state:
|
||||
var player = _game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
# Validate cost
|
||||
var validation = _validate_ability_cost(cost, source, player)
|
||||
if not validation.valid:
|
||||
# Cannot pay cost - emit signal and skip ability
|
||||
ability_cost_failed.emit(source, ability, validation.reason)
|
||||
push_warning("AbilitySystem: Cannot pay cost for ability - %s" % validation.reason)
|
||||
return
|
||||
|
||||
# Pay the cost
|
||||
_pay_ability_cost(cost, source, player)
|
||||
|
||||
ability_triggered.emit(source, ability)
|
||||
|
||||
# Add effects to pending stack (LIFO for proper resolution order)
|
||||
var effects = parsed.get("effects", [])
|
||||
for i in range(effects.size() - 1, -1, -1):
|
||||
_pending_effects.push_front({
|
||||
"effect": effects[i],
|
||||
"source": source,
|
||||
"controller": source.controller_index,
|
||||
"ability": ability,
|
||||
"event_data": trigger_info.get("event_data", {})
|
||||
})
|
||||
|
||||
# Start resolving if not already
|
||||
if not _is_resolving:
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Resolve the next pending effect
|
||||
func _resolve_next_effect() -> void:
|
||||
if _pending_effects.is_empty():
|
||||
_is_resolving = false
|
||||
return
|
||||
|
||||
_is_resolving = true
|
||||
var pending = _pending_effects[0]
|
||||
var effect = pending.effect
|
||||
var source = pending.source
|
||||
|
||||
# Check if effect is optional and we haven't prompted yet
|
||||
if effect.get("optional", false) and not pending.get("optional_prompted", false):
|
||||
_waiting_for_optional = true
|
||||
pending["optional_prompted"] = true # Mark as prompted to avoid re-prompting
|
||||
|
||||
# Determine which player should decide
|
||||
var player_index = source.controller_index if source else 0
|
||||
|
||||
# Build description from effect
|
||||
var description = _build_effect_description(effect)
|
||||
|
||||
# Emit signal for UI to handle
|
||||
optional_effect_prompt.emit(player_index, effect, description, _on_optional_effect_choice)
|
||||
return # Wait for callback
|
||||
|
||||
# Check if effect needs targeting
|
||||
if _effect_needs_targeting(effect):
|
||||
var valid_targets = target_selector.get_valid_targets(
|
||||
effect.get("target", {}), source, _game_state
|
||||
)
|
||||
|
||||
if valid_targets.is_empty():
|
||||
# No valid targets, skip effect
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
return
|
||||
|
||||
# Request target selection from player
|
||||
targeting_required.emit(effect, valid_targets, _on_targets_selected)
|
||||
# Wait for targeting_completed signal
|
||||
else:
|
||||
# Resolve immediately
|
||||
_execute_effect(pending)
|
||||
|
||||
|
||||
## Execute an effect with its targets
|
||||
func _execute_effect(pending: Dictionary) -> void:
|
||||
var effect = pending.effect
|
||||
var source = pending.source
|
||||
var targets = pending.get("targets", [])
|
||||
|
||||
effect_resolver.resolve(effect, source, targets, _game_state)
|
||||
|
||||
|
||||
## Called when effect resolution completes
|
||||
func _on_effect_completed(effect: Dictionary, targets: Array) -> void:
|
||||
effect_resolved.emit(effect, targets)
|
||||
|
||||
if not _pending_effects.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Called when player selects targets
|
||||
func _on_targets_selected(targets: Array) -> void:
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
_pending_effects[0]["targets"] = targets
|
||||
targeting_completed.emit(_pending_effects[0].effect, targets)
|
||||
_execute_effect(_pending_effects[0])
|
||||
|
||||
|
||||
## Check if effect requires player targeting
|
||||
func _effect_needs_targeting(effect: Dictionary) -> bool:
|
||||
if not effect.has("target"):
|
||||
return false
|
||||
var target = effect.target
|
||||
return target.get("type") == "CHOOSE"
|
||||
|
||||
|
||||
## Called when player responds to optional effect prompt
|
||||
func _on_optional_effect_choice(accepted: bool) -> void:
|
||||
_waiting_for_optional = false
|
||||
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
if accepted:
|
||||
# Player chose to execute the optional effect
|
||||
# Continue with normal resolution (targeting or execution)
|
||||
var pending = _pending_effects[0]
|
||||
var effect = pending.effect
|
||||
|
||||
if _effect_needs_targeting(effect):
|
||||
var source = pending.source
|
||||
var valid_targets = target_selector.get_valid_targets(
|
||||
effect.get("target", {}), source, _game_state
|
||||
)
|
||||
|
||||
if valid_targets.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
return
|
||||
|
||||
targeting_required.emit(effect, valid_targets, _on_targets_selected)
|
||||
else:
|
||||
_execute_effect(pending)
|
||||
else:
|
||||
# Player declined the optional effect - skip it
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Build a human-readable description of an effect for prompts
|
||||
func _build_effect_description(effect: Dictionary) -> String:
|
||||
var effect_type = str(effect.get("type", "")).to_upper()
|
||||
var amount = effect.get("amount", 0)
|
||||
|
||||
match effect_type:
|
||||
"DRAW":
|
||||
var count = effect.get("amount", 1)
|
||||
return "Draw %d card%s" % [count, "s" if count > 1 else ""]
|
||||
"DAMAGE":
|
||||
return "Deal %d damage" % amount
|
||||
"POWER_MOD":
|
||||
var sign = "+" if amount >= 0 else ""
|
||||
return "Give %s%d power" % [sign, amount]
|
||||
"DULL":
|
||||
return "Dull a Forward"
|
||||
"ACTIVATE":
|
||||
return "Activate a card"
|
||||
"BREAK":
|
||||
return "Break a card"
|
||||
"RETURN":
|
||||
return "Return a card to hand"
|
||||
"SEARCH":
|
||||
return "Search your deck"
|
||||
"DISCARD":
|
||||
var count = effect.get("amount", 1)
|
||||
return "Discard %d card%s" % [count, "s" if count > 1 else ""]
|
||||
_:
|
||||
# Use the original_text if available
|
||||
if effect.has("original_text"):
|
||||
return effect.original_text
|
||||
return "Use this effect"
|
||||
|
||||
|
||||
## Called when EffectResolver encounters a CHOOSE_MODE effect
|
||||
func _on_choice_required(effect: Dictionary, modes: Array) -> void:
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
var pending = _pending_effects[0]
|
||||
var source = pending.get("source") as CardInstance
|
||||
|
||||
# Check for enhanced condition (e.g., "If you have 5+ Ifrit, select 3 instead")
|
||||
var select_count = effect.get("select_count", 1)
|
||||
var select_up_to = effect.get("select_up_to", false)
|
||||
|
||||
var enhanced = effect.get("enhanced_condition", {})
|
||||
if not enhanced.is_empty() and _check_enhanced_condition(enhanced, source):
|
||||
select_count = enhanced.get("select_count", select_count)
|
||||
select_up_to = enhanced.get("select_up_to", select_up_to)
|
||||
|
||||
# If we have a ChoiceModal, use it
|
||||
if choice_modal:
|
||||
_waiting_for_choice = true
|
||||
_handle_modal_choice_async(effect, modes, select_count, select_up_to, source)
|
||||
else:
|
||||
# No UI available - auto-select first N modes
|
||||
push_warning("AbilitySystem: No ChoiceModal available, auto-selecting first mode(s)")
|
||||
var auto_selected: Array = []
|
||||
for i in range(min(select_count, modes.size())):
|
||||
auto_selected.append(i)
|
||||
_on_modes_selected(effect, modes, auto_selected, source)
|
||||
|
||||
|
||||
## Handle modal choice asynchronously
|
||||
func _handle_modal_choice_async(
|
||||
effect: Dictionary,
|
||||
modes: Array,
|
||||
select_count: int,
|
||||
select_up_to: bool,
|
||||
source: CardInstance
|
||||
) -> void:
|
||||
var selected = await choice_modal.show_choices(
|
||||
"", # Title is generated by ChoiceModal
|
||||
modes,
|
||||
select_count,
|
||||
select_up_to,
|
||||
false # Not cancellable for mandatory abilities
|
||||
)
|
||||
|
||||
_waiting_for_choice = false
|
||||
_on_modes_selected(effect, modes, selected, source)
|
||||
|
||||
|
||||
## Cached regex for enhanced condition parsing
|
||||
var _enhanced_count_regex: RegEx = null
|
||||
|
||||
|
||||
## Check if enhanced condition is met
|
||||
func _check_enhanced_condition(condition: Dictionary, source: CardInstance) -> bool:
|
||||
var description = condition.get("description", "").to_lower()
|
||||
|
||||
# Parse "if you have X or more [Card Name] in your Break Zone"
|
||||
if "break zone" in description:
|
||||
# Initialize regex once (lazy)
|
||||
if _enhanced_count_regex == null:
|
||||
_enhanced_count_regex = RegEx.new()
|
||||
_enhanced_count_regex.compile("(\\d+) or more")
|
||||
|
||||
var match_result = _enhanced_count_regex.search(description)
|
||||
if match_result:
|
||||
var required_count = int(match_result.get_string(1))
|
||||
|
||||
# Check break zone for matching cards
|
||||
if _game_state and source:
|
||||
var player = _game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
# Count matching cards in break zone
|
||||
var break_zone_count = 0
|
||||
for card in player.break_zone.get_cards():
|
||||
# Simple name matching (description contains card name pattern)
|
||||
if card.card_data and card.card_data.name.to_lower() in description:
|
||||
break_zone_count += 1
|
||||
|
||||
return break_zone_count >= required_count
|
||||
|
||||
return false
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COST VALIDATION AND PAYMENT
|
||||
# =============================================================================
|
||||
|
||||
## Signal emitted when cost cannot be paid
|
||||
signal ability_cost_failed(source: CardInstance, ability: Dictionary, reason: String)
|
||||
|
||||
|
||||
## Validate that a player can pay the cost for an ability
|
||||
## Returns true if cost can be paid, false otherwise
|
||||
func _validate_ability_cost(
|
||||
cost: Dictionary,
|
||||
source: CardInstance,
|
||||
player
|
||||
) -> Dictionary:
|
||||
var result = {"valid": true, "reason": ""}
|
||||
|
||||
if cost.is_empty():
|
||||
return result
|
||||
|
||||
# Check CP cost
|
||||
var cp_cost = cost.get("cp", 0)
|
||||
var element = cost.get("element", "")
|
||||
|
||||
if cp_cost > 0:
|
||||
if element and element != "" and element.to_upper() != "ANY":
|
||||
# Specific element required
|
||||
var element_enum = Enums.element_from_string(element)
|
||||
if player.cp_pool.get_cp(element_enum) < cp_cost:
|
||||
result.valid = false
|
||||
result.reason = "Not enough %s CP (need %d, have %d)" % [
|
||||
element, cp_cost, player.cp_pool.get_cp(element_enum)
|
||||
]
|
||||
return result
|
||||
else:
|
||||
# Any element
|
||||
if player.cp_pool.get_total_cp() < cp_cost:
|
||||
result.valid = false
|
||||
result.reason = "Not enough CP (need %d, have %d)" % [
|
||||
cp_cost, player.cp_pool.get_total_cp()
|
||||
]
|
||||
return result
|
||||
|
||||
# Check discard cost
|
||||
var discard = cost.get("discard", 0)
|
||||
if discard > 0:
|
||||
if player.hand.get_count() < discard:
|
||||
result.valid = false
|
||||
result.reason = "Not enough cards in hand to discard (need %d, have %d)" % [
|
||||
discard, player.hand.get_count()
|
||||
]
|
||||
return result
|
||||
|
||||
# Check dull self cost
|
||||
var dull_self = cost.get("dull_self", false)
|
||||
if dull_self and source:
|
||||
if source.is_dull():
|
||||
result.valid = false
|
||||
result.reason = "Card is already dulled"
|
||||
return result
|
||||
|
||||
# Check specific card discard
|
||||
var specific_discard = cost.get("specific_discard", "")
|
||||
if specific_discard != "":
|
||||
# Player must have a card with this name in hand
|
||||
var has_card = false
|
||||
for card in player.hand.get_cards():
|
||||
if card.card_data and card.card_data.name.to_lower() == specific_discard.to_lower():
|
||||
has_card = true
|
||||
break
|
||||
if not has_card:
|
||||
result.valid = false
|
||||
result.reason = "Must discard a card named '%s'" % specific_discard
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Pay the cost for an ability
|
||||
## Returns true if cost was paid successfully
|
||||
func _pay_ability_cost(
|
||||
cost: Dictionary,
|
||||
source: CardInstance,
|
||||
player
|
||||
) -> bool:
|
||||
if cost.is_empty():
|
||||
return true
|
||||
|
||||
# Pay CP cost
|
||||
var cp_cost = cost.get("cp", 0)
|
||||
var element = cost.get("element", "")
|
||||
|
||||
if cp_cost > 0:
|
||||
if element and element != "" and element.to_upper() != "ANY":
|
||||
# Spend specific element CP
|
||||
var element_enum = Enums.element_from_string(element)
|
||||
player.cp_pool.add_cp(element_enum, -cp_cost)
|
||||
else:
|
||||
# Spend from any element (generic)
|
||||
var remaining = cp_cost
|
||||
for elem in Enums.Element.values():
|
||||
var available = player.cp_pool.get_cp(elem)
|
||||
if available > 0:
|
||||
var to_spend = mini(available, remaining)
|
||||
player.cp_pool.add_cp(elem, -to_spend)
|
||||
remaining -= to_spend
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
# Pay dull self cost
|
||||
var dull_self = cost.get("dull_self", false)
|
||||
if dull_self and source:
|
||||
source.dull()
|
||||
|
||||
# Note: Discard costs are handled through separate UI interaction
|
||||
# The discard selection would be queued as a separate effect
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Called when player selects mode(s)
|
||||
func _on_modes_selected(
|
||||
effect: Dictionary,
|
||||
modes: Array,
|
||||
selected_indices: Array,
|
||||
source: CardInstance
|
||||
) -> void:
|
||||
# Queue the effects from selected modes
|
||||
for index in selected_indices:
|
||||
if index >= 0 and index < modes.size():
|
||||
var mode = modes[index]
|
||||
var mode_effects = mode.get("effects", [])
|
||||
|
||||
for mode_effect in mode_effects:
|
||||
_pending_effects.push_back({
|
||||
"effect": mode_effect,
|
||||
"source": source,
|
||||
"controller": source.controller_index if source else 0,
|
||||
"ability": effect,
|
||||
"event_data": {}
|
||||
})
|
||||
|
||||
# Remove the CHOOSE_MODE effect from pending and continue
|
||||
if not _pending_effects.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Event Handlers
|
||||
# =============================================================================
|
||||
|
||||
func _on_card_played(card: CardInstance, player_index: int) -> void:
|
||||
# Register field abilities
|
||||
if card.is_forward() or card.is_backup():
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
field_effect_manager.register_field_abilities(card, card_abilities)
|
||||
|
||||
# Trigger enters field events
|
||||
process_event("ENTERS_FIELD", {
|
||||
"card": card,
|
||||
"player": player_index,
|
||||
"zone_from": Enums.ZoneType.HAND,
|
||||
"zone_to": Enums.ZoneType.FIELD_FORWARDS if card.is_forward() else Enums.ZoneType.FIELD_BACKUPS
|
||||
})
|
||||
|
||||
|
||||
func _on_summon_cast(card: CardInstance, player_index: int) -> void:
|
||||
process_event("SUMMON_CAST", {
|
||||
"card": card,
|
||||
"player": player_index
|
||||
})
|
||||
|
||||
|
||||
func _on_attack_declared(attacker: CardInstance) -> void:
|
||||
process_event("ATTACKS", {
|
||||
"card": attacker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_block_declared(blocker: CardInstance) -> void:
|
||||
if not _game_state or not _game_state.turn_manager:
|
||||
return
|
||||
|
||||
var attacker = _game_state.turn_manager.current_attacker
|
||||
|
||||
process_event("BLOCKS", {
|
||||
"card": blocker,
|
||||
"attacker": attacker,
|
||||
"player": blocker.controller_index
|
||||
})
|
||||
|
||||
if attacker:
|
||||
process_event("IS_BLOCKED", {
|
||||
"card": attacker,
|
||||
"blocker": blocker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_forward_broken(card: CardInstance) -> void:
|
||||
# Unregister field abilities
|
||||
field_effect_manager.unregister_field_abilities(card)
|
||||
|
||||
process_event("LEAVES_FIELD", {
|
||||
"card": card,
|
||||
"zone_from": Enums.ZoneType.FIELD_FORWARDS,
|
||||
"zone_to": Enums.ZoneType.BREAK
|
||||
})
|
||||
|
||||
|
||||
func _on_damage_dealt(player_index: int, amount: int, cards: Array) -> void:
|
||||
process_event("DAMAGE_DEALT_TO_PLAYER", {
|
||||
"player": player_index,
|
||||
"amount": amount,
|
||||
"cards": cards
|
||||
})
|
||||
|
||||
# Check for EX BURST triggers on damage cards
|
||||
for card in cards:
|
||||
if card.card_data and card.card_data.has_ex_burst:
|
||||
_trigger_ex_burst(card, player_index)
|
||||
|
||||
|
||||
func _on_card_moved(card: CardInstance, from_zone: Enums.ZoneType, to_zone: Enums.ZoneType) -> void:
|
||||
# Handle zone changes
|
||||
if to_zone == Enums.ZoneType.BREAK:
|
||||
field_effect_manager.unregister_field_abilities(card)
|
||||
|
||||
|
||||
func _on_combat_resolved(attacker: CardInstance, blocker: CardInstance) -> void:
|
||||
if not blocker:
|
||||
# Unblocked attack
|
||||
process_event("DEALS_DAMAGE_TO_OPPONENT", {
|
||||
"card": attacker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
else:
|
||||
# Blocked combat
|
||||
process_event("DEALS_DAMAGE", {
|
||||
"card": attacker,
|
||||
"target": blocker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
process_event("DEALS_DAMAGE", {
|
||||
"card": blocker,
|
||||
"target": attacker,
|
||||
"player": blocker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_phase_changed(phase: Enums.TurnPhase) -> void:
|
||||
var event_type = ""
|
||||
match phase:
|
||||
Enums.TurnPhase.ACTIVE:
|
||||
event_type = "START_OF_ACTIVE_PHASE"
|
||||
Enums.TurnPhase.DRAW:
|
||||
event_type = "START_OF_DRAW_PHASE"
|
||||
Enums.TurnPhase.MAIN_1:
|
||||
event_type = "START_OF_MAIN_PHASE"
|
||||
Enums.TurnPhase.ATTACK:
|
||||
event_type = "START_OF_ATTACK_PHASE"
|
||||
Enums.TurnPhase.MAIN_2:
|
||||
event_type = "START_OF_MAIN_PHASE_2"
|
||||
Enums.TurnPhase.END:
|
||||
event_type = "START_OF_END_PHASE"
|
||||
|
||||
if event_type:
|
||||
process_event(event_type, {
|
||||
"player": _game_state.turn_manager.current_player_index if _game_state else 0
|
||||
})
|
||||
|
||||
|
||||
func _on_turn_started(player_index: int, turn_number: int) -> void:
|
||||
process_event("START_OF_TURN", {
|
||||
"player": player_index,
|
||||
"turn_number": turn_number
|
||||
})
|
||||
|
||||
|
||||
func _on_turn_ended(player_index: int) -> void:
|
||||
process_event("END_OF_TURN", {
|
||||
"player": player_index
|
||||
})
|
||||
|
||||
|
||||
## Trigger EX BURST for a damage card
|
||||
func _trigger_ex_burst(card: CardInstance, damaged_player: int) -> void:
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
|
||||
for ability in card_abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("is_ex_burst", false):
|
||||
# Queue the EX BURST ability
|
||||
_queue_ability({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": {
|
||||
"player": damaged_player,
|
||||
"trigger_type": "EX_BURST"
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
## Get power modifier from field effects for a card
|
||||
func get_field_power_modifier(card: CardInstance) -> int:
|
||||
return field_effect_manager.get_power_modifiers(card, _game_state)
|
||||
|
||||
|
||||
## Check if a card has a field-granted keyword
|
||||
func has_field_keyword(card: CardInstance, keyword: String) -> bool:
|
||||
return field_effect_manager.has_keyword(card, keyword, _game_state)
|
||||
|
||||
|
||||
## Check if a card has field-granted protection
|
||||
func has_field_protection(card: CardInstance, protection_type: String) -> bool:
|
||||
return field_effect_manager.has_protection(card, protection_type, _game_state)
|
||||
|
||||
|
||||
## Get all granted keywords for a card from field effects
|
||||
func get_field_keywords(card: CardInstance) -> Array:
|
||||
return field_effect_manager.get_granted_keywords(card, _game_state)
|
||||
|
||||
|
||||
## Trigger EX BURST on a specific card (called by EffectResolver)
|
||||
func trigger_ex_burst_on_card(card: CardInstance) -> void:
|
||||
if not card or not card.card_data:
|
||||
return
|
||||
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
|
||||
for ability in card_abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("is_ex_burst", false):
|
||||
_queue_ability({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": {
|
||||
"trigger_type": "EX_BURST_TRIGGERED"
|
||||
}
|
||||
})
|
||||
break
|
||||
219
scripts/game/abilities/CardFilter.gd
Normal file
219
scripts/game/abilities/CardFilter.gd
Normal file
@@ -0,0 +1,219 @@
|
||||
class_name CardFilter
|
||||
extends RefCounted
|
||||
|
||||
## CardFilter - Shared card filtering utility used by EffectResolver, FieldEffectManager, and TargetSelector
|
||||
##
|
||||
## This utility provides a unified way to filter cards based on various criteria
|
||||
## including element, job, category, cost, power, card type, and state.
|
||||
|
||||
|
||||
## Check if a card matches a filter dictionary
|
||||
static func matches_filter(card: CardInstance, filter: Dictionary, source: CardInstance = null) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
if filter.is_empty():
|
||||
return true
|
||||
|
||||
# Element filter
|
||||
if filter.has("element"):
|
||||
var element_str = str(filter.element).to_upper()
|
||||
var element = Enums.element_from_string(element_str)
|
||||
if element not in card.get_elements():
|
||||
return false
|
||||
|
||||
# Job filter
|
||||
if filter.has("job"):
|
||||
var job_filter = str(filter.job).to_lower()
|
||||
var card_job = str(card.card_data.job).to_lower() if card.card_data.job else ""
|
||||
if job_filter != card_job:
|
||||
return false
|
||||
|
||||
# Category filter
|
||||
if filter.has("category"):
|
||||
if not _has_category(card, str(filter.category).to_upper()):
|
||||
return false
|
||||
|
||||
# Cost filters
|
||||
if not _matches_cost_filter(card, filter):
|
||||
return false
|
||||
|
||||
# Power filters
|
||||
if not _matches_power_filter(card, filter):
|
||||
return false
|
||||
|
||||
# Card name filter
|
||||
if filter.has("card_name"):
|
||||
var name_filter = str(filter.card_name).to_lower()
|
||||
var card_name = str(card.card_data.name).to_lower() if card.card_data.name else ""
|
||||
if name_filter != card_name:
|
||||
return false
|
||||
if filter.has("name"):
|
||||
var name_filter = str(filter.name).to_lower()
|
||||
var card_name = str(card.card_data.name).to_lower() if card.card_data.name else ""
|
||||
if name_filter != card_name:
|
||||
return false
|
||||
|
||||
# Card type filter
|
||||
if filter.has("card_type"):
|
||||
if not _matches_type(card, str(filter.card_type)):
|
||||
return false
|
||||
|
||||
# State filters
|
||||
if filter.has("is_dull"):
|
||||
var card_dull = card.is_dull() if card.has_method("is_dull") else card.is_dull
|
||||
if card_dull != filter.is_dull:
|
||||
return false
|
||||
if filter.has("is_active"):
|
||||
var card_active = card.is_active() if card.has_method("is_active") else not card.is_dull
|
||||
if card_active != filter.is_active:
|
||||
return false
|
||||
|
||||
# Exclude self
|
||||
if filter.get("exclude_self", false) and source != null and card == source:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Count cards that match a filter
|
||||
static func count_matching(cards: Array, filter: Dictionary, source: CardInstance = null) -> int:
|
||||
var count = 0
|
||||
for card in cards:
|
||||
if matches_filter(card, filter, source):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
## Get all cards that match a filter
|
||||
static func get_matching(cards: Array, filter: Dictionary, source: CardInstance = null) -> Array:
|
||||
var matching: Array = []
|
||||
for card in cards:
|
||||
if matches_filter(card, filter, source):
|
||||
matching.append(card)
|
||||
return matching
|
||||
|
||||
|
||||
## Get the highest power among cards (optionally filtered)
|
||||
static func get_highest_power(cards: Array, filter: Dictionary = {}, source: CardInstance = null) -> int:
|
||||
var highest = 0
|
||||
for card in cards:
|
||||
if filter.is_empty() or matches_filter(card, filter, source):
|
||||
var power = card.get_power() if card.has_method("get_power") else 0
|
||||
if power > highest:
|
||||
highest = power
|
||||
return highest
|
||||
|
||||
|
||||
## Get the lowest power among cards (optionally filtered)
|
||||
static func get_lowest_power(cards: Array, filter: Dictionary = {}, source: CardInstance = null) -> int:
|
||||
var lowest = -1
|
||||
for card in cards:
|
||||
if filter.is_empty() or matches_filter(card, filter, source):
|
||||
var power = card.get_power() if card.has_method("get_power") else 0
|
||||
if lowest == -1 or power < lowest:
|
||||
lowest = power
|
||||
return lowest if lowest != -1 else 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRIVATE HELPER METHODS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Check if a card has a specific category
|
||||
static func _has_category(card: CardInstance, category_filter: String) -> bool:
|
||||
# Check card's category field
|
||||
if card.card_data.has("category") and card.card_data.category:
|
||||
if category_filter in str(card.card_data.category).to_upper():
|
||||
return true
|
||||
# Check categories array if present
|
||||
if card.card_data.has("categories"):
|
||||
for cat in card.card_data.categories:
|
||||
if category_filter in str(cat).to_upper():
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Check if a card matches cost filter criteria
|
||||
static func _matches_cost_filter(card: CardInstance, filter: Dictionary) -> bool:
|
||||
var card_cost = card.card_data.cost
|
||||
|
||||
# Exact cost filter
|
||||
if filter.has("cost") and not filter.has("cost_comparison"):
|
||||
if card_cost != int(filter.cost):
|
||||
return false
|
||||
|
||||
# Min/max style cost filters (from TargetSelector)
|
||||
if filter.has("cost_min") and card_cost < int(filter.cost_min):
|
||||
return false
|
||||
if filter.has("cost_max") and card_cost > int(filter.cost_max):
|
||||
return false
|
||||
|
||||
# Cost comparison filter
|
||||
if filter.has("cost_comparison") and filter.has("cost_value"):
|
||||
var target_cost = int(filter.cost_value)
|
||||
match str(filter.cost_comparison).to_upper():
|
||||
"LTE":
|
||||
if card_cost > target_cost:
|
||||
return false
|
||||
"GTE":
|
||||
if card_cost < target_cost:
|
||||
return false
|
||||
"EQ":
|
||||
if card_cost != target_cost:
|
||||
return false
|
||||
"LT":
|
||||
if card_cost >= target_cost:
|
||||
return false
|
||||
"GT":
|
||||
if card_cost <= target_cost:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card matches type filter
|
||||
static func _matches_type(card: CardInstance, type_filter: String) -> bool:
|
||||
match type_filter.to_upper():
|
||||
"FORWARD":
|
||||
return card.is_forward()
|
||||
"BACKUP":
|
||||
return card.is_backup()
|
||||
"SUMMON":
|
||||
return card.is_summon()
|
||||
"MONSTER":
|
||||
return card.is_monster()
|
||||
"CHARACTER":
|
||||
return card.is_forward() or card.is_backup()
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card matches power filter criteria
|
||||
static func _matches_power_filter(card: CardInstance, filter: Dictionary) -> bool:
|
||||
var power = card.get_power() if card.has_method("get_power") else card.card_data.power
|
||||
|
||||
# Min/max style power filters
|
||||
if filter.has("power_min") and power < int(filter.power_min):
|
||||
return false
|
||||
if filter.has("power_max") and power > int(filter.power_max):
|
||||
return false
|
||||
|
||||
# Comparison style power filter
|
||||
if filter.has("power_comparison") and filter.has("power_value"):
|
||||
var target = int(filter.power_value)
|
||||
match str(filter.power_comparison).to_upper():
|
||||
"LTE":
|
||||
if power > target:
|
||||
return false
|
||||
"GTE":
|
||||
if power < target:
|
||||
return false
|
||||
"LT":
|
||||
if power >= target:
|
||||
return false
|
||||
"GT":
|
||||
if power <= target:
|
||||
return false
|
||||
|
||||
return true
|
||||
510
scripts/game/abilities/ConditionChecker.gd
Normal file
510
scripts/game/abilities/ConditionChecker.gd
Normal file
@@ -0,0 +1,510 @@
|
||||
class_name ConditionChecker
|
||||
extends RefCounted
|
||||
|
||||
## Centralized condition evaluation for all ability types
|
||||
## Handles conditions like "If you control X", "If you have received Y damage", etc.
|
||||
|
||||
|
||||
## Main evaluation entry point
|
||||
## Returns true if condition is met, false otherwise
|
||||
func evaluate(condition: Dictionary, context: Dictionary) -> bool:
|
||||
if condition.is_empty():
|
||||
return true # Empty condition = unconditional
|
||||
|
||||
var condition_type = condition.get("type", "")
|
||||
|
||||
match condition_type:
|
||||
"CONTROL_CARD":
|
||||
return _check_control_card(condition, context)
|
||||
"CONTROL_COUNT":
|
||||
return _check_control_count(condition, context)
|
||||
"DAMAGE_RECEIVED":
|
||||
return _check_damage_received(condition, context)
|
||||
"BREAK_ZONE_COUNT":
|
||||
return _check_break_zone_count(condition, context)
|
||||
"CARD_IN_ZONE":
|
||||
return _check_card_in_zone(condition, context)
|
||||
"FORWARD_STATE":
|
||||
return _check_forward_state(condition, context)
|
||||
"COST_COMPARISON":
|
||||
return _check_cost_comparison(condition, context)
|
||||
"POWER_COMPARISON":
|
||||
return _check_power_comparison(condition, context)
|
||||
"ELEMENT_MATCH":
|
||||
return _check_element_match(condition, context)
|
||||
"CARD_TYPE_MATCH":
|
||||
return _check_card_type_match(condition, context)
|
||||
"JOB_MATCH":
|
||||
return _check_job_match(condition, context)
|
||||
"CATEGORY_MATCH":
|
||||
return _check_category_match(condition, context)
|
||||
"AND":
|
||||
return _check_and(condition, context)
|
||||
"OR":
|
||||
return _check_or(condition, context)
|
||||
"NOT":
|
||||
return _check_not(condition, context)
|
||||
_:
|
||||
push_warning("ConditionChecker: Unknown condition type '%s'" % condition_type)
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONTROL CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_control_card(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_name = condition.get("card_name", "")
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
# Check all field cards for the player
|
||||
var field_cards = _get_field_cards(game_state, player)
|
||||
for card in field_cards:
|
||||
if card and card.card_data and card.card_data.name == card_name:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _check_control_count(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_type = condition.get("card_type", "")
|
||||
var element = condition.get("element", "")
|
||||
var job = condition.get("job", "")
|
||||
var category = condition.get("category", "")
|
||||
var comparison = condition.get("comparison", "GTE")
|
||||
var value = condition.get("value", 1)
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var count = 0
|
||||
var field_cards = _get_field_cards(game_state, player)
|
||||
|
||||
for card in field_cards:
|
||||
if not card or not card.card_data:
|
||||
continue
|
||||
|
||||
var matches = true
|
||||
|
||||
# Check card type filter
|
||||
if card_type != "" and not _matches_card_type(card, card_type):
|
||||
matches = false
|
||||
|
||||
# Check element filter
|
||||
if element != "" and not _matches_element(card, element):
|
||||
matches = false
|
||||
|
||||
# Check job filter
|
||||
if job != "" and not _matches_job(card, job):
|
||||
matches = false
|
||||
|
||||
# Check category filter
|
||||
if category != "" and not _matches_category(card, category):
|
||||
matches = false
|
||||
|
||||
if matches:
|
||||
count += 1
|
||||
|
||||
return _compare(count, comparison, value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DAMAGE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_damage_received(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var comparison = condition.get("comparison", "GTE")
|
||||
var value = condition.get("value", 1)
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var damage = _get_player_damage(game_state, player)
|
||||
return _compare(damage, comparison, value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ZONE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_break_zone_count(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_name = condition.get("card_name", "")
|
||||
var card_names: Array = condition.get("card_names", [])
|
||||
if card_name != "" and card_name not in card_names:
|
||||
card_names.append(card_name)
|
||||
|
||||
var comparison = condition.get("comparison", "GTE")
|
||||
var value = condition.get("value", 1)
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var count = 0
|
||||
var break_zone = _get_break_zone(game_state, player)
|
||||
|
||||
for card in break_zone:
|
||||
if not card or not card.card_data:
|
||||
continue
|
||||
|
||||
# If no specific names, count all
|
||||
if card_names.is_empty():
|
||||
count += 1
|
||||
elif card.card_data.name in card_names:
|
||||
count += 1
|
||||
|
||||
return _compare(count, comparison, value)
|
||||
|
||||
|
||||
func _check_card_in_zone(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var zone = condition.get("zone", "") # "HAND", "DECK", "BREAK_ZONE", "REMOVED"
|
||||
var card_name = condition.get("card_name", "")
|
||||
var card_type = condition.get("card_type", "")
|
||||
var player = context.get("player_id", 0)
|
||||
var game_state = context.get("game_state")
|
||||
|
||||
if not game_state:
|
||||
return false
|
||||
|
||||
var zone_cards: Array = []
|
||||
match zone:
|
||||
"HAND":
|
||||
zone_cards = _get_hand(game_state, player)
|
||||
"DECK":
|
||||
zone_cards = _get_deck(game_state, player)
|
||||
"BREAK_ZONE":
|
||||
zone_cards = _get_break_zone(game_state, player)
|
||||
"REMOVED":
|
||||
zone_cards = _get_removed_zone(game_state, player)
|
||||
"FIELD":
|
||||
zone_cards = _get_field_cards(game_state, player)
|
||||
|
||||
for card in zone_cards:
|
||||
if not card or not card.card_data:
|
||||
continue
|
||||
|
||||
var matches = true
|
||||
if card_name != "" and card.card_data.name != card_name:
|
||||
matches = false
|
||||
if card_type != "" and not _matches_card_type(card, card_type):
|
||||
matches = false
|
||||
|
||||
if matches:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CARD STATE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_forward_state(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var state = condition.get("state", "") # "DULL", "ACTIVE", "DAMAGED"
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target:
|
||||
return false
|
||||
|
||||
match state:
|
||||
"DULL":
|
||||
return target.is_dull if target.has_method("get") or "is_dull" in target else false
|
||||
"ACTIVE":
|
||||
return not target.is_dull if "is_dull" in target else false
|
||||
"DAMAGED":
|
||||
if "current_power" in target and target.card_data:
|
||||
return target.current_power < target.card_data.power
|
||||
"FROZEN":
|
||||
return target.is_frozen if "is_frozen" in target else false
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _check_cost_comparison(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var comparison = condition.get("comparison", "LTE")
|
||||
var value = condition.get("value", 0)
|
||||
var compare_to = condition.get("compare_to", "") # "SELF_COST", "VALUE", or empty for value
|
||||
var target = context.get("target_card")
|
||||
var source = context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
var target_cost = target.card_data.cost
|
||||
var compare_value = value
|
||||
|
||||
if compare_to == "SELF_COST" and source and source.card_data:
|
||||
compare_value = source.card_data.cost
|
||||
|
||||
return _compare(target_cost, comparison, compare_value)
|
||||
|
||||
|
||||
func _check_power_comparison(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var comparison = condition.get("comparison", "LTE")
|
||||
var value = condition.get("value", 0)
|
||||
var compare_to = condition.get("compare_to", "") # "SELF_POWER", "VALUE"
|
||||
var target = context.get("target_card")
|
||||
var source = context.get("source_card")
|
||||
|
||||
if not target:
|
||||
return false
|
||||
|
||||
var target_power = target.current_power if "current_power" in target else 0
|
||||
var compare_value = value
|
||||
|
||||
if compare_to == "SELF_POWER" and source:
|
||||
compare_value = source.current_power if "current_power" in source else 0
|
||||
|
||||
return _compare(target_power, comparison, compare_value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CARD ATTRIBUTE CONDITIONS
|
||||
# =============================================================================
|
||||
|
||||
func _check_element_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var element = condition.get("element", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
return _matches_element(target, element)
|
||||
|
||||
|
||||
func _check_card_type_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var card_type = condition.get("card_type", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target:
|
||||
return false
|
||||
|
||||
return _matches_card_type(target, card_type)
|
||||
|
||||
|
||||
func _check_job_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var job = condition.get("job", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
return _matches_job(target, job)
|
||||
|
||||
|
||||
func _check_category_match(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var category = condition.get("category", "")
|
||||
var check_self = condition.get("check_self", false)
|
||||
var target = context.get("target_card") if not check_self else context.get("source_card")
|
||||
|
||||
if not target or not target.card_data:
|
||||
return false
|
||||
|
||||
return _matches_category(target, category)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LOGICAL OPERATORS
|
||||
# =============================================================================
|
||||
|
||||
func _check_and(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var conditions: Array = condition.get("conditions", [])
|
||||
for sub_condition in conditions:
|
||||
if not evaluate(sub_condition, context):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _check_or(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var conditions: Array = condition.get("conditions", [])
|
||||
for sub_condition in conditions:
|
||||
if evaluate(sub_condition, context):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _check_not(condition: Dictionary, context: Dictionary) -> bool:
|
||||
var inner: Dictionary = condition.get("condition", {})
|
||||
return not evaluate(inner, context)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
func _compare(actual: int, comparison: String, expected: int) -> bool:
|
||||
match comparison:
|
||||
"EQ":
|
||||
return actual == expected
|
||||
"NEQ":
|
||||
return actual != expected
|
||||
"GT":
|
||||
return actual > expected
|
||||
"GTE":
|
||||
return actual >= expected
|
||||
"LT":
|
||||
return actual < expected
|
||||
"LTE":
|
||||
return actual <= expected
|
||||
return false
|
||||
|
||||
|
||||
func _matches_card_type(card, card_type: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var type_upper = card_type.to_upper()
|
||||
var card_type_value = card.card_data.type
|
||||
|
||||
# Handle string or enum type
|
||||
if card_type_value is String:
|
||||
return card_type_value.to_upper() == type_upper
|
||||
|
||||
# Handle Enums.CardType enum
|
||||
match type_upper:
|
||||
"FORWARD":
|
||||
return card_type_value == Enums.CardType.FORWARD
|
||||
"BACKUP":
|
||||
return card_type_value == Enums.CardType.BACKUP
|
||||
"SUMMON":
|
||||
return card_type_value == Enums.CardType.SUMMON
|
||||
"MONSTER":
|
||||
return card_type_value == Enums.CardType.MONSTER
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _matches_element(card, element: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var element_upper = element.to_upper()
|
||||
var card_element = card.card_data.element
|
||||
|
||||
if card_element is String:
|
||||
return card_element.to_upper() == element_upper
|
||||
|
||||
# Handle Enums.Element enum
|
||||
match element_upper:
|
||||
"FIRE":
|
||||
return card_element == Enums.Element.FIRE
|
||||
"ICE":
|
||||
return card_element == Enums.Element.ICE
|
||||
"WIND":
|
||||
return card_element == Enums.Element.WIND
|
||||
"EARTH":
|
||||
return card_element == Enums.Element.EARTH
|
||||
"LIGHTNING":
|
||||
return card_element == Enums.Element.LIGHTNING
|
||||
"WATER":
|
||||
return card_element == Enums.Element.WATER
|
||||
"LIGHT":
|
||||
return card_element == Enums.Element.LIGHT
|
||||
"DARK":
|
||||
return card_element == Enums.Element.DARK
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _matches_job(card, job: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var card_job = card.card_data.get("job", "")
|
||||
if card_job is String:
|
||||
return card_job.to_lower() == job.to_lower()
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _matches_category(card, category: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var card_categories = card.card_data.get("categories", [])
|
||||
if card_categories is Array:
|
||||
for cat in card_categories:
|
||||
if cat is String and cat.to_lower() == category.to_lower():
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME STATE ACCESSORS
|
||||
# These abstract away the game state interface for flexibility
|
||||
# =============================================================================
|
||||
|
||||
func _get_field_cards(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_field_cards"):
|
||||
return game_state.get_field_cards(player)
|
||||
elif game_state.has_method("get_player_field"):
|
||||
return game_state.get_player_field(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "field" in p:
|
||||
return p.field
|
||||
return []
|
||||
|
||||
|
||||
func _get_player_damage(game_state, player: int) -> int:
|
||||
if game_state.has_method("get_player_damage"):
|
||||
return game_state.get_player_damage(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "damage" in p:
|
||||
return p.damage
|
||||
return 0
|
||||
|
||||
|
||||
func _get_break_zone(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_break_zone"):
|
||||
return game_state.get_break_zone(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "break_zone" in p:
|
||||
return p.break_zone
|
||||
return []
|
||||
|
||||
|
||||
func _get_hand(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_hand"):
|
||||
return game_state.get_hand(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "hand" in p:
|
||||
return p.hand
|
||||
return []
|
||||
|
||||
|
||||
func _get_deck(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_deck"):
|
||||
return game_state.get_deck(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "deck" in p:
|
||||
return p.deck
|
||||
return []
|
||||
|
||||
|
||||
func _get_removed_zone(game_state, player: int) -> Array:
|
||||
if game_state.has_method("get_removed_zone"):
|
||||
return game_state.get_removed_zone(player)
|
||||
elif "players" in game_state and player < game_state.players.size():
|
||||
var p = game_state.players[player]
|
||||
if "removed_zone" in p:
|
||||
return p.removed_zone
|
||||
return []
|
||||
1807
scripts/game/abilities/EffectResolver.gd
Normal file
1807
scripts/game/abilities/EffectResolver.gd
Normal file
File diff suppressed because it is too large
Load Diff
681
scripts/game/abilities/FieldEffectManager.gd
Normal file
681
scripts/game/abilities/FieldEffectManager.gd
Normal file
@@ -0,0 +1,681 @@
|
||||
class_name FieldEffectManager
|
||||
extends RefCounted
|
||||
|
||||
## FieldEffectManager - Manages continuous FIELD abilities
|
||||
## Tracks active field effects and calculates their impact on the game state
|
||||
|
||||
# Active field abilities by source card instance_id
|
||||
var _active_abilities: Dictionary = {} # instance_id -> Array of abilities
|
||||
|
||||
|
||||
## Register field abilities when a card enters the field
|
||||
func register_field_abilities(card: CardInstance, abilities: Array) -> void:
|
||||
var field_abilities: Array = []
|
||||
|
||||
for ability in abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("type") == "FIELD":
|
||||
field_abilities.append({
|
||||
"ability": ability,
|
||||
"source": card
|
||||
})
|
||||
|
||||
if not field_abilities.is_empty():
|
||||
_active_abilities[card.instance_id] = field_abilities
|
||||
|
||||
|
||||
## Unregister field abilities when a card leaves the field
|
||||
func unregister_field_abilities(card: CardInstance) -> void:
|
||||
_active_abilities.erase(card.instance_id)
|
||||
|
||||
|
||||
## Get total power modifier for a card from all active field effects
|
||||
func get_power_modifiers(card: CardInstance, game_state) -> int:
|
||||
var total_modifier: int = 0
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "POWER_MOD":
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
total_modifier += effect.get("amount", 0)
|
||||
|
||||
return total_modifier
|
||||
|
||||
|
||||
## Check if a card has a keyword granted by field effects
|
||||
func has_keyword(card: CardInstance, keyword: String, game_state) -> bool:
|
||||
var keyword_upper = keyword.to_upper()
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "KEYWORD":
|
||||
var granted_keyword = str(effect.get("keyword", "")).to_upper()
|
||||
if granted_keyword == keyword_upper:
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Get all keywords granted to a card by field effects
|
||||
func get_granted_keywords(card: CardInstance, game_state) -> Array:
|
||||
var keywords: Array = []
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "KEYWORD":
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
var keyword = effect.get("keyword", "")
|
||||
if keyword and keyword not in keywords:
|
||||
keywords.append(keyword)
|
||||
|
||||
return keywords
|
||||
|
||||
|
||||
## Check if a card has protection from something via field effects
|
||||
func has_protection(card: CardInstance, protection_type: String, game_state) -> bool:
|
||||
var protection_upper = protection_type.to_upper()
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "PROTECTION":
|
||||
var from = str(effect.get("from", "")).to_upper()
|
||||
if from == protection_upper or from == "ALL":
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Check if a card is affected by a damage modifier
|
||||
func get_damage_modifier(card: CardInstance, game_state) -> int:
|
||||
var total_modifier: int = 0
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "DAMAGE_MODIFIER":
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
total_modifier += effect.get("amount", 0)
|
||||
|
||||
return total_modifier
|
||||
|
||||
|
||||
## Check if a card matches an effect's target specification
|
||||
func _card_matches_effect_target(
|
||||
card: CardInstance,
|
||||
effect: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var target = effect.get("target", {})
|
||||
if target.is_empty():
|
||||
# No target specified, assume applies to source only
|
||||
return card == source
|
||||
|
||||
var target_type = str(target.get("type", "")).to_upper()
|
||||
|
||||
# Check owner
|
||||
var owner = str(target.get("owner", "ANY")).to_upper()
|
||||
match owner:
|
||||
"CONTROLLER":
|
||||
if card.controller_index != source.controller_index:
|
||||
return false
|
||||
"OPPONENT":
|
||||
if card.controller_index == source.controller_index:
|
||||
return false
|
||||
# "ANY" matches all
|
||||
|
||||
# Check if applies to self
|
||||
if target_type == "SELF":
|
||||
return card == source
|
||||
|
||||
# Check if applies to all matching
|
||||
if target_type == "ALL":
|
||||
return _matches_filter(card, target.get("filter", {}), source)
|
||||
|
||||
# Default check filter
|
||||
return _matches_filter(card, target.get("filter", {}), source)
|
||||
|
||||
|
||||
## Check if a card matches a filter (duplicated from TargetSelector for independence)
|
||||
func _matches_filter(
|
||||
card: CardInstance,
|
||||
filter: Dictionary,
|
||||
source: CardInstance
|
||||
) -> bool:
|
||||
if filter.is_empty():
|
||||
return true
|
||||
|
||||
# Card type filter
|
||||
if filter.has("card_type"):
|
||||
var type_str = str(filter.card_type).to_upper()
|
||||
match type_str:
|
||||
"FORWARD":
|
||||
if not card.is_forward():
|
||||
return false
|
||||
"BACKUP":
|
||||
if not card.is_backup():
|
||||
return false
|
||||
"SUMMON":
|
||||
if not card.is_summon():
|
||||
return false
|
||||
"CHARACTER":
|
||||
if not (card.is_forward() or card.is_backup()):
|
||||
return false
|
||||
|
||||
# Element filter
|
||||
if filter.has("element"):
|
||||
var element_str = str(filter.element).to_upper()
|
||||
var element = Enums.element_from_string(element_str)
|
||||
if element not in card.get_elements():
|
||||
return false
|
||||
|
||||
# Cost filters
|
||||
if filter.has("cost_min") and card.card_data.cost < int(filter.cost_min):
|
||||
return false
|
||||
if filter.has("cost_max") and card.card_data.cost > int(filter.cost_max):
|
||||
return false
|
||||
if filter.has("cost") and card.card_data.cost != int(filter.cost):
|
||||
return false
|
||||
|
||||
# Power filters
|
||||
if filter.has("power_min") and card.get_power() < int(filter.power_min):
|
||||
return false
|
||||
if filter.has("power_max") and card.get_power() > int(filter.power_max):
|
||||
return false
|
||||
|
||||
# Name filter
|
||||
if filter.has("name") and card.card_data.name != filter.name:
|
||||
return false
|
||||
|
||||
# Category filter
|
||||
if filter.has("category") and card.card_data.category != filter.category:
|
||||
return false
|
||||
|
||||
# Job filter
|
||||
if filter.has("job") and card.card_data.job != filter.job:
|
||||
return false
|
||||
|
||||
# Exclude self
|
||||
if filter.get("exclude_self", false) and card == source:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Get count of active field abilities
|
||||
func get_active_ability_count() -> int:
|
||||
var count = 0
|
||||
for instance_id in _active_abilities:
|
||||
count += _active_abilities[instance_id].size()
|
||||
return count
|
||||
|
||||
|
||||
## Clear all active abilities (for game reset)
|
||||
func clear_all() -> void:
|
||||
_active_abilities.clear()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BLOCK IMMUNITY CHECKS
|
||||
# =============================================================================
|
||||
|
||||
## Check if a card has block immunity (can't be blocked by certain cards)
|
||||
func has_block_immunity(card: CardInstance, potential_blocker: CardInstance, game_state) -> bool:
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var source = ability_data.source
|
||||
if source != card:
|
||||
continue
|
||||
|
||||
var ability = ability_data.ability
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "BLOCK_IMMUNITY":
|
||||
var condition = effect.get("condition", {})
|
||||
if _blocker_matches_immunity_condition(potential_blocker, condition, card):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Check if blocker matches the immunity condition
|
||||
func _blocker_matches_immunity_condition(
|
||||
blocker: CardInstance,
|
||||
condition: Dictionary,
|
||||
attacker: CardInstance
|
||||
) -> bool:
|
||||
if condition.is_empty():
|
||||
return true # Unconditional block immunity
|
||||
|
||||
var comparison = condition.get("comparison", "")
|
||||
var attribute = condition.get("attribute", "")
|
||||
var value = condition.get("value", 0)
|
||||
var compare_to = condition.get("compare_to", "")
|
||||
|
||||
var blocker_value = 0
|
||||
match attribute:
|
||||
"cost":
|
||||
blocker_value = blocker.card_data.cost if blocker.card_data else 0
|
||||
"power":
|
||||
blocker_value = blocker.get_power()
|
||||
|
||||
var compare_value = value
|
||||
if compare_to == "SELF_POWER":
|
||||
compare_value = attacker.get_power()
|
||||
|
||||
match comparison:
|
||||
"GTE":
|
||||
return blocker_value >= compare_value
|
||||
"GT":
|
||||
return blocker_value > compare_value
|
||||
"LTE":
|
||||
return blocker_value <= compare_value
|
||||
"LT":
|
||||
return blocker_value < compare_value
|
||||
"EQ":
|
||||
return blocker_value == compare_value
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ATTACK RESTRICTION CHECKS
|
||||
# =============================================================================
|
||||
|
||||
## Check if a card has attack restrictions
|
||||
func has_attack_restriction(card: CardInstance, game_state) -> bool:
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "RESTRICTION":
|
||||
var restriction = effect.get("restriction", "")
|
||||
if restriction in ["CANNOT_ATTACK", "CANNOT_ATTACK_OR_BLOCK"]:
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Check if a card has block restrictions
|
||||
func has_block_restriction(card: CardInstance, game_state) -> bool:
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "RESTRICTION":
|
||||
var restriction = effect.get("restriction", "")
|
||||
if restriction in ["CANNOT_BLOCK", "CANNOT_ATTACK_OR_BLOCK"]:
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TAUNT CHECKS (Must be targeted if possible)
|
||||
# =============================================================================
|
||||
|
||||
## Get cards that must be targeted by opponent's abilities if possible
|
||||
func get_taunt_targets(player_index: int, game_state) -> Array:
|
||||
var taunt_cards: Array = []
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "TAUNT":
|
||||
var target = effect.get("target", {})
|
||||
if target.get("type") == "SELF":
|
||||
if source.controller_index == player_index:
|
||||
taunt_cards.append(source)
|
||||
|
||||
return taunt_cards
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COST MODIFICATION
|
||||
# =============================================================================
|
||||
|
||||
## Get cost modifier for playing a card
|
||||
func get_cost_modifier(
|
||||
card_to_play: CardInstance,
|
||||
playing_player: int,
|
||||
game_state
|
||||
) -> int:
|
||||
var total_modifier = 0
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
var effect_type = effect.get("type", "")
|
||||
|
||||
if effect_type == "COST_REDUCTION":
|
||||
if _cost_effect_applies(effect, card_to_play, playing_player, source, game_state):
|
||||
total_modifier -= effect.get("amount", 0)
|
||||
|
||||
elif effect_type == "COST_REDUCTION_SCALING":
|
||||
if _cost_effect_applies(effect, card_to_play, playing_player, source, game_state):
|
||||
var reduction = _calculate_scaling_cost_reduction(effect, source, game_state)
|
||||
total_modifier -= reduction
|
||||
|
||||
elif effect_type == "COST_INCREASE":
|
||||
if _cost_effect_applies(effect, card_to_play, playing_player, source, game_state):
|
||||
total_modifier += effect.get("amount", 0)
|
||||
|
||||
return total_modifier
|
||||
|
||||
|
||||
## Check if a cost modification effect applies to a card being played
|
||||
func _cost_effect_applies(
|
||||
effect: Dictionary,
|
||||
card: CardInstance,
|
||||
player: int,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var for_player = effect.get("for_player", "CONTROLLER")
|
||||
|
||||
# Check if effect applies to this player
|
||||
match for_player:
|
||||
"CONTROLLER":
|
||||
if player != source.controller_index:
|
||||
return false
|
||||
"OPPONENT":
|
||||
if player == source.controller_index:
|
||||
return false
|
||||
|
||||
# Check card filter
|
||||
var card_filter = effect.get("card_filter", "")
|
||||
if card_filter and not _card_matches_name_filter(card, card_filter):
|
||||
return false
|
||||
|
||||
# Check condition
|
||||
var condition = effect.get("condition", {})
|
||||
if not condition.is_empty():
|
||||
if not _cost_condition_met(condition, source, game_state):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card name matches a filter
|
||||
func _card_matches_name_filter(card: CardInstance, filter_text: String) -> bool:
|
||||
if not card or not card.card_data:
|
||||
return false
|
||||
|
||||
var filter_lower = filter_text.to_lower()
|
||||
var card_name = card.card_data.name.to_lower()
|
||||
|
||||
# Direct name match
|
||||
if card_name in filter_lower or filter_lower in card_name:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Check if a cost condition is met
|
||||
func _cost_condition_met(condition: Dictionary, source: CardInstance, game_state) -> bool:
|
||||
if condition.has("control_card_name"):
|
||||
var name_to_find = condition.control_card_name.to_lower()
|
||||
var player = game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
for card in player.field_forwards.get_cards():
|
||||
if name_to_find in card.card_data.name.to_lower():
|
||||
return true
|
||||
for card in player.field_backups.get_cards():
|
||||
if name_to_find in card.card_data.name.to_lower():
|
||||
return true
|
||||
return false
|
||||
|
||||
if condition.has("control_category"):
|
||||
var category = condition.control_category.to_lower()
|
||||
var player = game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
for card in player.field_forwards.get_cards():
|
||||
if category in card.card_data.category.to_lower():
|
||||
return true
|
||||
for card in player.field_backups.get_cards():
|
||||
if category in card.card_data.category.to_lower():
|
||||
return true
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SCALING COST REDUCTION
|
||||
# =============================================================================
|
||||
|
||||
## Calculate cost reduction for a COST_REDUCTION_SCALING effect
|
||||
func _calculate_scaling_cost_reduction(
|
||||
effect: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> int:
|
||||
var reduction_per = effect.get("reduction_per", 1)
|
||||
var scale_by = str(effect.get("scale_by", "")).to_upper()
|
||||
var scale_filter = effect.get("scale_filter", {})
|
||||
|
||||
# Get scale value using similar logic to EffectResolver
|
||||
var scale_value = _get_scale_value(scale_by, source, game_state, scale_filter)
|
||||
|
||||
return scale_value * reduction_per
|
||||
|
||||
|
||||
## Get scale value based on scale_by type (with optional filter)
|
||||
## Mirrors the logic in EffectResolver for consistency
|
||||
func _get_scale_value(
|
||||
scale_by: String,
|
||||
source: CardInstance,
|
||||
game_state,
|
||||
scale_filter: Dictionary = {}
|
||||
) -> int:
|
||||
if not source or not game_state:
|
||||
return 0
|
||||
|
||||
var player_index = source.controller_index
|
||||
var player = game_state.get_player(player_index)
|
||||
if not player:
|
||||
return 0
|
||||
|
||||
# Determine owner from filter (default to CONTROLLER)
|
||||
var owner = scale_filter.get("owner", "CONTROLLER").to_upper() if scale_filter else "CONTROLLER"
|
||||
|
||||
# Get cards based on scale_by and owner
|
||||
var cards_to_count: Array = []
|
||||
|
||||
match scale_by:
|
||||
"DAMAGE_RECEIVED":
|
||||
# Special case - not card-based
|
||||
return _get_damage_for_owner(owner, player_index, game_state)
|
||||
"FORWARDS_CONTROLLED", "FORWARDS":
|
||||
cards_to_count = _get_forwards_for_owner(owner, player_index, game_state)
|
||||
"BACKUPS_CONTROLLED", "BACKUPS":
|
||||
cards_to_count = _get_backups_for_owner(owner, player_index, game_state)
|
||||
"FIELD_CARDS_CONTROLLED", "FIELD_CARDS":
|
||||
cards_to_count = _get_field_cards_for_owner(owner, player_index, game_state)
|
||||
"CARDS_IN_HAND":
|
||||
cards_to_count = _get_hand_for_owner(owner, player_index, game_state)
|
||||
"CARDS_IN_BREAK_ZONE":
|
||||
cards_to_count = _get_break_zone_for_owner(owner, player_index, game_state)
|
||||
"OPPONENT_FORWARDS":
|
||||
cards_to_count = _get_forwards_for_owner("OPPONENT", player_index, game_state)
|
||||
"OPPONENT_BACKUPS":
|
||||
cards_to_count = _get_backups_for_owner("OPPONENT", player_index, game_state)
|
||||
_:
|
||||
push_warning("FieldEffectManager: Unknown scale_by type: " + scale_by)
|
||||
return 0
|
||||
|
||||
# If no filter, just return count
|
||||
if not scale_filter or scale_filter.is_empty() or (scale_filter.size() == 1 and scale_filter.has("owner")):
|
||||
return cards_to_count.size()
|
||||
|
||||
# Apply filter and count matching cards using CardFilter utility
|
||||
return CardFilter.count_matching(cards_to_count, scale_filter)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OWNER-BASED ACCESS HELPERS FOR SCALING
|
||||
# =============================================================================
|
||||
|
||||
func _get_forwards_for_owner(owner: String, player_index: int, game_state) -> Array:
|
||||
match owner.to_upper():
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(player_index)
|
||||
return player.field_forwards.get_cards() if player and player.field_forwards else []
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - player_index)
|
||||
return opponent.field_forwards.get_cards() if opponent and opponent.field_forwards else []
|
||||
_:
|
||||
var all_cards = []
|
||||
for p in game_state.players:
|
||||
if p and p.field_forwards:
|
||||
all_cards.append_array(p.field_forwards.get_cards())
|
||||
return all_cards
|
||||
|
||||
|
||||
func _get_backups_for_owner(owner: String, player_index: int, game_state) -> Array:
|
||||
match owner.to_upper():
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(player_index)
|
||||
return player.field_backups.get_cards() if player and player.field_backups else []
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - player_index)
|
||||
return opponent.field_backups.get_cards() if opponent and opponent.field_backups else []
|
||||
_:
|
||||
var all_cards = []
|
||||
for p in game_state.players:
|
||||
if p and p.field_backups:
|
||||
all_cards.append_array(p.field_backups.get_cards())
|
||||
return all_cards
|
||||
|
||||
|
||||
func _get_field_cards_for_owner(owner: String, player_index: int, game_state) -> Array:
|
||||
var cards = []
|
||||
cards.append_array(_get_forwards_for_owner(owner, player_index, game_state))
|
||||
cards.append_array(_get_backups_for_owner(owner, player_index, game_state))
|
||||
return cards
|
||||
|
||||
|
||||
func _get_hand_for_owner(owner: String, player_index: int, game_state) -> Array:
|
||||
match owner.to_upper():
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(player_index)
|
||||
return player.hand.get_cards() if player and player.hand else []
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - player_index)
|
||||
return opponent.hand.get_cards() if opponent and opponent.hand else []
|
||||
_:
|
||||
var all_cards = []
|
||||
for p in game_state.players:
|
||||
if p and p.hand:
|
||||
all_cards.append_array(p.hand.get_cards())
|
||||
return all_cards
|
||||
|
||||
|
||||
func _get_break_zone_for_owner(owner: String, player_index: int, game_state) -> Array:
|
||||
match owner.to_upper():
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(player_index)
|
||||
return player.break_zone.get_cards() if player and player.break_zone else []
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - player_index)
|
||||
return opponent.break_zone.get_cards() if opponent and opponent.break_zone else []
|
||||
_:
|
||||
var all_cards = []
|
||||
for p in game_state.players:
|
||||
if p and p.break_zone:
|
||||
all_cards.append_array(p.break_zone.get_cards())
|
||||
return all_cards
|
||||
|
||||
|
||||
func _get_damage_for_owner(owner: String, player_index: int, game_state) -> int:
|
||||
match owner.to_upper():
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(player_index)
|
||||
return player.damage if player and "damage" in player else 0
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - player_index)
|
||||
return opponent.damage if opponent and "damage" in opponent else 0
|
||||
_:
|
||||
var total = 0
|
||||
for p in game_state.players:
|
||||
if p and "damage" in p:
|
||||
total += p.damage
|
||||
return total
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MULTI-ATTACK CHECKS
|
||||
# =============================================================================
|
||||
|
||||
## Get maximum attacks allowed for a card this turn
|
||||
func get_max_attacks(card: CardInstance, game_state) -> int:
|
||||
var max_attacks = 1 # Default is 1 attack per turn
|
||||
|
||||
for instance_id in _active_abilities:
|
||||
var abilities = _active_abilities[instance_id]
|
||||
for ability_data in abilities:
|
||||
var ability = ability_data.ability
|
||||
var source = ability_data.source
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
for effect in parsed.get("effects", []):
|
||||
if effect.get("type") == "MULTI_ATTACK":
|
||||
if _card_matches_effect_target(card, effect, source, game_state):
|
||||
var attack_count = effect.get("attack_count", 1)
|
||||
if attack_count > max_attacks:
|
||||
max_attacks = attack_count
|
||||
|
||||
return max_attacks
|
||||
174
scripts/game/abilities/TargetSelector.gd
Normal file
174
scripts/game/abilities/TargetSelector.gd
Normal file
@@ -0,0 +1,174 @@
|
||||
class_name TargetSelector
|
||||
extends RefCounted
|
||||
|
||||
## TargetSelector - Validates and provides target options for effects
|
||||
|
||||
|
||||
## Get all valid targets for an effect's target specification
|
||||
func get_valid_targets(
|
||||
target_spec: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
if target_spec.is_empty():
|
||||
return []
|
||||
|
||||
var candidates: Array = []
|
||||
|
||||
var zone = str(target_spec.get("zone", "FIELD")).to_upper()
|
||||
var owner = str(target_spec.get("owner", "ANY")).to_upper()
|
||||
var filter = target_spec.get("filter", {})
|
||||
var target_type = str(target_spec.get("type", "CHOOSE")).to_upper()
|
||||
|
||||
# Handle SELF and ALL targets specially
|
||||
if target_type == "SELF":
|
||||
return [source]
|
||||
elif target_type == "ALL":
|
||||
return _get_all_matching(owner, zone, filter, source, game_state)
|
||||
|
||||
# Collect candidates from appropriate zones
|
||||
match zone:
|
||||
"FIELD":
|
||||
candidates = _get_field_cards(owner, source, game_state)
|
||||
"HAND":
|
||||
candidates = _get_hand_cards(owner, source, game_state)
|
||||
"BREAK_ZONE", "BREAK":
|
||||
candidates = _get_break_zone_cards(owner, source, game_state)
|
||||
"DECK":
|
||||
candidates = _get_deck_cards(owner, source, game_state)
|
||||
_:
|
||||
# Default to field
|
||||
candidates = _get_field_cards(owner, source, game_state)
|
||||
|
||||
# Apply filters using CardFilter utility
|
||||
return CardFilter.get_matching(candidates, filter, source)
|
||||
|
||||
|
||||
## Get all cards matching filter (for "ALL" target type)
|
||||
func _get_all_matching(
|
||||
owner: String,
|
||||
zone: String,
|
||||
filter: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var candidates = _get_field_cards(owner, source, game_state)
|
||||
return CardFilter.get_matching(candidates, filter, source)
|
||||
|
||||
|
||||
## Get cards from field
|
||||
func _get_field_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var cards: Array = []
|
||||
|
||||
match owner:
|
||||
"CONTROLLER":
|
||||
var player = game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
cards.append_array(_get_player_field_cards(player))
|
||||
"OPPONENT":
|
||||
var opponent = game_state.get_player(1 - source.controller_index)
|
||||
if opponent:
|
||||
cards.append_array(_get_player_field_cards(opponent))
|
||||
"ANY", _:
|
||||
for player in game_state.players:
|
||||
cards.append_array(_get_player_field_cards(player))
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Get all field cards for a player
|
||||
func _get_player_field_cards(player) -> Array:
|
||||
var cards: Array = []
|
||||
cards.append_array(player.field_forwards.get_cards())
|
||||
cards.append_array(player.field_backups.get_cards())
|
||||
return cards
|
||||
|
||||
|
||||
## Get cards from hand
|
||||
func _get_hand_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var cards: Array = []
|
||||
var player_index = source.controller_index
|
||||
|
||||
if owner == "OPPONENT":
|
||||
player_index = 1 - player_index
|
||||
|
||||
var player = game_state.get_player(player_index)
|
||||
if player:
|
||||
cards.append_array(player.hand.get_cards())
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Get cards from break zone
|
||||
func _get_break_zone_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
var cards: Array = []
|
||||
var player_index = source.controller_index
|
||||
|
||||
if owner == "OPPONENT":
|
||||
player_index = 1 - player_index
|
||||
|
||||
var player = game_state.get_player(player_index)
|
||||
if player:
|
||||
cards.append_array(player.break_zone.get_cards())
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Get cards from deck
|
||||
func _get_deck_cards(
|
||||
owner: String,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> Array:
|
||||
# Usually not directly targetable, used for search effects
|
||||
var cards: Array = []
|
||||
var player_index = source.controller_index
|
||||
|
||||
if owner == "OPPONENT":
|
||||
player_index = 1 - player_index
|
||||
|
||||
var player = game_state.get_player(player_index)
|
||||
if player:
|
||||
cards.append_array(player.deck.get_cards())
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
## Validate that a set of targets meets the target specification requirements
|
||||
func validate_targets(
|
||||
targets: Array,
|
||||
target_spec: Dictionary,
|
||||
source: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var target_type = str(target_spec.get("type", "CHOOSE")).to_upper()
|
||||
|
||||
# Check count requirements
|
||||
if target_spec.has("count"):
|
||||
var required = int(target_spec.count)
|
||||
if targets.size() != required:
|
||||
return false
|
||||
elif target_spec.has("count_up_to"):
|
||||
var max_count = int(target_spec.count_up_to)
|
||||
if targets.size() > max_count:
|
||||
return false
|
||||
|
||||
# Validate each target is valid
|
||||
var valid_targets = get_valid_targets(target_spec, source, game_state)
|
||||
for target in targets:
|
||||
if target not in valid_targets:
|
||||
return false
|
||||
|
||||
return true
|
||||
233
scripts/game/abilities/TriggerMatcher.gd
Normal file
233
scripts/game/abilities/TriggerMatcher.gd
Normal file
@@ -0,0 +1,233 @@
|
||||
class_name TriggerMatcher
|
||||
extends RefCounted
|
||||
|
||||
## TriggerMatcher - Matches game events to ability triggers
|
||||
## Scans all cards on field for abilities that trigger from the given event
|
||||
|
||||
## Reference to ConditionChecker for evaluating trigger conditions
|
||||
var condition_checker: ConditionChecker = null
|
||||
|
||||
|
||||
## Find all abilities that should trigger for a given event
|
||||
func find_triggered_abilities(
|
||||
event_type: String,
|
||||
event_data: Dictionary,
|
||||
game_state,
|
||||
all_abilities: Dictionary
|
||||
) -> Array:
|
||||
var triggered = []
|
||||
|
||||
# Check abilities on all cards in play
|
||||
for player in game_state.players:
|
||||
# Check forwards
|
||||
for card in player.field_forwards.get_cards():
|
||||
var card_abilities = all_abilities.get(card.card_data.id, [])
|
||||
triggered.append_array(_check_card_abilities(card, card_abilities, event_type, event_data, game_state))
|
||||
|
||||
# Check backups
|
||||
for card in player.field_backups.get_cards():
|
||||
var card_abilities = all_abilities.get(card.card_data.id, [])
|
||||
triggered.append_array(_check_card_abilities(card, card_abilities, event_type, event_data, game_state))
|
||||
|
||||
return triggered
|
||||
|
||||
|
||||
## Check all abilities on a card for triggers
|
||||
func _check_card_abilities(
|
||||
card: CardInstance,
|
||||
abilities: Array,
|
||||
event_type: String,
|
||||
event_data: Dictionary,
|
||||
game_state
|
||||
) -> Array:
|
||||
var triggered = []
|
||||
|
||||
for ability in abilities:
|
||||
if _matches_trigger(ability, event_type, event_data, card, game_state):
|
||||
triggered.append({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": event_data
|
||||
})
|
||||
|
||||
return triggered
|
||||
|
||||
|
||||
## Check if an ability's trigger matches the event
|
||||
func _matches_trigger(
|
||||
ability: Dictionary,
|
||||
event_type: String,
|
||||
event_data: Dictionary,
|
||||
source_card: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.is_empty():
|
||||
return false
|
||||
|
||||
# Only AUTO abilities have triggers
|
||||
if parsed.get("type") != "AUTO":
|
||||
return false
|
||||
|
||||
var trigger = parsed.get("trigger", {})
|
||||
if trigger.is_empty():
|
||||
return false
|
||||
|
||||
# Check event type matches
|
||||
var trigger_event = trigger.get("event", "")
|
||||
if not _event_matches(trigger_event, event_type):
|
||||
return false
|
||||
|
||||
# Check source filter
|
||||
var trigger_source = trigger.get("source", "ANY")
|
||||
if not _source_matches(trigger_source, event_data, source_card, game_state):
|
||||
return false
|
||||
|
||||
# Check additional trigger filters
|
||||
if trigger.has("source_filter"):
|
||||
var filter = trigger.source_filter
|
||||
var event_card = event_data.get("card")
|
||||
if event_card and not _matches_card_filter(event_card, filter):
|
||||
return false
|
||||
|
||||
# Check trigger condition (if present)
|
||||
var trigger_condition = trigger.get("condition", {})
|
||||
if not trigger_condition.is_empty() and condition_checker:
|
||||
var context = {
|
||||
"source_card": source_card,
|
||||
"target_card": event_data.get("card"),
|
||||
"game_state": game_state,
|
||||
"player_id": source_card.controller_index if source_card else 0,
|
||||
"event_data": event_data
|
||||
}
|
||||
if not condition_checker.evaluate(trigger_condition, context):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if event type matches trigger event
|
||||
func _event_matches(trigger_event: String, actual_event: String) -> bool:
|
||||
# Direct match
|
||||
if trigger_event == actual_event:
|
||||
return true
|
||||
|
||||
# Handle variations
|
||||
match trigger_event:
|
||||
"ENTERS_FIELD":
|
||||
return actual_event in ["ENTERS_FIELD", "CARD_PLAYED"]
|
||||
"LEAVES_FIELD":
|
||||
return actual_event in ["LEAVES_FIELD", "FORWARD_BROKEN", "CARD_BROKEN"]
|
||||
"DEALS_DAMAGE":
|
||||
return actual_event in ["DEALS_DAMAGE", "DEALS_DAMAGE_TO_OPPONENT", "DEALS_DAMAGE_TO_FORWARD"]
|
||||
"DEALS_DAMAGE_TO_OPPONENT":
|
||||
return actual_event == "DEALS_DAMAGE_TO_OPPONENT"
|
||||
"DEALS_DAMAGE_TO_FORWARD":
|
||||
return actual_event == "DEALS_DAMAGE_TO_FORWARD"
|
||||
"BLOCKS_OR_IS_BLOCKED":
|
||||
return actual_event in ["BLOCKS", "IS_BLOCKED"]
|
||||
|
||||
return false
|
||||
|
||||
|
||||
## Check if source matches trigger requirements
|
||||
func _source_matches(
|
||||
trigger_source: String,
|
||||
event_data: Dictionary,
|
||||
source_card: CardInstance,
|
||||
game_state
|
||||
) -> bool:
|
||||
var event_card = event_data.get("card")
|
||||
|
||||
match trigger_source:
|
||||
"SELF":
|
||||
# Trigger source must be this card
|
||||
return event_card == source_card
|
||||
"CONTROLLER":
|
||||
# Trigger source must be controlled by same player
|
||||
if event_card:
|
||||
return event_card.controller_index == source_card.controller_index
|
||||
var event_player = event_data.get("player", -1)
|
||||
return event_player == source_card.controller_index
|
||||
"OPPONENT":
|
||||
# Trigger source must be controlled by opponent
|
||||
if event_card:
|
||||
return event_card.controller_index != source_card.controller_index
|
||||
var event_player = event_data.get("player", -1)
|
||||
return event_player != source_card.controller_index and event_player >= 0
|
||||
"ANY", _:
|
||||
# Any source triggers
|
||||
return true
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Check if a card matches a filter
|
||||
func _matches_card_filter(card: CardInstance, filter: Dictionary) -> bool:
|
||||
if filter.is_empty():
|
||||
return true
|
||||
|
||||
# Card type filter
|
||||
if filter.has("card_type"):
|
||||
var type_str = str(filter.card_type).to_upper()
|
||||
match type_str:
|
||||
"FORWARD":
|
||||
if not card.is_forward():
|
||||
return false
|
||||
"BACKUP":
|
||||
if not card.is_backup():
|
||||
return false
|
||||
"SUMMON":
|
||||
if not card.is_summon():
|
||||
return false
|
||||
|
||||
# Element filter
|
||||
if filter.has("element"):
|
||||
var element_str = str(filter.element).to_upper()
|
||||
var element = Enums.element_from_string(element_str)
|
||||
if element not in card.get_elements():
|
||||
return false
|
||||
|
||||
# Cost filters
|
||||
if filter.has("cost_min"):
|
||||
if card.card_data.cost < filter.cost_min:
|
||||
return false
|
||||
if filter.has("cost_max"):
|
||||
if card.card_data.cost > filter.cost_max:
|
||||
return false
|
||||
if filter.has("cost"):
|
||||
if card.card_data.cost != filter.cost:
|
||||
return false
|
||||
|
||||
# Power filters
|
||||
if filter.has("power_min"):
|
||||
if card.get_power() < filter.power_min:
|
||||
return false
|
||||
if filter.has("power_max"):
|
||||
if card.get_power() > filter.power_max:
|
||||
return false
|
||||
|
||||
# State filters
|
||||
if filter.has("is_dull"):
|
||||
if card.is_dull() != filter.is_dull:
|
||||
return false
|
||||
if filter.has("is_active"):
|
||||
if card.is_active() != filter.is_active:
|
||||
return false
|
||||
|
||||
# Name filter
|
||||
if filter.has("name"):
|
||||
if card.card_data.name != filter.name:
|
||||
return false
|
||||
|
||||
# Category filter
|
||||
if filter.has("category"):
|
||||
if card.card_data.category != filter.category:
|
||||
return false
|
||||
|
||||
# Job filter
|
||||
if filter.has("job"):
|
||||
if card.card_data.job != filter.job:
|
||||
return false
|
||||
|
||||
return true
|
||||
223
scripts/game/ai/AIController.gd
Normal file
223
scripts/game/ai/AIController.gd
Normal file
@@ -0,0 +1,223 @@
|
||||
class_name AIController
|
||||
extends Node
|
||||
|
||||
## AIController - Coordinates AI player turns
|
||||
## Handles timing, action execution, and phase transitions
|
||||
|
||||
signal ai_action_started
|
||||
signal ai_action_completed
|
||||
signal ai_thinking(player_index: int)
|
||||
|
||||
var strategy: AIStrategy
|
||||
var game_state: GameState
|
||||
var player_index: int
|
||||
var is_processing: bool = false
|
||||
|
||||
# Reference to GameManager for executing actions
|
||||
var _game_manager: Node
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func setup(p_player_index: int, difficulty: AIStrategy.Difficulty, p_game_manager: Node) -> void:
|
||||
player_index = p_player_index
|
||||
_game_manager = p_game_manager
|
||||
|
||||
# Create appropriate strategy based on difficulty
|
||||
match difficulty:
|
||||
AIStrategy.Difficulty.EASY:
|
||||
strategy = EasyAI.new(player_index)
|
||||
AIStrategy.Difficulty.NORMAL:
|
||||
strategy = NormalAI.new(player_index)
|
||||
AIStrategy.Difficulty.HARD:
|
||||
strategy = HardAI.new(player_index)
|
||||
|
||||
|
||||
func set_game_state(state: GameState) -> void:
|
||||
game_state = state
|
||||
if strategy:
|
||||
strategy.set_game_state(state)
|
||||
|
||||
|
||||
## Called when it's the AI's turn to act in the current phase
|
||||
func process_turn() -> void:
|
||||
if is_processing:
|
||||
return
|
||||
|
||||
is_processing = true
|
||||
ai_thinking.emit(player_index)
|
||||
|
||||
# Add thinking delay
|
||||
var delay := strategy.get_thinking_delay()
|
||||
await get_tree().create_timer(delay).timeout
|
||||
|
||||
var phase := game_state.turn_manager.current_phase
|
||||
|
||||
match phase:
|
||||
Enums.TurnPhase.ACTIVE:
|
||||
# Active phase is automatic - no AI decision needed
|
||||
_pass_priority()
|
||||
|
||||
Enums.TurnPhase.DRAW:
|
||||
# Draw phase is automatic
|
||||
_pass_priority()
|
||||
|
||||
Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2:
|
||||
await _process_main_phase()
|
||||
|
||||
Enums.TurnPhase.ATTACK:
|
||||
await _process_attack_phase()
|
||||
|
||||
Enums.TurnPhase.END:
|
||||
# End phase is automatic
|
||||
_pass_priority()
|
||||
|
||||
is_processing = false
|
||||
ai_action_completed.emit()
|
||||
|
||||
|
||||
## Process main phase - play cards or pass
|
||||
func _process_main_phase() -> void:
|
||||
var max_actions := 10 # Prevent infinite loops
|
||||
var actions_taken := 0
|
||||
|
||||
while actions_taken < max_actions:
|
||||
var decision := strategy.decide_main_phase_action()
|
||||
|
||||
if decision.action == "pass":
|
||||
_pass_priority()
|
||||
break
|
||||
|
||||
elif decision.action == "play":
|
||||
var card: CardInstance = decision.card
|
||||
var success := await _try_play_card(card)
|
||||
|
||||
if not success:
|
||||
# Couldn't play - pass
|
||||
_pass_priority()
|
||||
break
|
||||
|
||||
# Small delay between actions
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
|
||||
actions_taken += 1
|
||||
|
||||
|
||||
## Try to play a card, handling CP generation if needed
|
||||
func _try_play_card(card: CardInstance) -> bool:
|
||||
var player := game_state.get_player(player_index)
|
||||
var cost := card.card_data.cost
|
||||
|
||||
# Check if we have enough CP
|
||||
var current_cp := player.cp_pool.get_total_cp()
|
||||
|
||||
if current_cp < cost:
|
||||
# Need to generate CP
|
||||
var needed := cost - current_cp
|
||||
var success := await _generate_cp(needed, card.card_data.elements)
|
||||
if not success:
|
||||
return false
|
||||
|
||||
# Try to play the card
|
||||
return _game_manager.try_play_card(card)
|
||||
|
||||
|
||||
## Generate CP by dulling backups or discarding cards
|
||||
func _generate_cp(needed: int, elements: Array) -> bool:
|
||||
var generated := 0
|
||||
var max_attempts := 20
|
||||
|
||||
while generated < needed and max_attempts > 0:
|
||||
var decision := strategy.decide_cp_generation({ "needed": needed - generated, "elements": elements })
|
||||
|
||||
if decision.is_empty():
|
||||
return false
|
||||
|
||||
if decision.action == "dull_backup":
|
||||
var backup: CardInstance = decision.card
|
||||
if _game_manager.dull_backup_for_cp(backup):
|
||||
generated += 1
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
|
||||
elif decision.action == "discard":
|
||||
var discard_card: CardInstance = decision.card
|
||||
if _game_manager.discard_card_for_cp(discard_card):
|
||||
generated += 2
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
|
||||
max_attempts -= 1
|
||||
|
||||
return generated >= needed
|
||||
|
||||
|
||||
## Process attack phase - declare attacks
|
||||
func _process_attack_phase() -> void:
|
||||
var attack_step := game_state.turn_manager.attack_step
|
||||
|
||||
match attack_step:
|
||||
Enums.AttackStep.PREPARATION, Enums.AttackStep.DECLARATION:
|
||||
await _process_attack_declaration()
|
||||
|
||||
Enums.AttackStep.BLOCK_DECLARATION:
|
||||
# This shouldn't happen - AI blocks are handled in opponent's turn
|
||||
_pass_priority()
|
||||
|
||||
Enums.AttackStep.DAMAGE_RESOLUTION:
|
||||
# Automatic
|
||||
_pass_priority()
|
||||
|
||||
|
||||
## Declare attacks with forwards
|
||||
func _process_attack_declaration() -> void:
|
||||
var max_attacks := 5
|
||||
var attacks_made := 0
|
||||
|
||||
while attacks_made < max_attacks:
|
||||
var decision := strategy.decide_attack_action()
|
||||
|
||||
if decision.action == "end_attacks":
|
||||
# End attack phase
|
||||
_game_manager.pass_priority()
|
||||
break
|
||||
|
||||
elif decision.action == "attack":
|
||||
var attacker: CardInstance = decision.card
|
||||
var success := _game_manager.declare_attack(attacker)
|
||||
|
||||
if success:
|
||||
attacks_made += 1
|
||||
# Wait for block decision or damage resolution
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
else:
|
||||
# Couldn't attack - end attacks
|
||||
_game_manager.pass_priority()
|
||||
break
|
||||
|
||||
|
||||
## Called when AI needs to decide on blocking
|
||||
func process_block_decision(attacker: CardInstance) -> void:
|
||||
if is_processing:
|
||||
return
|
||||
|
||||
is_processing = true
|
||||
ai_thinking.emit(player_index)
|
||||
|
||||
var delay := strategy.get_thinking_delay()
|
||||
await get_tree().create_timer(delay).timeout
|
||||
|
||||
var decision := strategy.decide_block_action(attacker)
|
||||
|
||||
if decision.action == "block":
|
||||
var blocker: CardInstance = decision.card
|
||||
_game_manager.declare_block(blocker)
|
||||
else:
|
||||
_game_manager.skip_block()
|
||||
|
||||
is_processing = false
|
||||
ai_action_completed.emit()
|
||||
|
||||
|
||||
func _pass_priority() -> void:
|
||||
_game_manager.pass_priority()
|
||||
190
scripts/game/ai/AIStrategy.gd
Normal file
190
scripts/game/ai/AIStrategy.gd
Normal file
@@ -0,0 +1,190 @@
|
||||
class_name AIStrategy
|
||||
extends RefCounted
|
||||
|
||||
## Base class for AI decision-making strategies
|
||||
## Subclasses implement different difficulty levels
|
||||
|
||||
enum Difficulty { EASY, NORMAL, HARD }
|
||||
|
||||
var difficulty: Difficulty
|
||||
var player_index: int
|
||||
var game_state: GameState
|
||||
|
||||
|
||||
func _init(p_difficulty: Difficulty, p_player_index: int) -> void:
|
||||
difficulty = p_difficulty
|
||||
player_index = p_player_index
|
||||
|
||||
|
||||
func set_game_state(state: GameState) -> void:
|
||||
game_state = state
|
||||
|
||||
|
||||
## Returns the player this AI controls
|
||||
func get_player() -> Player:
|
||||
return game_state.get_player(player_index)
|
||||
|
||||
|
||||
## Returns the opponent player
|
||||
func get_opponent() -> Player:
|
||||
return game_state.get_player(1 - player_index)
|
||||
|
||||
|
||||
## Called during Main Phase - decide what card to play or pass
|
||||
## Returns: { "action": "play", "card": CardInstance } or { "action": "pass" }
|
||||
func decide_main_phase_action() -> Dictionary:
|
||||
push_error("AIStrategy.decide_main_phase_action() must be overridden")
|
||||
return { "action": "pass" }
|
||||
|
||||
|
||||
## Called when CP is needed - decide how to generate CP
|
||||
## Returns: { "action": "discard", "card": CardInstance } or { "action": "dull_backup", "card": CardInstance }
|
||||
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
|
||||
push_error("AIStrategy.decide_cp_generation() must be overridden")
|
||||
return {}
|
||||
|
||||
|
||||
## Called during Attack Phase - decide which forward to attack with
|
||||
## Returns: { "action": "attack", "card": CardInstance } or { "action": "end_attacks" }
|
||||
func decide_attack_action() -> Dictionary:
|
||||
push_error("AIStrategy.decide_attack_action() must be overridden")
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
|
||||
## Called during Block Declaration - decide how to block
|
||||
## Returns: { "action": "block", "card": CardInstance } or { "action": "skip" }
|
||||
func decide_block_action(attacker: CardInstance) -> Dictionary:
|
||||
push_error("AIStrategy.decide_block_action() must be overridden")
|
||||
return { "action": "skip" }
|
||||
|
||||
|
||||
## Get thinking delay range in seconds based on difficulty
|
||||
func get_thinking_delay() -> float:
|
||||
match difficulty:
|
||||
Difficulty.EASY:
|
||||
return randf_range(1.5, 2.5)
|
||||
Difficulty.NORMAL:
|
||||
return randf_range(1.0, 1.5)
|
||||
Difficulty.HARD:
|
||||
return randf_range(0.5, 1.0)
|
||||
return 1.0
|
||||
|
||||
|
||||
# ============ HELPER METHODS FOR SUBCLASSES ============
|
||||
|
||||
## Get all cards in hand that can be played (have enough CP or can generate CP)
|
||||
func get_playable_cards() -> Array[CardInstance]:
|
||||
var player := get_player()
|
||||
var playable: Array[CardInstance] = []
|
||||
|
||||
for card in player.hand.get_cards():
|
||||
if _can_afford_card(card):
|
||||
playable.append(card)
|
||||
|
||||
return playable
|
||||
|
||||
|
||||
## Check if a card can be afforded (either have CP or can generate it)
|
||||
func _can_afford_card(card: CardInstance) -> bool:
|
||||
var player := get_player()
|
||||
var cost := card.card_data.cost
|
||||
var elements := card.card_data.elements
|
||||
|
||||
# Check if we already have enough CP
|
||||
var current_cp := player.cp_pool.get_total_cp()
|
||||
if current_cp >= cost:
|
||||
# Check element requirements
|
||||
for element in elements:
|
||||
if player.cp_pool.get_cp(element) > 0 or player.cp_pool.get_cp(Enums.Element.NONE) > 0:
|
||||
return true
|
||||
# If no specific element needed (Light/Dark cards), any CP works
|
||||
if elements.is_empty():
|
||||
return true
|
||||
|
||||
# Check if we can generate enough CP
|
||||
var potential_cp := _calculate_potential_cp()
|
||||
return potential_cp >= cost
|
||||
|
||||
|
||||
## Calculate total CP we could generate (hand discards + backup dulls)
|
||||
func _calculate_potential_cp() -> int:
|
||||
var player := get_player()
|
||||
var total := player.cp_pool.get_total_cp()
|
||||
|
||||
# Each card in hand can be discarded for 2 CP
|
||||
total += player.hand.get_card_count() * 2
|
||||
|
||||
# Each active backup can be dulled for 1 CP
|
||||
for backup in player.field_backups.get_cards():
|
||||
if backup.state == Enums.CardState.ACTIVE:
|
||||
total += 1
|
||||
|
||||
return total
|
||||
|
||||
|
||||
## Get forwards that can attack
|
||||
func get_attackable_forwards() -> Array[CardInstance]:
|
||||
return get_player().get_attackable_forwards()
|
||||
|
||||
|
||||
## Get forwards that can block
|
||||
func get_blockable_forwards() -> Array[CardInstance]:
|
||||
return get_player().get_blockable_forwards()
|
||||
|
||||
|
||||
## Calculate a simple card value score
|
||||
func calculate_card_value(card: CardInstance) -> float:
|
||||
var data := card.card_data
|
||||
var value := 0.0
|
||||
|
||||
match data.type:
|
||||
Enums.CardType.FORWARD:
|
||||
# Forwards valued by power/cost ratio + abilities
|
||||
value = float(data.power) / float(max(data.cost, 1))
|
||||
if data.has_ability("Brave"):
|
||||
value *= 1.3
|
||||
if data.has_ability("First Strike"):
|
||||
value *= 1.2
|
||||
if data.has_ability("Haste"):
|
||||
value *= 1.4
|
||||
Enums.CardType.BACKUP:
|
||||
# Backups valued by utility (cost efficiency)
|
||||
value = 3.0 / float(max(data.cost, 1))
|
||||
Enums.CardType.SUMMON:
|
||||
# Summons valued by effect strength (approximated by cost)
|
||||
value = float(data.cost) * 0.8
|
||||
Enums.CardType.MONSTER:
|
||||
# Monsters similar to forwards
|
||||
value = float(data.power) / float(max(data.cost, 1))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
## Evaluate board advantage (positive = we're ahead)
|
||||
func evaluate_board_state() -> float:
|
||||
var player := get_player()
|
||||
var opponent := get_opponent()
|
||||
|
||||
var score := 0.0
|
||||
|
||||
# Forward power advantage
|
||||
var our_power := 0
|
||||
for forward in player.field_forwards.get_cards():
|
||||
our_power += forward.get_power()
|
||||
|
||||
var their_power := 0
|
||||
for forward in opponent.field_forwards.get_cards():
|
||||
their_power += forward.get_power()
|
||||
|
||||
score += (our_power - their_power) / 1000.0
|
||||
|
||||
# Backup count advantage
|
||||
score += (player.field_backups.get_card_count() - opponent.field_backups.get_card_count()) * 2.0
|
||||
|
||||
# Hand size advantage
|
||||
score += (player.hand.get_card_count() - opponent.hand.get_card_count()) * 0.5
|
||||
|
||||
# Damage disadvantage (more damage = worse)
|
||||
score -= (player.get_damage_count() - opponent.get_damage_count()) * 3.0
|
||||
|
||||
return score
|
||||
71
scripts/game/ai/EasyAI.gd
Normal file
71
scripts/game/ai/EasyAI.gd
Normal file
@@ -0,0 +1,71 @@
|
||||
class_name EasyAI
|
||||
extends AIStrategy
|
||||
|
||||
## Easy AI - Makes suboptimal choices, sometimes skips good plays
|
||||
## Good for beginners learning the game
|
||||
|
||||
|
||||
func _init(p_player_index: int) -> void:
|
||||
super._init(Difficulty.EASY, p_player_index)
|
||||
|
||||
|
||||
func decide_main_phase_action() -> Dictionary:
|
||||
var playable := get_playable_cards()
|
||||
|
||||
if playable.is_empty():
|
||||
return { "action": "pass" }
|
||||
|
||||
# 30% chance to just pass even if we have playable cards
|
||||
if randf() < 0.3:
|
||||
return { "action": "pass" }
|
||||
|
||||
# Pick a random playable card (not optimal)
|
||||
var card: CardInstance = playable[randi() % playable.size()]
|
||||
return { "action": "play", "card": card }
|
||||
|
||||
|
||||
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
|
||||
var player := get_player()
|
||||
|
||||
# Prefer dulling backups first (Easy AI doesn't optimize)
|
||||
for backup in player.field_backups.get_cards():
|
||||
if backup.state == Enums.CardState.ACTIVE:
|
||||
return { "action": "dull_backup", "card": backup }
|
||||
|
||||
# Discard a random card from hand
|
||||
var hand_cards := player.hand.get_cards()
|
||||
if not hand_cards.is_empty():
|
||||
var card: CardInstance = hand_cards[randi() % hand_cards.size()]
|
||||
return { "action": "discard", "card": card }
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
func decide_attack_action() -> Dictionary:
|
||||
var attackers := get_attackable_forwards()
|
||||
|
||||
if attackers.is_empty():
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
# 40% chance to not attack even if we can
|
||||
if randf() < 0.4:
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
# Pick a random attacker
|
||||
var attacker: CardInstance = attackers[randi() % attackers.size()]
|
||||
return { "action": "attack", "card": attacker }
|
||||
|
||||
|
||||
func decide_block_action(attacker: CardInstance) -> Dictionary:
|
||||
var blockers := get_blockable_forwards()
|
||||
|
||||
if blockers.is_empty():
|
||||
return { "action": "skip" }
|
||||
|
||||
# 50% chance to skip blocking even when possible
|
||||
if randf() < 0.5:
|
||||
return { "action": "skip" }
|
||||
|
||||
# Pick a random blocker (might not be optimal)
|
||||
var blocker: CardInstance = blockers[randi() % blockers.size()]
|
||||
return { "action": "block", "card": blocker }
|
||||
271
scripts/game/ai/HardAI.gd
Normal file
271
scripts/game/ai/HardAI.gd
Normal file
@@ -0,0 +1,271 @@
|
||||
class_name HardAI
|
||||
extends AIStrategy
|
||||
|
||||
## Hard AI - Optimal rule-based decisions with full board analysis
|
||||
## Considers multiple factors and makes the best available play
|
||||
|
||||
|
||||
func _init(p_player_index: int) -> void:
|
||||
super._init(Difficulty.HARD, p_player_index)
|
||||
|
||||
|
||||
func decide_main_phase_action() -> Dictionary:
|
||||
var playable := get_playable_cards()
|
||||
|
||||
if playable.is_empty():
|
||||
return { "action": "pass" }
|
||||
|
||||
var board_eval := evaluate_board_state()
|
||||
var player := get_player()
|
||||
var opponent := get_opponent()
|
||||
|
||||
# Analyze the best play considering multiple factors
|
||||
var best_card: CardInstance = null
|
||||
var best_score := -999.0
|
||||
|
||||
for card in playable:
|
||||
var score := _evaluate_play(card, board_eval, player, opponent)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_card = card
|
||||
|
||||
# Only play if the score is positive
|
||||
if best_score > 0 and best_card:
|
||||
return { "action": "play", "card": best_card }
|
||||
|
||||
return { "action": "pass" }
|
||||
|
||||
|
||||
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
|
||||
var player := get_player()
|
||||
|
||||
# Prioritize dulling backups (they refresh next turn, no card loss)
|
||||
var backups_to_dull: Array[CardInstance] = []
|
||||
for backup in player.field_backups.get_cards():
|
||||
if backup.state == Enums.CardState.ACTIVE:
|
||||
backups_to_dull.append(backup)
|
||||
|
||||
if not backups_to_dull.is_empty():
|
||||
# Dull backups with least useful abilities first
|
||||
backups_to_dull.sort_custom(_compare_backup_utility)
|
||||
return { "action": "dull_backup", "card": backups_to_dull[0] }
|
||||
|
||||
# Discard cards - choose most expendable
|
||||
var hand_cards := player.hand.get_cards()
|
||||
if hand_cards.is_empty():
|
||||
return {}
|
||||
|
||||
# Evaluate each card for discard value
|
||||
var best_discard: CardInstance = null
|
||||
var lowest_value := 999.0
|
||||
|
||||
for card in hand_cards:
|
||||
var value := _evaluate_discard_value(card, player)
|
||||
if value < lowest_value:
|
||||
lowest_value = value
|
||||
best_discard = card
|
||||
|
||||
if best_discard:
|
||||
return { "action": "discard", "card": best_discard }
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
func decide_attack_action() -> Dictionary:
|
||||
var attackers := get_attackable_forwards()
|
||||
var opponent := get_opponent()
|
||||
|
||||
if attackers.is_empty():
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
var opponent_blockers := opponent.get_blockable_forwards()
|
||||
|
||||
# Calculate optimal attack order
|
||||
var attack_order := _calculate_attack_order(attackers, opponent_blockers, opponent)
|
||||
|
||||
if attack_order.is_empty():
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
# Return the best attack
|
||||
return { "action": "attack", "card": attack_order[0] }
|
||||
|
||||
|
||||
func decide_block_action(attacker: CardInstance) -> Dictionary:
|
||||
var blockers := get_blockable_forwards()
|
||||
var player := get_player()
|
||||
|
||||
if blockers.is_empty():
|
||||
return { "action": "skip" }
|
||||
|
||||
var attacker_power := attacker.get_power()
|
||||
var current_damage := player.get_damage_count()
|
||||
var would_be_lethal := current_damage >= 6
|
||||
|
||||
# Evaluate all blocking options
|
||||
var best_blocker: CardInstance = null
|
||||
var best_score := 0.0 # Baseline: skip blocking (score 0)
|
||||
|
||||
for blocker in blockers:
|
||||
var score := _evaluate_block(blocker, attacker, would_be_lethal)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_blocker = blocker
|
||||
|
||||
if best_blocker:
|
||||
return { "action": "block", "card": best_blocker }
|
||||
|
||||
return { "action": "skip" }
|
||||
|
||||
|
||||
func _evaluate_play(card: CardInstance, board_eval: float, player: Player, opponent: Player) -> float:
|
||||
var data := card.card_data
|
||||
var score := calculate_card_value(card)
|
||||
|
||||
match data.type:
|
||||
Enums.CardType.FORWARD:
|
||||
# Forwards more valuable when behind on board
|
||||
if board_eval < 0:
|
||||
score *= 1.5
|
||||
|
||||
# Extra value if opponent has no blockers
|
||||
if opponent.get_blockable_forwards().is_empty():
|
||||
score *= 1.3
|
||||
|
||||
# Consider if we already have 5 forwards (max)
|
||||
if player.field_forwards.get_card_count() >= 5:
|
||||
score *= 0.3
|
||||
|
||||
Enums.CardType.BACKUP:
|
||||
# Backups valuable for long game
|
||||
var backup_count := player.field_backups.get_card_count()
|
||||
if backup_count >= 5:
|
||||
score = -10.0 # Can't play more
|
||||
elif backup_count < 3:
|
||||
score *= 1.5 # Need more backups
|
||||
elif board_eval > 5:
|
||||
score *= 1.3 # Ahead, build infrastructure
|
||||
|
||||
Enums.CardType.SUMMON:
|
||||
# Summons are situational - evaluate based on current needs
|
||||
# This is simplified; real evaluation would check summon effects
|
||||
if board_eval < -3:
|
||||
score *= 1.4 # Need removal/utility when behind
|
||||
|
||||
Enums.CardType.MONSTER:
|
||||
# Similar to forwards but usually less efficient
|
||||
score *= 0.9
|
||||
|
||||
# Penalize expensive plays when low on cards
|
||||
if player.hand.get_card_count() <= 2 and data.cost >= 4:
|
||||
score *= 0.5
|
||||
|
||||
return score
|
||||
|
||||
|
||||
func _evaluate_discard_value(card: CardInstance, player: Player) -> float:
|
||||
var value := calculate_card_value(card)
|
||||
|
||||
# Duplicates in hand are less valuable
|
||||
var same_name_count := 0
|
||||
for hand_card in player.hand.get_cards():
|
||||
if hand_card.card_data.name == card.card_data.name:
|
||||
same_name_count += 1
|
||||
if same_name_count > 1:
|
||||
value *= 0.5
|
||||
|
||||
# High cost cards we can't afford soon are less valuable
|
||||
var potential_cp := _calculate_potential_cp()
|
||||
if card.card_data.cost > potential_cp:
|
||||
value *= 0.7
|
||||
|
||||
# Cards matching elements we don't have CP for are less valuable
|
||||
var has_element_match := false
|
||||
for element in card.card_data.elements:
|
||||
if player.cp_pool.get_cp(element) > 0:
|
||||
has_element_match = true
|
||||
break
|
||||
if not has_element_match and not card.card_data.elements.is_empty():
|
||||
value *= 0.8
|
||||
|
||||
return value
|
||||
|
||||
|
||||
func _calculate_attack_order(attackers: Array[CardInstance], blockers: Array[CardInstance], opponent: Player) -> Array[CardInstance]:
|
||||
var order: Array[CardInstance] = []
|
||||
var scores: Array[Dictionary] = []
|
||||
|
||||
for attacker in attackers:
|
||||
var score := _evaluate_attack_value(attacker, blockers, opponent)
|
||||
if score > 0:
|
||||
scores.append({ "card": attacker, "score": score })
|
||||
|
||||
# Sort by score descending
|
||||
scores.sort_custom(func(a, b): return a.score > b.score)
|
||||
|
||||
for entry in scores:
|
||||
order.append(entry.card)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
func _evaluate_attack_value(attacker: CardInstance, blockers: Array[CardInstance], opponent: Player) -> float:
|
||||
var score := 0.0
|
||||
var attacker_power := attacker.get_power()
|
||||
|
||||
# Base value for dealing damage
|
||||
score += 3.0
|
||||
|
||||
# Lethal damage is extremely valuable
|
||||
if opponent.get_damage_count() >= 6:
|
||||
score += 20.0
|
||||
|
||||
# Evaluate blocking scenarios
|
||||
var profitable_blocks := 0
|
||||
for blocker in blockers:
|
||||
var blocker_power := blocker.get_power()
|
||||
if blocker_power >= attacker_power:
|
||||
profitable_blocks += 1
|
||||
|
||||
if profitable_blocks == 0:
|
||||
# No profitable blocks - guaranteed damage
|
||||
score += 5.0
|
||||
else:
|
||||
# Risk of losing our forward
|
||||
score -= calculate_card_value(attacker) * 0.5
|
||||
|
||||
# Brave forwards can attack safely (don't dull)
|
||||
if attacker.card_data.has_ability("Brave"):
|
||||
score += 2.0
|
||||
|
||||
return score
|
||||
|
||||
|
||||
func _evaluate_block(blocker: CardInstance, attacker: CardInstance, would_be_lethal: bool) -> float:
|
||||
var blocker_power := blocker.get_power()
|
||||
var attacker_power := attacker.get_power()
|
||||
var score := 0.0
|
||||
|
||||
# If lethal, blocking is almost always correct
|
||||
if would_be_lethal:
|
||||
score += 15.0
|
||||
|
||||
# Do we kill the attacker?
|
||||
if blocker_power >= attacker_power:
|
||||
score += calculate_card_value(attacker)
|
||||
|
||||
# Do we lose our blocker?
|
||||
if attacker_power >= blocker_power:
|
||||
score -= calculate_card_value(blocker)
|
||||
|
||||
# First strike changes the calculation
|
||||
if blocker.card_data.has_ability("First Strike") and blocker_power >= attacker_power:
|
||||
# We kill them before they hit us
|
||||
score += calculate_card_value(blocker) * 0.5
|
||||
|
||||
return score
|
||||
|
||||
|
||||
func _compare_backup_utility(a: CardInstance, b: CardInstance) -> bool:
|
||||
# Lower utility = dull first
|
||||
# This is simplified; could check specific backup abilities
|
||||
return calculate_card_value(a) < calculate_card_value(b)
|
||||
161
scripts/game/ai/NormalAI.gd
Normal file
161
scripts/game/ai/NormalAI.gd
Normal file
@@ -0,0 +1,161 @@
|
||||
class_name NormalAI
|
||||
extends AIStrategy
|
||||
|
||||
## Normal AI - Balanced play using cost/power heuristics
|
||||
## Makes generally good decisions but doesn't deeply analyze
|
||||
|
||||
|
||||
func _init(p_player_index: int) -> void:
|
||||
super._init(Difficulty.NORMAL, p_player_index)
|
||||
|
||||
|
||||
func decide_main_phase_action() -> Dictionary:
|
||||
var playable := get_playable_cards()
|
||||
|
||||
if playable.is_empty():
|
||||
return { "action": "pass" }
|
||||
|
||||
# Sort by value (best cards first)
|
||||
playable.sort_custom(_compare_card_value)
|
||||
|
||||
# Consider board state - prioritize forwards if we're behind
|
||||
var board_eval := evaluate_board_state()
|
||||
|
||||
for card in playable:
|
||||
var card_type := card.card_data.type
|
||||
|
||||
# If behind on board, prioritize forwards
|
||||
if board_eval < -5.0 and card_type == Enums.CardType.FORWARD:
|
||||
return { "action": "play", "card": card }
|
||||
|
||||
# If ahead, might want backups for sustainability
|
||||
if board_eval > 5.0 and card_type == Enums.CardType.BACKUP:
|
||||
if get_player().field_backups.get_card_count() < 5:
|
||||
return { "action": "play", "card": card }
|
||||
|
||||
# Default: play the highest value card we can afford
|
||||
return { "action": "play", "card": playable[0] }
|
||||
|
||||
|
||||
func decide_cp_generation(needed_cp: Dictionary) -> Dictionary:
|
||||
var player := get_player()
|
||||
|
||||
# First, dull backups (they refresh next turn)
|
||||
for backup in player.field_backups.get_cards():
|
||||
if backup.state == Enums.CardState.ACTIVE:
|
||||
return { "action": "dull_backup", "card": backup }
|
||||
|
||||
# Then, discard lowest value card from hand
|
||||
var hand_cards := player.hand.get_cards()
|
||||
if hand_cards.is_empty():
|
||||
return {}
|
||||
|
||||
# Sort by value (lowest first for discard)
|
||||
var sorted_hand := hand_cards.duplicate()
|
||||
sorted_hand.sort_custom(_compare_card_value_reverse)
|
||||
|
||||
return { "action": "discard", "card": sorted_hand[0] }
|
||||
|
||||
|
||||
func decide_attack_action() -> Dictionary:
|
||||
var attackers := get_attackable_forwards()
|
||||
var opponent := get_opponent()
|
||||
|
||||
if attackers.is_empty():
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
# Get opponent's potential blockers
|
||||
var opponent_blockers := opponent.get_blockable_forwards()
|
||||
|
||||
# Evaluate each potential attacker
|
||||
var best_attacker: CardInstance = null
|
||||
var best_score := -999.0
|
||||
|
||||
for attacker in attackers:
|
||||
var score := _evaluate_attack(attacker, opponent_blockers, opponent)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_attacker = attacker
|
||||
|
||||
# Only attack if the score is positive (favorable)
|
||||
if best_score > 0 and best_attacker:
|
||||
return { "action": "attack", "card": best_attacker }
|
||||
|
||||
return { "action": "end_attacks" }
|
||||
|
||||
|
||||
func decide_block_action(attacker: CardInstance) -> Dictionary:
|
||||
var blockers := get_blockable_forwards()
|
||||
var player := get_player()
|
||||
|
||||
if blockers.is_empty():
|
||||
return { "action": "skip" }
|
||||
|
||||
var attacker_power := attacker.get_power()
|
||||
|
||||
# Check if this attack would be lethal
|
||||
var current_damage := player.get_damage_count()
|
||||
var would_be_lethal := current_damage >= 6 # 7th damage loses
|
||||
|
||||
# Find best blocker
|
||||
var best_blocker: CardInstance = null
|
||||
var best_score := -999.0
|
||||
|
||||
for blocker in blockers:
|
||||
var blocker_power := blocker.get_power()
|
||||
var score := 0.0
|
||||
|
||||
# Would we win the trade?
|
||||
if blocker_power >= attacker_power:
|
||||
score += 5.0 # We kill their forward
|
||||
if attacker_power >= blocker_power:
|
||||
score -= calculate_card_value(blocker) # We lose our blocker
|
||||
|
||||
# If lethal, blocking is very important
|
||||
if would_be_lethal:
|
||||
score += 10.0
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_blocker = blocker
|
||||
|
||||
# Block if favorable or if lethal
|
||||
if best_score > 0 or would_be_lethal:
|
||||
if best_blocker:
|
||||
return { "action": "block", "card": best_blocker }
|
||||
|
||||
return { "action": "skip" }
|
||||
|
||||
|
||||
func _evaluate_attack(attacker: CardInstance, opponent_blockers: Array[CardInstance], opponent: Player) -> float:
|
||||
var score := 0.0
|
||||
var attacker_power := attacker.get_power()
|
||||
|
||||
# Base value: dealing damage is good
|
||||
score += 2.0
|
||||
|
||||
# Check if opponent can block profitably
|
||||
var can_be_blocked := false
|
||||
for blocker in opponent_blockers:
|
||||
if blocker.get_power() >= attacker_power:
|
||||
can_be_blocked = true
|
||||
score -= 3.0 # Likely to lose our forward
|
||||
break
|
||||
|
||||
# If unblockable damage, more valuable
|
||||
if not can_be_blocked:
|
||||
score += 3.0
|
||||
|
||||
# If this would be lethal damage (7th), very valuable
|
||||
if opponent.get_damage_count() >= 6:
|
||||
score += 10.0
|
||||
|
||||
return score
|
||||
|
||||
|
||||
func _compare_card_value(a: CardInstance, b: CardInstance) -> bool:
|
||||
return calculate_card_value(a) > calculate_card_value(b)
|
||||
|
||||
|
||||
func _compare_card_value_reverse(a: CardInstance, b: CardInstance) -> bool:
|
||||
return calculate_card_value(a) < calculate_card_value(b)
|
||||
Reference in New Issue
Block a user