150 lines
5.5 KiB
GDScript
150 lines
5.5 KiB
GDScript
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
|