feature updates

This commit is contained in:
2026-02-02 16:28:53 -05:00
parent bf9aa3fa23
commit 44c06530ac
83 changed files with 282641 additions and 11251 deletions

View File

@@ -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 = ""

View File

@@ -41,6 +41,7 @@ enum TurnPhase {
## Attack Phase Steps
enum AttackStep {
NONE, # Not in attack phase or between attacks
PREPARATION,
DECLARATION,
BLOCK_DECLARATION,

View File

@@ -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()

View 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

View 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

View 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 []

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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()

View 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
View 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
View 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
View 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)