799 lines
23 KiB
GDScript
799 lines
23 KiB
GDScript
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
|