feat: Implement Live Director System for interactive simulation control
- Added waypoint context updates to handle action queue statuses in `waypoint_context.gd`. - Introduced new scene file `manwalk.scene` with terrain and stickman setup. - Created architecture summary for the Live Director System detailing core state transitions, physics mechanics, UI elements, and action execution. - Specified class contracts and interfaces for the Live Director implementation in `phase_12_live_director.md`. - Defined state transitions, input handling, and queue editing mechanics for the Live Director feature.
This commit is contained in:
@@ -10,6 +10,7 @@ const SPEECH_BUBBLE_OFFSET := Vector2(0.0, -640.0)
|
||||
|
||||
enum RunnerState { IDLE, EXECUTING }
|
||||
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }
|
||||
enum QueueStatus { NOT_STARTED, COMPLETED, INTERRUPTED, ACTIVE, PENDING }
|
||||
|
||||
signal action_started(action: Dictionary, index: int)
|
||||
signal action_finished(action: Dictionary, index: int)
|
||||
@@ -18,8 +19,14 @@ signal queue_changed
|
||||
signal speech_finished
|
||||
signal ragdoll_requested(enabled: bool)
|
||||
signal recovery_requested
|
||||
signal active_action_cancelled(action: Dictionary)
|
||||
|
||||
var action_queue: Array[Dictionary] = []
|
||||
var _authored_queue: Array[Dictionary] = []
|
||||
var action_queue: Array[Dictionary]:
|
||||
get:
|
||||
return _authored_queue.duplicate(true)
|
||||
set(value):
|
||||
set_authored_queue(value)
|
||||
var _rig: StickmanRig = null
|
||||
var _locomotion: StickmanLocomotion = null
|
||||
var _runner_state: RunnerState = RunnerState.IDLE
|
||||
@@ -32,6 +39,13 @@ var _phase_timer: float = 0.0
|
||||
var _primary_speech: Dictionary = {}
|
||||
var _overlay_speech: Dictionary = {}
|
||||
var _speech_bubble: Node2D = null
|
||||
var _action_ids: Array[int] = []
|
||||
var _next_action_id: int = 1
|
||||
var _current_action_id: int = -1
|
||||
var _completed_ids: Dictionary = {}
|
||||
var _interrupted_ids: Dictionary = {}
|
||||
var _paused: bool = false
|
||||
var _session_started: bool = false
|
||||
|
||||
|
||||
func configure(rig: StickmanRig, locomotion: StickmanLocomotion) -> void:
|
||||
@@ -40,6 +54,8 @@ func configure(rig: StickmanRig, locomotion: StickmanLocomotion) -> void:
|
||||
|
||||
|
||||
func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
|
||||
if _paused:
|
||||
return
|
||||
_update_speech(delta)
|
||||
if _runner_state != RunnerState.EXECUTING or _is_blocked():
|
||||
return
|
||||
@@ -86,13 +102,31 @@ func sync_facing(profile: int) -> void:
|
||||
_speech_bubble.scale = Vector2(-1.0, 1.0) if profile == StickmanRig.FacingProfile.LEFT else Vector2.ONE
|
||||
|
||||
|
||||
func queue_action(action: Dictionary) -> void:
|
||||
action_queue.append(action)
|
||||
func set_authored_queue(actions: Array[Dictionary]) -> bool:
|
||||
if _session_started:
|
||||
return false
|
||||
_authored_queue = actions.duplicate(true)
|
||||
_action_ids.clear()
|
||||
for _action: Dictionary in _authored_queue:
|
||||
_action_ids.append(_allocate_action_id())
|
||||
queue_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func queue_action(action: Dictionary) -> void:
|
||||
insert_action(action_queue.size(), action)
|
||||
|
||||
|
||||
func clear_queue() -> void:
|
||||
action_queue.clear()
|
||||
_authored_queue.clear()
|
||||
_action_ids.clear()
|
||||
if not _current_run_item.is_empty():
|
||||
_cancel_current_primary(true)
|
||||
_current_action_id = -1
|
||||
_run_queue.clear()
|
||||
_run_cursor = 0
|
||||
if _session_started:
|
||||
_runner_state = RunnerState.IDLE
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
@@ -104,15 +138,109 @@ func remove_action(index: int) -> void:
|
||||
if index < 0 or index >= action_queue.size():
|
||||
push_warning("StickmanActionRunner: remove_action index %d out of range." % index)
|
||||
return
|
||||
action_queue.remove_at(index)
|
||||
var action_id: int = _action_ids[index] if index < _action_ids.size() else -1
|
||||
var status: int = _status_for_action_id(action_id)
|
||||
if _session_started and _paused and status == QueueStatus.ACTIVE:
|
||||
_cancel_current_primary(true)
|
||||
_current_action_id = -1
|
||||
_runner_state = RunnerState.IDLE
|
||||
_authored_queue.remove_at(index)
|
||||
if index < _action_ids.size():
|
||||
_action_ids.remove_at(index)
|
||||
_rebuild_pending_runtime_queue()
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func insert_action(index: int, action: Dictionary) -> void:
|
||||
action_queue.insert(clampi(index, 0, action_queue.size()), action)
|
||||
var target: int = clampi(index, 0, _authored_queue.size())
|
||||
if _session_started and _paused:
|
||||
if target <= _paused_history_floor():
|
||||
return
|
||||
var action_id: int = _allocate_action_id()
|
||||
_authored_queue.insert(target, action.duplicate(true))
|
||||
_action_ids.insert(target, action_id)
|
||||
if _session_started and _paused:
|
||||
_rebuild_pending_runtime_queue()
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func replace_action(index: int, action: Dictionary) -> bool:
|
||||
if index < 0 or index >= _authored_queue.size():
|
||||
return false
|
||||
var action_id: int = _action_ids[index]
|
||||
var status: int = _status_for_action_id(action_id)
|
||||
if _session_started and _paused and (status == QueueStatus.COMPLETED or status == QueueStatus.INTERRUPTED):
|
||||
return false
|
||||
if _session_started and _paused and status == QueueStatus.ACTIVE:
|
||||
_cancel_current_primary(true)
|
||||
_current_action_id = -1
|
||||
_runner_state = RunnerState.IDLE
|
||||
_authored_queue[index] = action.duplicate(true)
|
||||
if _session_started and _paused:
|
||||
_completed_ids.erase(action_id)
|
||||
_interrupted_ids.erase(action_id)
|
||||
_rebuild_pending_runtime_queue()
|
||||
queue_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func move_action(from_index: int, to_index: int) -> bool:
|
||||
if from_index < 0 or from_index >= _authored_queue.size() or to_index < 0 or to_index >= _authored_queue.size():
|
||||
return false
|
||||
if _session_started and _paused:
|
||||
var id: int = _action_ids[from_index]
|
||||
if _status_for_action_id(id) != QueueStatus.PENDING:
|
||||
return false
|
||||
if to_index <= _paused_history_floor():
|
||||
return false
|
||||
if from_index == to_index:
|
||||
return true
|
||||
var action: Dictionary = _authored_queue.pop_at(from_index)
|
||||
var action_id: int = _action_ids.pop_at(from_index)
|
||||
_authored_queue.insert(to_index, action)
|
||||
_action_ids.insert(to_index, action_id)
|
||||
if _session_started and _paused:
|
||||
_rebuild_pending_runtime_queue()
|
||||
queue_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func get_queue_view() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for index: int in _authored_queue.size():
|
||||
var action_id: int = _action_ids[index] if index < _action_ids.size() else -1
|
||||
result.append({"action": _authored_queue[index].duplicate(true), "authored_index": index, "status": _status_for_action_id(action_id), "action_id": action_id})
|
||||
return result
|
||||
|
||||
|
||||
func action_id_at(index: int) -> int:
|
||||
return _action_ids[index] if index >= 0 and index < _action_ids.size() else -1
|
||||
|
||||
|
||||
func index_for_action_id(action_id: int) -> int:
|
||||
return _index_for_action_id(action_id)
|
||||
|
||||
|
||||
func get_action_status(action_id: int) -> int:
|
||||
return _status_for_action_id(action_id)
|
||||
|
||||
|
||||
func pause_simulation() -> void:
|
||||
_paused = true
|
||||
|
||||
|
||||
func resume_simulation() -> void:
|
||||
_paused = false
|
||||
|
||||
|
||||
func clear_pause_state() -> void:
|
||||
_paused = false
|
||||
_completed_ids.clear()
|
||||
_interrupted_ids.clear()
|
||||
_current_action_id = -1
|
||||
_session_started = false
|
||||
|
||||
|
||||
func queue_size() -> int:
|
||||
return action_queue.size()
|
||||
|
||||
@@ -120,10 +248,10 @@ func queue_size() -> int:
|
||||
func enqueue_reactive(actions: Array[Dictionary]) -> void:
|
||||
if actions.is_empty():
|
||||
return
|
||||
if not _session_started:
|
||||
_start_session_runtime()
|
||||
if _run_queue.is_empty() and _runner_state == RunnerState.IDLE:
|
||||
for index: int in action_queue.size():
|
||||
_run_queue.append({"action": action_queue[index].duplicate(true), "queue_index": index, "reactive": false})
|
||||
_run_cursor = 0
|
||||
_rebuild_pending_runtime_queue()
|
||||
var primary: Array[Dictionary] = []
|
||||
var takeover_seen: bool = false
|
||||
for action: Dictionary in actions:
|
||||
@@ -134,7 +262,10 @@ func enqueue_reactive(actions: Array[Dictionary]) -> void:
|
||||
if role == ACTION_REGISTRY.ReactiveRole.TAKEOVER:
|
||||
if not takeover_seen:
|
||||
takeover_seen = true
|
||||
_cancel_current_primary()
|
||||
if _current_action_id >= 0:
|
||||
_interrupted_ids[_current_action_id] = true
|
||||
_cancel_current_primary(true)
|
||||
_current_action_id = -1
|
||||
_overlay_speech.clear()
|
||||
hide_speech()
|
||||
var kept: Array[Dictionary] = []
|
||||
@@ -147,8 +278,6 @@ func enqueue_reactive(actions: Array[Dictionary]) -> void:
|
||||
primary.append(action.duplicate(true))
|
||||
if primary.is_empty():
|
||||
return
|
||||
if not takeover_seen:
|
||||
_cancel_current_primary()
|
||||
var injected: Array[Dictionary] = []
|
||||
for action: Dictionary in primary:
|
||||
injected.append({"action": action, "queue_index": -1, "reactive": true})
|
||||
@@ -166,27 +295,30 @@ func clear_reactive_actions() -> void:
|
||||
_run_queue = runtime_kept
|
||||
_run_cursor = mini(_run_cursor, _run_queue.size())
|
||||
var kept: Array[Dictionary] = []
|
||||
for action: Dictionary in action_queue:
|
||||
for action: Dictionary in _authored_queue:
|
||||
if not bool(action.get("reactive", false)):
|
||||
kept.append(action)
|
||||
if kept.size() != action_queue.size():
|
||||
action_queue = kept
|
||||
if kept.size() != _authored_queue.size():
|
||||
_authored_queue = kept
|
||||
_action_ids.clear()
|
||||
for _action: Dictionary in _authored_queue:
|
||||
_action_ids.append(_allocate_action_id())
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func start_queue() -> void:
|
||||
if _runner_state == RunnerState.EXECUTING:
|
||||
if _runner_state == RunnerState.EXECUTING and _session_started:
|
||||
return
|
||||
if action_queue.is_empty():
|
||||
if not _session_started:
|
||||
_start_session_runtime()
|
||||
_paused = false
|
||||
if _authored_queue.is_empty():
|
||||
queue_finished.emit()
|
||||
return
|
||||
_runner_state = RunnerState.EXECUTING
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_index = -1
|
||||
_run_queue.clear()
|
||||
for index: int in action_queue.size():
|
||||
_run_queue.append({"action": action_queue[index].duplicate(true), "queue_index": index, "reactive": false})
|
||||
_run_cursor = 0
|
||||
_rebuild_pending_runtime_queue()
|
||||
_current_run_item.clear()
|
||||
|
||||
|
||||
@@ -198,6 +330,11 @@ func stop_queue() -> void:
|
||||
_runner_state = RunnerState.IDLE
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_index = -1
|
||||
_current_action_id = -1
|
||||
_paused = false
|
||||
_session_started = false
|
||||
_completed_ids.clear()
|
||||
_interrupted_ids.clear()
|
||||
|
||||
|
||||
func is_queue_running() -> bool:
|
||||
@@ -217,8 +354,24 @@ func _advance_to_next_action() -> void:
|
||||
return
|
||||
_current_run_item = _run_queue[_run_cursor]
|
||||
_run_cursor += 1
|
||||
var action: Dictionary = _current_run_item.get("action", {})
|
||||
_current_index = int(_current_run_item.get("queue_index", -1))
|
||||
_current_action_id = int(_current_run_item.get("action_id", -1))
|
||||
_current_index = _index_for_action_id(_current_action_id) if _current_action_id >= 0 else -1
|
||||
var action: Dictionary = _current_run_item.get("action", {})
|
||||
if String(action.get("type", "")) == "walk_to" and _rig != null:
|
||||
if _rig.state == StickmanRig.RigState.RECOVERING:
|
||||
_run_queue.insert(_run_cursor, _current_run_item.duplicate(true))
|
||||
_current_run_item.clear()
|
||||
_current_action_id = -1
|
||||
_current_index = -1
|
||||
return
|
||||
if _rig.is_ragdolled():
|
||||
_run_queue.insert(_run_cursor, _current_run_item.duplicate(true))
|
||||
_run_queue.insert(_run_cursor, {"action": {"type": "recover"}, "queue_index": -1, "action_id": -1, "reactive": true})
|
||||
_current_run_item.clear()
|
||||
_current_action_id = -1
|
||||
_current_index = -1
|
||||
return
|
||||
action_started.emit(action, _current_index)
|
||||
_begin_action(action)
|
||||
|
||||
@@ -249,14 +402,22 @@ func _begin_action(action: Dictionary) -> void:
|
||||
func _finish_action() -> void:
|
||||
var finished: Dictionary = _current_run_item.get("action", {})
|
||||
var index: int = int(_current_run_item.get("queue_index", -1))
|
||||
var action_id: int = int(_current_run_item.get("action_id", -1))
|
||||
if action_id >= 0:
|
||||
_completed_ids[action_id] = true
|
||||
index = _index_for_action_id(action_id) if action_id >= 0 else -1
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_run_item.clear()
|
||||
_current_index = -1
|
||||
_current_action_id = -1
|
||||
if not finished.is_empty():
|
||||
action_finished.emit(finished, index)
|
||||
|
||||
|
||||
func _cancel_current_primary() -> void:
|
||||
func _cancel_current_primary(notify: bool = false) -> void:
|
||||
if notify and not _current_run_item.is_empty():
|
||||
var action: Dictionary = _current_run_item.get("action", {})
|
||||
active_action_cancelled.emit(action.duplicate(true))
|
||||
if _locomotion != null:
|
||||
_locomotion.cancel_walk(not _is_blocked())
|
||||
_primary_speech.clear()
|
||||
@@ -264,10 +425,75 @@ func _cancel_current_primary() -> void:
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_run_item.clear()
|
||||
_current_index = -1
|
||||
_current_action_id = -1
|
||||
if _overlay_speech.is_empty() and _speech_bubble != null and is_instance_valid(_speech_bubble):
|
||||
_speech_bubble.hide_bubble()
|
||||
|
||||
|
||||
func _allocate_action_id() -> int:
|
||||
var result: int = _next_action_id
|
||||
_next_action_id += 1
|
||||
return result
|
||||
|
||||
|
||||
func _start_session_runtime() -> void:
|
||||
_session_started = true
|
||||
_completed_ids.clear()
|
||||
_interrupted_ids.clear()
|
||||
_action_ids.clear()
|
||||
for _action: Dictionary in _authored_queue:
|
||||
_action_ids.append(_allocate_action_id())
|
||||
|
||||
|
||||
func _runtime_item(action_id: int, action: Dictionary, index: int) -> Dictionary:
|
||||
return {"action": action.duplicate(true), "queue_index": index, "action_id": action_id, "reactive": false}
|
||||
|
||||
|
||||
func _rebuild_pending_runtime_queue() -> void:
|
||||
var rebuilt: Array[Dictionary] = []
|
||||
var retained_reactive: Array[Dictionary] = []
|
||||
for runtime_index: int in range(_run_cursor, _run_queue.size()):
|
||||
var runtime_item: Dictionary = _run_queue[runtime_index]
|
||||
if bool(runtime_item.get("reactive", false)):
|
||||
retained_reactive.append(runtime_item.duplicate(true))
|
||||
for index: int in _authored_queue.size():
|
||||
var action_id: int = _action_ids[index]
|
||||
var status: int = _status_for_action_id(action_id)
|
||||
if status == QueueStatus.PENDING or status == QueueStatus.NOT_STARTED:
|
||||
rebuilt.append(_runtime_item(action_id, _authored_queue[index], index))
|
||||
_run_queue = retained_reactive + rebuilt
|
||||
_run_cursor = 0
|
||||
if not _run_queue.is_empty() and _session_started:
|
||||
_runner_state = RunnerState.EXECUTING
|
||||
|
||||
|
||||
func _paused_history_floor() -> int:
|
||||
var floor_index: int = -1
|
||||
for index: int in _authored_queue.size():
|
||||
var status: int = _status_for_action_id(_action_ids[index])
|
||||
if status != QueueStatus.PENDING and status != QueueStatus.NOT_STARTED:
|
||||
floor_index = index
|
||||
return floor_index
|
||||
|
||||
|
||||
func _index_for_action_id(action_id: int) -> int:
|
||||
return _action_ids.find(action_id)
|
||||
|
||||
|
||||
func _status_for_action_id(action_id: int) -> int:
|
||||
if action_id < 0:
|
||||
return QueueStatus.NOT_STARTED
|
||||
if _completed_ids.has(action_id):
|
||||
return QueueStatus.COMPLETED
|
||||
if _interrupted_ids.has(action_id):
|
||||
return QueueStatus.INTERRUPTED
|
||||
if action_id == _current_action_id:
|
||||
return QueueStatus.ACTIVE
|
||||
if _session_started:
|
||||
return QueueStatus.PENDING
|
||||
return QueueStatus.NOT_STARTED
|
||||
|
||||
|
||||
func _is_blocked() -> bool:
|
||||
return _locomotion != null and _locomotion.is_action_blocked()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user