class_name ActionExecutor extends Node const StatResolverScript := preload("res://scripts/resolvers/stat_resolver.gd") signal action_executed(action: Resource, judgement: StringName) signal action_failed(action: Resource, reason: StringName) @export var energy_component_path: NodePath @export var damage_emitter_path: NodePath @onready var _energy_component: Node = get_node_or_null(energy_component_path) @onready var _damage_emitter: Node = get_node_or_null(damage_emitter_path) var last_failure_reason: StringName = &"" func execute(action: Resource, judgement: StringName, _stat_provider: Variant = null) -> bool: last_failure_reason = &"" if action == null: _fail(action, &"missing_action") return false if _action_requires_damage_emitter(action) and _damage_emitter == null: _fail(action, &"missing_damage_emitter") return false if _action_requires_damage_emitter(action) and _damage_emitter != null and _damage_emitter.has_method("configure_hit"): _damage_emitter.configure_hit(action, {"label": str(judgement)}) elif _damage_emitter != null and _damage_emitter.has_method("clear_hit"): _damage_emitter.clear_hit() if _action_spawns_projectile(action): _request_projectile(action, judgement) var reward := int(round(_resolve_reward(action, {"label": str(judgement)}, _stat_provider))) if reward != 0 and _energy_component != null: _energy_component.change(reward) action_executed.emit(action, judgement) return true func finish_action() -> void: if _damage_emitter != null and _damage_emitter.has_method("clear_hit"): _damage_emitter.clear_hit() elif _damage_emitter != null: _damage_emitter.monitoring = false func _fail(action: Resource, reason: StringName) -> void: last_failure_reason = reason action_failed.emit(action, reason) func _action_requires_damage_emitter(action: Resource) -> bool: if action == null: return false return StringName(str(action.get("hit_type"))) == &"melee" func _action_spawns_projectile(action: Resource) -> bool: return action != null and StringName(str(action.get("hit_type"))) == &"projectile" func _resolve_cost(action: Resource, stat_provider: Variant) -> float: var combat := _combat_manager_or_null() if combat != null and combat.has_method("resolve_cost"): return float(combat.call("resolve_cost", action, {}, stat_provider)) return float(action.get("base_cost")) func _resolve_reward(action: Resource, judgement: Dictionary, stat_provider: Variant) -> float: var combat := _combat_manager_or_null() if combat != null and combat.has_method("resolve_reward"): return float(combat.call("resolve_reward", action, judgement, stat_provider)) return StatResolverScript.resolve_reward(action, judgement, stat_provider) func _request_projectile(action: Resource, judgement: StringName = &"perfect") -> void: var owner := get_parent() var context := { "team": _projectile_team(action), "action": action, "judgement": {"label": str(judgement)}, "range": float(action.get("range")) if action != null else 0.0, "attacker_interrupts": _owner_interrupt_authority(owner), "source_actor": owner, } # Projectile base damage belongs to the shooter (player/minion/boss), not # the projectile scene default. if _damage_emitter != null: context["base_damage"] = int(_damage_emitter.get("damage")) if owner != null and owner.has_method("projectile_requests_for_action"): var requests = owner.call("projectile_requests_for_action", action) if requests is Array and not requests.is_empty(): for request: Variant in requests: if request is Dictionary: _emit_projectile_request( request.get("projectile_scene", null) as PackedScene, request.get("spawn_position", Vector2.ZERO) as Vector2, request.get("direction", Vector2.RIGHT) as Vector2, context ) return var spawn_position := Vector2.ZERO var direction := Vector2.RIGHT if owner is Node2D: spawn_position = (owner as Node2D).global_position var heading = owner.get("heading") if heading is Vector2 and heading != Vector2.ZERO: direction = heading if owner != null and owner.has_method("projectile_spawn_position"): spawn_position = owner.call("projectile_spawn_position", action) if owner != null and owner.has_method("projectile_direction"): direction = owner.call("projectile_direction", action) _emit_projectile_request(null, spawn_position, direction, context) func _owner_interrupt_authority(owner: Node) -> bool: if owner != null and owner.has_method("is_strong_in_current_time_phase"): return bool(owner.call("is_strong_in_current_time_phase")) return true func _projectile_team(action: Resource) -> StringName: if action != null and action.get("action_tags") is Array: for tag: Variant in action.get("action_tags"): if StringName(str(tag)) == &"enemy": return &"enemy" return &"player" func _emit_projectile_request(projectile_scene: PackedScene, spawn_position: Vector2, direction: Vector2, context: Dictionary = {}) -> void: var bus := _event_bus_or_null() if bus != null and bus.has_signal("projectile_requested"): bus.emit_signal("projectile_requested", projectile_scene, spawn_position, direction, context) func _combat_manager_or_null() -> Node: if not is_inside_tree(): return null return get_tree().root.get_node_or_null("CombatManager") func _event_bus_or_null() -> Node: if not is_inside_tree(): return null var root := get_tree().root var bus := root.get_node_or_null("EventBus") if bus == null: bus = load("res://autoload/event_bus.gd").new() bus.name = "EventBus" root.add_child(bus) return bus