feature updates
This commit is contained in:
798
scripts/game/abilities/AbilitySystem.gd
Normal file
798
scripts/game/abilities/AbilitySystem.gd
Normal file
@@ -0,0 +1,798 @@
|
||||
class_name AbilitySystem
|
||||
extends Node
|
||||
|
||||
## AbilitySystem - Central coordinator for ability processing
|
||||
## Loads processed abilities and handles trigger matching and effect resolution
|
||||
|
||||
signal ability_triggered(source: CardInstance, ability: Dictionary)
|
||||
signal effect_resolved(effect: Dictionary, targets: Array)
|
||||
signal targeting_required(effect: Dictionary, valid_targets: Array, callback: Callable)
|
||||
signal targeting_completed(effect: Dictionary, selected_targets: Array)
|
||||
signal choice_modal_required(effect: Dictionary, modes: Array, callback: Callable)
|
||||
signal optional_effect_prompt(player_index: int, effect: Dictionary, description: String, callback: Callable)
|
||||
|
||||
const ABILITIES_PATH = "res://data/abilities_processed.json"
|
||||
|
||||
# Loaded ability data
|
||||
var _abilities: Dictionary = {} # card_id -> Array of parsed abilities
|
||||
var _version: String = ""
|
||||
var _stats: Dictionary = {}
|
||||
|
||||
# Sub-systems
|
||||
var trigger_matcher: TriggerMatcher
|
||||
var effect_resolver: EffectResolver
|
||||
var target_selector: TargetSelector
|
||||
var field_effect_manager: FieldEffectManager
|
||||
var condition_checker: ConditionChecker
|
||||
|
||||
# UI Reference
|
||||
var choice_modal: ChoiceModal = null
|
||||
|
||||
# Effect resolution stack
|
||||
var _pending_effects: Array = []
|
||||
var _is_resolving: bool = false
|
||||
var _waiting_for_choice: bool = false
|
||||
var _waiting_for_optional: bool = false
|
||||
|
||||
# Connected game state
|
||||
var _game_state = null # GameState reference
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_load_abilities()
|
||||
_init_subsystems()
|
||||
|
||||
|
||||
func _init_subsystems() -> void:
|
||||
trigger_matcher = TriggerMatcher.new()
|
||||
effect_resolver = EffectResolver.new()
|
||||
target_selector = TargetSelector.new()
|
||||
field_effect_manager = FieldEffectManager.new()
|
||||
condition_checker = ConditionChecker.new()
|
||||
|
||||
# Wire ConditionChecker to subsystems that need it
|
||||
effect_resolver.condition_checker = condition_checker
|
||||
trigger_matcher.condition_checker = condition_checker
|
||||
|
||||
# Connect effect resolver signals
|
||||
effect_resolver.effect_completed.connect(_on_effect_completed)
|
||||
effect_resolver.choice_required.connect(_on_choice_required)
|
||||
|
||||
|
||||
func _load_abilities() -> void:
|
||||
if not FileAccess.file_exists(ABILITIES_PATH):
|
||||
push_warning("AbilitySystem: No processed abilities found at " + ABILITIES_PATH)
|
||||
push_warning("Run: python tools/ability_processor.py")
|
||||
return
|
||||
|
||||
var file = FileAccess.open(ABILITIES_PATH, FileAccess.READ)
|
||||
if not file:
|
||||
push_error("AbilitySystem: Failed to open " + ABILITIES_PATH)
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(file.get_as_text())
|
||||
file.close()
|
||||
|
||||
if error != OK:
|
||||
push_error("AbilitySystem: Failed to parse abilities JSON: " + json.get_error_message())
|
||||
return
|
||||
|
||||
var data = json.get_data()
|
||||
_version = data.get("version", "unknown")
|
||||
_stats = data.get("statistics", {})
|
||||
_abilities = data.get("abilities", {})
|
||||
|
||||
print("AbilitySystem: Loaded v%s - %d cards, %d abilities (%d high confidence)" % [
|
||||
_version,
|
||||
_stats.get("total_cards", 0),
|
||||
_stats.get("total_abilities", 0),
|
||||
_stats.get("parsed_high", 0)
|
||||
])
|
||||
|
||||
|
||||
## Connect to a game state to listen for events
|
||||
func connect_to_game(game_state) -> void:
|
||||
if _game_state:
|
||||
_disconnect_from_game()
|
||||
|
||||
_game_state = game_state
|
||||
|
||||
# Connect to game events that can trigger abilities
|
||||
game_state.card_played.connect(_on_card_played)
|
||||
game_state.summon_cast.connect(_on_summon_cast)
|
||||
game_state.attack_declared.connect(_on_attack_declared)
|
||||
game_state.block_declared.connect(_on_block_declared)
|
||||
game_state.forward_broken.connect(_on_forward_broken)
|
||||
game_state.damage_dealt.connect(_on_damage_dealt)
|
||||
game_state.card_moved.connect(_on_card_moved)
|
||||
game_state.combat_resolved.connect(_on_combat_resolved)
|
||||
|
||||
# Turn manager signals
|
||||
if game_state.turn_manager:
|
||||
game_state.turn_manager.phase_changed.connect(_on_phase_changed)
|
||||
game_state.turn_manager.turn_started.connect(_on_turn_started)
|
||||
game_state.turn_manager.turn_ended.connect(_on_turn_ended)
|
||||
|
||||
print("AbilitySystem: Connected to GameState")
|
||||
|
||||
|
||||
func _disconnect_from_game() -> void:
|
||||
if not _game_state:
|
||||
return
|
||||
|
||||
# Disconnect all signals
|
||||
if _game_state.card_played.is_connected(_on_card_played):
|
||||
_game_state.card_played.disconnect(_on_card_played)
|
||||
if _game_state.summon_cast.is_connected(_on_summon_cast):
|
||||
_game_state.summon_cast.disconnect(_on_summon_cast)
|
||||
if _game_state.attack_declared.is_connected(_on_attack_declared):
|
||||
_game_state.attack_declared.disconnect(_on_attack_declared)
|
||||
if _game_state.block_declared.is_connected(_on_block_declared):
|
||||
_game_state.block_declared.disconnect(_on_block_declared)
|
||||
if _game_state.forward_broken.is_connected(_on_forward_broken):
|
||||
_game_state.forward_broken.disconnect(_on_forward_broken)
|
||||
if _game_state.damage_dealt.is_connected(_on_damage_dealt):
|
||||
_game_state.damage_dealt.disconnect(_on_damage_dealt)
|
||||
if _game_state.card_moved.is_connected(_on_card_moved):
|
||||
_game_state.card_moved.disconnect(_on_card_moved)
|
||||
|
||||
_game_state = null
|
||||
|
||||
|
||||
## Get parsed abilities for a card
|
||||
func get_abilities(card_id: String) -> Array:
|
||||
return _abilities.get(card_id, [])
|
||||
|
||||
|
||||
## Check if a card has parsed abilities
|
||||
func has_abilities(card_id: String) -> bool:
|
||||
return _abilities.has(card_id) and _abilities[card_id].size() > 0
|
||||
|
||||
|
||||
## Get a specific parsed ability
|
||||
func get_ability(card_id: String, ability_index: int) -> Dictionary:
|
||||
var abilities = get_abilities(card_id)
|
||||
if ability_index >= 0 and ability_index < abilities.size():
|
||||
return abilities[ability_index]
|
||||
return {}
|
||||
|
||||
|
||||
## Process a game event and trigger matching abilities
|
||||
func process_event(event_type: String, event_data: Dictionary) -> void:
|
||||
if not _game_state:
|
||||
return
|
||||
|
||||
var triggered = trigger_matcher.find_triggered_abilities(
|
||||
event_type, event_data, _game_state, _abilities
|
||||
)
|
||||
|
||||
for trigger_info in triggered:
|
||||
_queue_ability(trigger_info)
|
||||
|
||||
|
||||
## Queue an ability for resolution
|
||||
func _queue_ability(trigger_info: Dictionary) -> void:
|
||||
var source = trigger_info.source as CardInstance
|
||||
var ability = trigger_info.ability as Dictionary
|
||||
var parsed = ability.get("parsed", {})
|
||||
|
||||
if not parsed or not parsed.has("effects"):
|
||||
return
|
||||
|
||||
# Check if ability has a cost that needs to be paid
|
||||
var cost = parsed.get("cost", {})
|
||||
if not cost.is_empty() and source and _game_state:
|
||||
var player = _game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
# Validate cost
|
||||
var validation = _validate_ability_cost(cost, source, player)
|
||||
if not validation.valid:
|
||||
# Cannot pay cost - emit signal and skip ability
|
||||
ability_cost_failed.emit(source, ability, validation.reason)
|
||||
push_warning("AbilitySystem: Cannot pay cost for ability - %s" % validation.reason)
|
||||
return
|
||||
|
||||
# Pay the cost
|
||||
_pay_ability_cost(cost, source, player)
|
||||
|
||||
ability_triggered.emit(source, ability)
|
||||
|
||||
# Add effects to pending stack (LIFO for proper resolution order)
|
||||
var effects = parsed.get("effects", [])
|
||||
for i in range(effects.size() - 1, -1, -1):
|
||||
_pending_effects.push_front({
|
||||
"effect": effects[i],
|
||||
"source": source,
|
||||
"controller": source.controller_index,
|
||||
"ability": ability,
|
||||
"event_data": trigger_info.get("event_data", {})
|
||||
})
|
||||
|
||||
# Start resolving if not already
|
||||
if not _is_resolving:
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Resolve the next pending effect
|
||||
func _resolve_next_effect() -> void:
|
||||
if _pending_effects.is_empty():
|
||||
_is_resolving = false
|
||||
return
|
||||
|
||||
_is_resolving = true
|
||||
var pending = _pending_effects[0]
|
||||
var effect = pending.effect
|
||||
var source = pending.source
|
||||
|
||||
# Check if effect is optional and we haven't prompted yet
|
||||
if effect.get("optional", false) and not pending.get("optional_prompted", false):
|
||||
_waiting_for_optional = true
|
||||
pending["optional_prompted"] = true # Mark as prompted to avoid re-prompting
|
||||
|
||||
# Determine which player should decide
|
||||
var player_index = source.controller_index if source else 0
|
||||
|
||||
# Build description from effect
|
||||
var description = _build_effect_description(effect)
|
||||
|
||||
# Emit signal for UI to handle
|
||||
optional_effect_prompt.emit(player_index, effect, description, _on_optional_effect_choice)
|
||||
return # Wait for callback
|
||||
|
||||
# Check if effect needs targeting
|
||||
if _effect_needs_targeting(effect):
|
||||
var valid_targets = target_selector.get_valid_targets(
|
||||
effect.get("target", {}), source, _game_state
|
||||
)
|
||||
|
||||
if valid_targets.is_empty():
|
||||
# No valid targets, skip effect
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
return
|
||||
|
||||
# Request target selection from player
|
||||
targeting_required.emit(effect, valid_targets, _on_targets_selected)
|
||||
# Wait for targeting_completed signal
|
||||
else:
|
||||
# Resolve immediately
|
||||
_execute_effect(pending)
|
||||
|
||||
|
||||
## Execute an effect with its targets
|
||||
func _execute_effect(pending: Dictionary) -> void:
|
||||
var effect = pending.effect
|
||||
var source = pending.source
|
||||
var targets = pending.get("targets", [])
|
||||
|
||||
effect_resolver.resolve(effect, source, targets, _game_state)
|
||||
|
||||
|
||||
## Called when effect resolution completes
|
||||
func _on_effect_completed(effect: Dictionary, targets: Array) -> void:
|
||||
effect_resolved.emit(effect, targets)
|
||||
|
||||
if not _pending_effects.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Called when player selects targets
|
||||
func _on_targets_selected(targets: Array) -> void:
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
_pending_effects[0]["targets"] = targets
|
||||
targeting_completed.emit(_pending_effects[0].effect, targets)
|
||||
_execute_effect(_pending_effects[0])
|
||||
|
||||
|
||||
## Check if effect requires player targeting
|
||||
func _effect_needs_targeting(effect: Dictionary) -> bool:
|
||||
if not effect.has("target"):
|
||||
return false
|
||||
var target = effect.target
|
||||
return target.get("type") == "CHOOSE"
|
||||
|
||||
|
||||
## Called when player responds to optional effect prompt
|
||||
func _on_optional_effect_choice(accepted: bool) -> void:
|
||||
_waiting_for_optional = false
|
||||
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
if accepted:
|
||||
# Player chose to execute the optional effect
|
||||
# Continue with normal resolution (targeting or execution)
|
||||
var pending = _pending_effects[0]
|
||||
var effect = pending.effect
|
||||
|
||||
if _effect_needs_targeting(effect):
|
||||
var source = pending.source
|
||||
var valid_targets = target_selector.get_valid_targets(
|
||||
effect.get("target", {}), source, _game_state
|
||||
)
|
||||
|
||||
if valid_targets.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
return
|
||||
|
||||
targeting_required.emit(effect, valid_targets, _on_targets_selected)
|
||||
else:
|
||||
_execute_effect(pending)
|
||||
else:
|
||||
# Player declined the optional effect - skip it
|
||||
_pending_effects.pop_front()
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
## Build a human-readable description of an effect for prompts
|
||||
func _build_effect_description(effect: Dictionary) -> String:
|
||||
var effect_type = str(effect.get("type", "")).to_upper()
|
||||
var amount = effect.get("amount", 0)
|
||||
|
||||
match effect_type:
|
||||
"DRAW":
|
||||
var count = effect.get("amount", 1)
|
||||
return "Draw %d card%s" % [count, "s" if count > 1 else ""]
|
||||
"DAMAGE":
|
||||
return "Deal %d damage" % amount
|
||||
"POWER_MOD":
|
||||
var sign = "+" if amount >= 0 else ""
|
||||
return "Give %s%d power" % [sign, amount]
|
||||
"DULL":
|
||||
return "Dull a Forward"
|
||||
"ACTIVATE":
|
||||
return "Activate a card"
|
||||
"BREAK":
|
||||
return "Break a card"
|
||||
"RETURN":
|
||||
return "Return a card to hand"
|
||||
"SEARCH":
|
||||
return "Search your deck"
|
||||
"DISCARD":
|
||||
var count = effect.get("amount", 1)
|
||||
return "Discard %d card%s" % [count, "s" if count > 1 else ""]
|
||||
_:
|
||||
# Use the original_text if available
|
||||
if effect.has("original_text"):
|
||||
return effect.original_text
|
||||
return "Use this effect"
|
||||
|
||||
|
||||
## Called when EffectResolver encounters a CHOOSE_MODE effect
|
||||
func _on_choice_required(effect: Dictionary, modes: Array) -> void:
|
||||
if _pending_effects.is_empty():
|
||||
return
|
||||
|
||||
var pending = _pending_effects[0]
|
||||
var source = pending.get("source") as CardInstance
|
||||
|
||||
# Check for enhanced condition (e.g., "If you have 5+ Ifrit, select 3 instead")
|
||||
var select_count = effect.get("select_count", 1)
|
||||
var select_up_to = effect.get("select_up_to", false)
|
||||
|
||||
var enhanced = effect.get("enhanced_condition", {})
|
||||
if not enhanced.is_empty() and _check_enhanced_condition(enhanced, source):
|
||||
select_count = enhanced.get("select_count", select_count)
|
||||
select_up_to = enhanced.get("select_up_to", select_up_to)
|
||||
|
||||
# If we have a ChoiceModal, use it
|
||||
if choice_modal:
|
||||
_waiting_for_choice = true
|
||||
_handle_modal_choice_async(effect, modes, select_count, select_up_to, source)
|
||||
else:
|
||||
# No UI available - auto-select first N modes
|
||||
push_warning("AbilitySystem: No ChoiceModal available, auto-selecting first mode(s)")
|
||||
var auto_selected: Array = []
|
||||
for i in range(min(select_count, modes.size())):
|
||||
auto_selected.append(i)
|
||||
_on_modes_selected(effect, modes, auto_selected, source)
|
||||
|
||||
|
||||
## Handle modal choice asynchronously
|
||||
func _handle_modal_choice_async(
|
||||
effect: Dictionary,
|
||||
modes: Array,
|
||||
select_count: int,
|
||||
select_up_to: bool,
|
||||
source: CardInstance
|
||||
) -> void:
|
||||
var selected = await choice_modal.show_choices(
|
||||
"", # Title is generated by ChoiceModal
|
||||
modes,
|
||||
select_count,
|
||||
select_up_to,
|
||||
false # Not cancellable for mandatory abilities
|
||||
)
|
||||
|
||||
_waiting_for_choice = false
|
||||
_on_modes_selected(effect, modes, selected, source)
|
||||
|
||||
|
||||
## Cached regex for enhanced condition parsing
|
||||
var _enhanced_count_regex: RegEx = null
|
||||
|
||||
|
||||
## Check if enhanced condition is met
|
||||
func _check_enhanced_condition(condition: Dictionary, source: CardInstance) -> bool:
|
||||
var description = condition.get("description", "").to_lower()
|
||||
|
||||
# Parse "if you have X or more [Card Name] in your Break Zone"
|
||||
if "break zone" in description:
|
||||
# Initialize regex once (lazy)
|
||||
if _enhanced_count_regex == null:
|
||||
_enhanced_count_regex = RegEx.new()
|
||||
_enhanced_count_regex.compile("(\\d+) or more")
|
||||
|
||||
var match_result = _enhanced_count_regex.search(description)
|
||||
if match_result:
|
||||
var required_count = int(match_result.get_string(1))
|
||||
|
||||
# Check break zone for matching cards
|
||||
if _game_state and source:
|
||||
var player = _game_state.get_player(source.controller_index)
|
||||
if player:
|
||||
# Count matching cards in break zone
|
||||
var break_zone_count = 0
|
||||
for card in player.break_zone.get_cards():
|
||||
# Simple name matching (description contains card name pattern)
|
||||
if card.card_data and card.card_data.name.to_lower() in description:
|
||||
break_zone_count += 1
|
||||
|
||||
return break_zone_count >= required_count
|
||||
|
||||
return false
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COST VALIDATION AND PAYMENT
|
||||
# =============================================================================
|
||||
|
||||
## Signal emitted when cost cannot be paid
|
||||
signal ability_cost_failed(source: CardInstance, ability: Dictionary, reason: String)
|
||||
|
||||
|
||||
## Validate that a player can pay the cost for an ability
|
||||
## Returns true if cost can be paid, false otherwise
|
||||
func _validate_ability_cost(
|
||||
cost: Dictionary,
|
||||
source: CardInstance,
|
||||
player
|
||||
) -> Dictionary:
|
||||
var result = {"valid": true, "reason": ""}
|
||||
|
||||
if cost.is_empty():
|
||||
return result
|
||||
|
||||
# Check CP cost
|
||||
var cp_cost = cost.get("cp", 0)
|
||||
var element = cost.get("element", "")
|
||||
|
||||
if cp_cost > 0:
|
||||
if element and element != "" and element.to_upper() != "ANY":
|
||||
# Specific element required
|
||||
var element_enum = Enums.element_from_string(element)
|
||||
if player.cp_pool.get_cp(element_enum) < cp_cost:
|
||||
result.valid = false
|
||||
result.reason = "Not enough %s CP (need %d, have %d)" % [
|
||||
element, cp_cost, player.cp_pool.get_cp(element_enum)
|
||||
]
|
||||
return result
|
||||
else:
|
||||
# Any element
|
||||
if player.cp_pool.get_total_cp() < cp_cost:
|
||||
result.valid = false
|
||||
result.reason = "Not enough CP (need %d, have %d)" % [
|
||||
cp_cost, player.cp_pool.get_total_cp()
|
||||
]
|
||||
return result
|
||||
|
||||
# Check discard cost
|
||||
var discard = cost.get("discard", 0)
|
||||
if discard > 0:
|
||||
if player.hand.get_count() < discard:
|
||||
result.valid = false
|
||||
result.reason = "Not enough cards in hand to discard (need %d, have %d)" % [
|
||||
discard, player.hand.get_count()
|
||||
]
|
||||
return result
|
||||
|
||||
# Check dull self cost
|
||||
var dull_self = cost.get("dull_self", false)
|
||||
if dull_self and source:
|
||||
if source.is_dull():
|
||||
result.valid = false
|
||||
result.reason = "Card is already dulled"
|
||||
return result
|
||||
|
||||
# Check specific card discard
|
||||
var specific_discard = cost.get("specific_discard", "")
|
||||
if specific_discard != "":
|
||||
# Player must have a card with this name in hand
|
||||
var has_card = false
|
||||
for card in player.hand.get_cards():
|
||||
if card.card_data and card.card_data.name.to_lower() == specific_discard.to_lower():
|
||||
has_card = true
|
||||
break
|
||||
if not has_card:
|
||||
result.valid = false
|
||||
result.reason = "Must discard a card named '%s'" % specific_discard
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Pay the cost for an ability
|
||||
## Returns true if cost was paid successfully
|
||||
func _pay_ability_cost(
|
||||
cost: Dictionary,
|
||||
source: CardInstance,
|
||||
player
|
||||
) -> bool:
|
||||
if cost.is_empty():
|
||||
return true
|
||||
|
||||
# Pay CP cost
|
||||
var cp_cost = cost.get("cp", 0)
|
||||
var element = cost.get("element", "")
|
||||
|
||||
if cp_cost > 0:
|
||||
if element and element != "" and element.to_upper() != "ANY":
|
||||
# Spend specific element CP
|
||||
var element_enum = Enums.element_from_string(element)
|
||||
player.cp_pool.add_cp(element_enum, -cp_cost)
|
||||
else:
|
||||
# Spend from any element (generic)
|
||||
var remaining = cp_cost
|
||||
for elem in Enums.Element.values():
|
||||
var available = player.cp_pool.get_cp(elem)
|
||||
if available > 0:
|
||||
var to_spend = mini(available, remaining)
|
||||
player.cp_pool.add_cp(elem, -to_spend)
|
||||
remaining -= to_spend
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
# Pay dull self cost
|
||||
var dull_self = cost.get("dull_self", false)
|
||||
if dull_self and source:
|
||||
source.dull()
|
||||
|
||||
# Note: Discard costs are handled through separate UI interaction
|
||||
# The discard selection would be queued as a separate effect
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Called when player selects mode(s)
|
||||
func _on_modes_selected(
|
||||
effect: Dictionary,
|
||||
modes: Array,
|
||||
selected_indices: Array,
|
||||
source: CardInstance
|
||||
) -> void:
|
||||
# Queue the effects from selected modes
|
||||
for index in selected_indices:
|
||||
if index >= 0 and index < modes.size():
|
||||
var mode = modes[index]
|
||||
var mode_effects = mode.get("effects", [])
|
||||
|
||||
for mode_effect in mode_effects:
|
||||
_pending_effects.push_back({
|
||||
"effect": mode_effect,
|
||||
"source": source,
|
||||
"controller": source.controller_index if source else 0,
|
||||
"ability": effect,
|
||||
"event_data": {}
|
||||
})
|
||||
|
||||
# Remove the CHOOSE_MODE effect from pending and continue
|
||||
if not _pending_effects.is_empty():
|
||||
_pending_effects.pop_front()
|
||||
|
||||
_resolve_next_effect()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Event Handlers
|
||||
# =============================================================================
|
||||
|
||||
func _on_card_played(card: CardInstance, player_index: int) -> void:
|
||||
# Register field abilities
|
||||
if card.is_forward() or card.is_backup():
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
field_effect_manager.register_field_abilities(card, card_abilities)
|
||||
|
||||
# Trigger enters field events
|
||||
process_event("ENTERS_FIELD", {
|
||||
"card": card,
|
||||
"player": player_index,
|
||||
"zone_from": Enums.ZoneType.HAND,
|
||||
"zone_to": Enums.ZoneType.FIELD_FORWARDS if card.is_forward() else Enums.ZoneType.FIELD_BACKUPS
|
||||
})
|
||||
|
||||
|
||||
func _on_summon_cast(card: CardInstance, player_index: int) -> void:
|
||||
process_event("SUMMON_CAST", {
|
||||
"card": card,
|
||||
"player": player_index
|
||||
})
|
||||
|
||||
|
||||
func _on_attack_declared(attacker: CardInstance) -> void:
|
||||
process_event("ATTACKS", {
|
||||
"card": attacker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_block_declared(blocker: CardInstance) -> void:
|
||||
if not _game_state or not _game_state.turn_manager:
|
||||
return
|
||||
|
||||
var attacker = _game_state.turn_manager.current_attacker
|
||||
|
||||
process_event("BLOCKS", {
|
||||
"card": blocker,
|
||||
"attacker": attacker,
|
||||
"player": blocker.controller_index
|
||||
})
|
||||
|
||||
if attacker:
|
||||
process_event("IS_BLOCKED", {
|
||||
"card": attacker,
|
||||
"blocker": blocker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_forward_broken(card: CardInstance) -> void:
|
||||
# Unregister field abilities
|
||||
field_effect_manager.unregister_field_abilities(card)
|
||||
|
||||
process_event("LEAVES_FIELD", {
|
||||
"card": card,
|
||||
"zone_from": Enums.ZoneType.FIELD_FORWARDS,
|
||||
"zone_to": Enums.ZoneType.BREAK
|
||||
})
|
||||
|
||||
|
||||
func _on_damage_dealt(player_index: int, amount: int, cards: Array) -> void:
|
||||
process_event("DAMAGE_DEALT_TO_PLAYER", {
|
||||
"player": player_index,
|
||||
"amount": amount,
|
||||
"cards": cards
|
||||
})
|
||||
|
||||
# Check for EX BURST triggers on damage cards
|
||||
for card in cards:
|
||||
if card.card_data and card.card_data.has_ex_burst:
|
||||
_trigger_ex_burst(card, player_index)
|
||||
|
||||
|
||||
func _on_card_moved(card: CardInstance, from_zone: Enums.ZoneType, to_zone: Enums.ZoneType) -> void:
|
||||
# Handle zone changes
|
||||
if to_zone == Enums.ZoneType.BREAK:
|
||||
field_effect_manager.unregister_field_abilities(card)
|
||||
|
||||
|
||||
func _on_combat_resolved(attacker: CardInstance, blocker: CardInstance) -> void:
|
||||
if not blocker:
|
||||
# Unblocked attack
|
||||
process_event("DEALS_DAMAGE_TO_OPPONENT", {
|
||||
"card": attacker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
else:
|
||||
# Blocked combat
|
||||
process_event("DEALS_DAMAGE", {
|
||||
"card": attacker,
|
||||
"target": blocker,
|
||||
"player": attacker.controller_index
|
||||
})
|
||||
process_event("DEALS_DAMAGE", {
|
||||
"card": blocker,
|
||||
"target": attacker,
|
||||
"player": blocker.controller_index
|
||||
})
|
||||
|
||||
|
||||
func _on_phase_changed(phase: Enums.TurnPhase) -> void:
|
||||
var event_type = ""
|
||||
match phase:
|
||||
Enums.TurnPhase.ACTIVE:
|
||||
event_type = "START_OF_ACTIVE_PHASE"
|
||||
Enums.TurnPhase.DRAW:
|
||||
event_type = "START_OF_DRAW_PHASE"
|
||||
Enums.TurnPhase.MAIN_1:
|
||||
event_type = "START_OF_MAIN_PHASE"
|
||||
Enums.TurnPhase.ATTACK:
|
||||
event_type = "START_OF_ATTACK_PHASE"
|
||||
Enums.TurnPhase.MAIN_2:
|
||||
event_type = "START_OF_MAIN_PHASE_2"
|
||||
Enums.TurnPhase.END:
|
||||
event_type = "START_OF_END_PHASE"
|
||||
|
||||
if event_type:
|
||||
process_event(event_type, {
|
||||
"player": _game_state.turn_manager.current_player_index if _game_state else 0
|
||||
})
|
||||
|
||||
|
||||
func _on_turn_started(player_index: int, turn_number: int) -> void:
|
||||
process_event("START_OF_TURN", {
|
||||
"player": player_index,
|
||||
"turn_number": turn_number
|
||||
})
|
||||
|
||||
|
||||
func _on_turn_ended(player_index: int) -> void:
|
||||
process_event("END_OF_TURN", {
|
||||
"player": player_index
|
||||
})
|
||||
|
||||
|
||||
## Trigger EX BURST for a damage card
|
||||
func _trigger_ex_burst(card: CardInstance, damaged_player: int) -> void:
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
|
||||
for ability in card_abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("is_ex_burst", false):
|
||||
# Queue the EX BURST ability
|
||||
_queue_ability({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": {
|
||||
"player": damaged_player,
|
||||
"trigger_type": "EX_BURST"
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
## Get power modifier from field effects for a card
|
||||
func get_field_power_modifier(card: CardInstance) -> int:
|
||||
return field_effect_manager.get_power_modifiers(card, _game_state)
|
||||
|
||||
|
||||
## Check if a card has a field-granted keyword
|
||||
func has_field_keyword(card: CardInstance, keyword: String) -> bool:
|
||||
return field_effect_manager.has_keyword(card, keyword, _game_state)
|
||||
|
||||
|
||||
## Check if a card has field-granted protection
|
||||
func has_field_protection(card: CardInstance, protection_type: String) -> bool:
|
||||
return field_effect_manager.has_protection(card, protection_type, _game_state)
|
||||
|
||||
|
||||
## Get all granted keywords for a card from field effects
|
||||
func get_field_keywords(card: CardInstance) -> Array:
|
||||
return field_effect_manager.get_granted_keywords(card, _game_state)
|
||||
|
||||
|
||||
## Trigger EX BURST on a specific card (called by EffectResolver)
|
||||
func trigger_ex_burst_on_card(card: CardInstance) -> void:
|
||||
if not card or not card.card_data:
|
||||
return
|
||||
|
||||
var card_abilities = get_abilities(card.card_data.id)
|
||||
|
||||
for ability in card_abilities:
|
||||
var parsed = ability.get("parsed", {})
|
||||
if parsed.get("is_ex_burst", false):
|
||||
_queue_ability({
|
||||
"source": card,
|
||||
"ability": ability,
|
||||
"event_data": {
|
||||
"trigger_type": "EX_BURST_TRIGGERED"
|
||||
}
|
||||
})
|
||||
break
|
||||
Reference in New Issue
Block a user