186 lines
5.2 KiB
GDScript
186 lines
5.2 KiB
GDScript
class_name ChartRunner
|
|
extends Node
|
|
|
|
signal chart_event_upcoming(event: Resource, time_to_event: float)
|
|
signal chart_event_triggered(event: Resource)
|
|
signal chart_reset(chart_id: StringName)
|
|
signal chart_finished(chart_id: StringName)
|
|
|
|
const UPCOMING_TIME_EPSILON := 0.02
|
|
|
|
@export var chart: Resource
|
|
@export var rhythm_manager_path: NodePath
|
|
@export var actors_container_path: NodePath
|
|
@export var beat_time_override := 0.0
|
|
@export var auto_run := true
|
|
|
|
var running := true
|
|
var _upcoming_keys: Dictionary = {}
|
|
var _triggered_keys: Dictionary = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
running = auto_run
|
|
_apply_initial_time_phase()
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
if not running or chart == null:
|
|
return
|
|
var rhythm := _rhythm_manager()
|
|
if rhythm == null or not rhythm.has_method("song_position"):
|
|
return
|
|
update_for_song_time(float(rhythm.call("song_position")))
|
|
|
|
|
|
func set_chart(next_chart: Resource) -> void:
|
|
chart = next_chart
|
|
reset()
|
|
|
|
|
|
func reset() -> void:
|
|
_upcoming_keys.clear()
|
|
_triggered_keys.clear()
|
|
var chart_id := &""
|
|
if chart != null:
|
|
chart_id = StringName(str(chart.get("chart_id")))
|
|
chart_reset.emit(chart_id)
|
|
var bus := _event_bus_or_null()
|
|
if bus != null:
|
|
bus.emit_signal("chart_reset", chart_id)
|
|
_apply_initial_time_phase()
|
|
|
|
|
|
func update_for_song_time(song_time: float) -> void:
|
|
if chart == null:
|
|
return
|
|
var beat_time := _beat_time()
|
|
for event: Resource in chart.call("all_events"):
|
|
var event_time := float(event.call("time_seconds", beat_time))
|
|
var time_to_event := event_time - song_time
|
|
var lead_time := maxf(0.0, float(event.get("lead_beats"))) * beat_time
|
|
var event_key: StringName = event.call("key")
|
|
if not _upcoming_keys.has(event_key) and time_to_event > 0.0 and time_to_event <= lead_time + UPCOMING_TIME_EPSILON:
|
|
if _time_phase_allows(event):
|
|
_upcoming_keys[event_key] = true
|
|
_emit_upcoming(event, time_to_event)
|
|
if not _triggered_keys.has(event_key) and song_time >= event_time:
|
|
_triggered_keys[event_key] = true
|
|
if _time_phase_allows(event):
|
|
_emit_triggered(event)
|
|
|
|
|
|
func pause() -> void:
|
|
running = false
|
|
|
|
|
|
func resume() -> void:
|
|
running = true
|
|
|
|
|
|
func _emit_upcoming(event: Resource, time_to_event: float) -> void:
|
|
chart_event_upcoming.emit(event, time_to_event)
|
|
var bus := _event_bus_or_null()
|
|
if bus != null:
|
|
bus.emit_signal("chart_event_upcoming", event, time_to_event)
|
|
|
|
|
|
func _emit_triggered(event: Resource) -> void:
|
|
_dispatch_to_target_actor(event)
|
|
chart_event_triggered.emit(event)
|
|
var bus := _event_bus_or_null()
|
|
if bus != null:
|
|
bus.emit_signal("chart_event_triggered", event)
|
|
|
|
|
|
func _dispatch_to_target_actor(event: Resource) -> void:
|
|
if event == null:
|
|
return
|
|
var action_id := StringName(str(event.get("action_id")))
|
|
if action_id.is_empty():
|
|
return
|
|
var target_id := StringName(str(event.get("target_id")))
|
|
if target_id.is_empty():
|
|
return
|
|
var actors := _actors_container()
|
|
if actors == null:
|
|
return
|
|
var target := actors.get_node_or_null(NodePath(str(target_id)))
|
|
if target == null:
|
|
return
|
|
var driver := target.get_node_or_null("EnemyActionDriver")
|
|
if driver != null and driver.has_method("handle_chart_event"):
|
|
driver.call("handle_chart_event", event)
|
|
|
|
|
|
func _time_phase_allows(event: Resource) -> bool:
|
|
if event == null:
|
|
return false
|
|
if event.has_method("matches_time_phase"):
|
|
var manager := _time_phase_manager()
|
|
if manager == null:
|
|
return true
|
|
return bool(event.call("matches_time_phase", StringName(str(manager.get("current_time_phase")))))
|
|
return true
|
|
|
|
|
|
func _apply_initial_time_phase() -> void:
|
|
if chart == null:
|
|
return
|
|
var initial = chart.get("initial_time_phase")
|
|
if initial == null:
|
|
return
|
|
var manager := _time_phase_manager()
|
|
if manager != null and manager.has_method("reset_to_initial"):
|
|
manager.call("reset_to_initial", StringName(str(initial)))
|
|
|
|
|
|
func _time_phase_manager() -> Node:
|
|
# Reverse root scan (matching TimeAnchorSystem): a test-added manager wins
|
|
# over the autoload, since name collisions get auto-renamed on add_child.
|
|
if not is_inside_tree():
|
|
return null
|
|
return _last_root_child_matching(func(child: Node) -> bool:
|
|
return child.has_method("set_time_phase") and child.get("current_time_phase") != null
|
|
)
|
|
|
|
|
|
func _beat_time() -> float:
|
|
if beat_time_override > 0.0:
|
|
return beat_time_override
|
|
var rhythm := _rhythm_manager()
|
|
if rhythm != null:
|
|
return float(rhythm.get("beat_time"))
|
|
return 0.5
|
|
|
|
|
|
func _rhythm_manager() -> Node:
|
|
if not is_inside_tree():
|
|
return null
|
|
if not rhythm_manager_path.is_empty():
|
|
return get_node_or_null(rhythm_manager_path)
|
|
return get_tree().root.get_node_or_null("RhythmManager")
|
|
|
|
|
|
func _actors_container() -> Node:
|
|
if not is_inside_tree() or actors_container_path.is_empty():
|
|
return null
|
|
return get_node_or_null(actors_container_path)
|
|
|
|
|
|
func _event_bus_or_null() -> Node:
|
|
if not is_inside_tree():
|
|
return null
|
|
return _last_root_child_matching(func(child: Node) -> bool:
|
|
return child != self and child.has_signal("chart_event_triggered") and child.has_signal("chart_reset") and child.has_signal("judgement_made")
|
|
)
|
|
|
|
|
|
func _last_root_child_matching(predicate: Callable) -> Node:
|
|
var children := get_tree().root.get_children()
|
|
for index: int in range(children.size() - 1, -1, -1):
|
|
var child: Node = children[index]
|
|
if bool(predicate.call(child)):
|
|
return child
|
|
return null
|