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:
+172
-6
@@ -213,9 +213,9 @@ var action_queue: Array[Dictionary]:
|
||||
return _action_runner_component.action_queue if _action_runner_component != null else _pending_action_queue
|
||||
set(value):
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.action_queue = value
|
||||
_action_runner_component.set_authored_queue(value)
|
||||
else:
|
||||
_pending_action_queue = value
|
||||
_pending_action_queue = value.duplicate(true)
|
||||
|
||||
## Original .stk asset path, assigned by StageSpawner for scene persistence.
|
||||
var source_stk_path: String = ""
|
||||
@@ -225,6 +225,10 @@ var source_stk_path: String = ""
|
||||
var _locomotion_component: StickmanLocomotion = null
|
||||
var _ragdoll_component: StickmanRagdoll = null
|
||||
var _action_runner_component: StickmanActionRunner = null
|
||||
var _simulation_paused: bool = false
|
||||
var _phase12_paused_animation: bool = false
|
||||
var _phase12_paused_animation_name: StringName = &""
|
||||
var _phase12_paused_animation_position: float = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
@@ -257,6 +261,7 @@ func _ready() -> void:
|
||||
_action_runner_component.speech_finished.connect(func() -> void: speech_finished.emit())
|
||||
_action_runner_component.ragdoll_requested.connect(func(enabled: bool) -> void: set_ragdoll(enabled))
|
||||
_action_runner_component.recovery_requested.connect(func() -> void: request_recovery())
|
||||
_action_runner_component.active_action_cancelled.connect(_on_active_action_cancelled)
|
||||
# Resolve all runtime node references.
|
||||
_skeleton = get_node_or_null(NodePath(SKELETON_PATH)) as Skeleton2D
|
||||
if _skeleton == null:
|
||||
@@ -304,6 +309,8 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if _simulation_paused:
|
||||
return
|
||||
if _ragdoll_component != null:
|
||||
_ragdoll_component.tick_pre_movement(delta, int(state), rest_timeout, auto_recover)
|
||||
if _locomotion_component != null:
|
||||
@@ -485,6 +492,12 @@ func request_recovery() -> void:
|
||||
func snap_to_standing() -> void:
|
||||
if _ragdoll_component != null:
|
||||
_ragdoll_component.snap_to_standing(int(state))
|
||||
_simulation_paused = false
|
||||
_phase12_paused_animation = false
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.clear_pause_state()
|
||||
if _ragdoll_component != null:
|
||||
_ragdoll_component.clear_pause_snapshot()
|
||||
|
||||
|
||||
## Restores the spawn-marker pose through the ragdoll component. Kept on the
|
||||
@@ -751,11 +764,17 @@ func _sync_speech_bubble_mirror() -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func queue_action(action: Dictionary) -> void:
|
||||
if _action_runner_component != null: _action_runner_component.queue_action(action)
|
||||
if _action_runner_component != null:
|
||||
insert_action(queue_size(), action)
|
||||
else:
|
||||
_pending_action_queue.append(action.duplicate(true))
|
||||
|
||||
|
||||
func clear_queue() -> void:
|
||||
if _action_runner_component != null: _action_runner_component.clear_queue()
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.clear_queue()
|
||||
else:
|
||||
_pending_action_queue.clear()
|
||||
|
||||
|
||||
func get_queue() -> Array[Dictionary]:
|
||||
@@ -763,11 +782,158 @@ func get_queue() -> Array[Dictionary]:
|
||||
|
||||
|
||||
func remove_action(index: int) -> void:
|
||||
if _action_runner_component != null: _action_runner_component.remove_action(index)
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.remove_action(index)
|
||||
elif index >= 0 and index < _pending_action_queue.size():
|
||||
_pending_action_queue.remove_at(index)
|
||||
|
||||
|
||||
func insert_action(index: int, action: Dictionary) -> void:
|
||||
if _action_runner_component != null: _action_runner_component.insert_action(index, action)
|
||||
if _action_runner_component != null:
|
||||
var normalized: Dictionary = _with_recover_barrier(action)
|
||||
if is_ragdolled() and String(normalized.get("type", "")) == "walk_to":
|
||||
var queue: Array[Dictionary] = get_queue()
|
||||
var at: int = clampi(index, 0, queue.size())
|
||||
var has_recover_barrier: bool = false
|
||||
if at > 0 and String(queue[at - 1].get("type", "")) == "recover":
|
||||
var previous_status: int = get_action_status(action_id_at(at - 1))
|
||||
has_recover_barrier = previous_status == StickmanActionRunner.QueueStatus.PENDING or previous_status == StickmanActionRunner.QueueStatus.ACTIVE
|
||||
if not has_recover_barrier:
|
||||
_action_runner_component.insert_action(at, {"type": "recover"})
|
||||
at += 1
|
||||
_action_runner_component.insert_action(at, normalized)
|
||||
else:
|
||||
_action_runner_component.insert_action(index, normalized)
|
||||
else:
|
||||
_pending_action_queue.insert(clampi(index, 0, _pending_action_queue.size()), action.duplicate(true))
|
||||
|
||||
|
||||
func replace_action(index: int, action: Dictionary) -> bool:
|
||||
if _action_runner_component == null:
|
||||
return false
|
||||
if is_ragdolled() and String(action.get("type", "")) == "walk_to":
|
||||
var queue: Array[Dictionary] = get_queue()
|
||||
var has_recover_barrier: bool = false
|
||||
if index > 0 and index < queue.size() and String(queue[index - 1].get("type", "")) == "recover":
|
||||
var previous_status: int = get_action_status(action_id_at(index - 1))
|
||||
has_recover_barrier = previous_status == StickmanActionRunner.QueueStatus.PENDING or previous_status == StickmanActionRunner.QueueStatus.ACTIVE
|
||||
if has_recover_barrier:
|
||||
return _action_runner_component.replace_action(index, action.duplicate(true))
|
||||
var replaced: bool = _action_runner_component.replace_action(index, {"type": "recover"})
|
||||
if not replaced:
|
||||
return false
|
||||
_action_runner_component.insert_action(index + 1, action.duplicate(true))
|
||||
return true
|
||||
return _action_runner_component.replace_action(index, _with_recover_barrier(action))
|
||||
|
||||
|
||||
func move_action(from_index: int, to_index: int) -> bool:
|
||||
return _action_runner_component.move_action(from_index, to_index) if _action_runner_component != null else false
|
||||
|
||||
|
||||
func get_queue_view() -> Array[Dictionary]:
|
||||
return _action_runner_component.get_queue_view() if _action_runner_component != null else []
|
||||
|
||||
|
||||
func action_id_at(index: int) -> int:
|
||||
return _action_runner_component.action_id_at(index) if _action_runner_component != null else -1
|
||||
|
||||
|
||||
func index_for_action_id(action_id: int) -> int:
|
||||
return _action_runner_component.index_for_action_id(action_id) if _action_runner_component != null else -1
|
||||
|
||||
|
||||
func get_action_status(action_id: int) -> int:
|
||||
return _action_runner_component.get_action_status(action_id) if _action_runner_component != null else StickmanActionRunner.QueueStatus.NOT_STARTED
|
||||
|
||||
|
||||
func pause_simulation() -> void:
|
||||
if _simulation_paused:
|
||||
return
|
||||
_simulation_paused = true
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.pause_simulation()
|
||||
if _ragdoll_component != null:
|
||||
_ragdoll_component.pause_simulation()
|
||||
if _anim_player != null and is_instance_valid(_anim_player) and _anim_player.is_playing():
|
||||
_phase12_paused_animation_name = _anim_player.current_animation
|
||||
_phase12_paused_animation_position = _anim_player.get_current_animation_position()
|
||||
_phase12_paused_animation = true
|
||||
_anim_player.pause()
|
||||
|
||||
|
||||
func resume_simulation() -> void:
|
||||
if not _simulation_paused:
|
||||
return
|
||||
if _ragdoll_component != null:
|
||||
_ragdoll_component.resume_simulation()
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.resume_simulation()
|
||||
_simulation_paused = false
|
||||
if _phase12_paused_animation and _anim_player != null and is_instance_valid(_anim_player):
|
||||
if _anim_player.current_animation == _phase12_paused_animation_name:
|
||||
_anim_player.play(_phase12_paused_animation_name)
|
||||
_anim_player.seek(_phase12_paused_animation_position, true)
|
||||
_phase12_paused_animation = false
|
||||
_phase12_paused_animation_name = &""
|
||||
_phase12_paused_animation_position = 0.0
|
||||
if _locomotion_component != null:
|
||||
_locomotion_component.ensure_grounded_walk_animation()
|
||||
|
||||
|
||||
func reset_simulation_pause() -> void:
|
||||
if _ragdoll_component != null:
|
||||
_ragdoll_component.clear_pause_snapshot()
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.clear_pause_state()
|
||||
_simulation_paused = false
|
||||
_phase12_paused_animation = false
|
||||
_phase12_paused_animation_name = &""
|
||||
_phase12_paused_animation_position = 0.0
|
||||
|
||||
|
||||
func get_director_anchor() -> Dictionary:
|
||||
var bounds: Rect2 = _ragdoll_component.get_world_body_bounds() if _ragdoll_component != null and state == RigState.RAGDOLL else Rect2()
|
||||
var feet: Vector2 = global_position - StickmanLocomotion.FOOT_OFFSET
|
||||
var airborne: bool = _locomotion_component != null and _locomotion_component.terrain_motion == StickmanLocomotion.TerrainMotion.AIRBORNE
|
||||
if state == RigState.RAGDOLL and not bounds.size.is_zero_approx():
|
||||
feet = Vector2(bounds.get_center().x, bounds.end.y)
|
||||
var probe_origin: Vector2 = Vector2(bounds.get_center().x, feet.y) if state == RigState.RAGDOLL and not bounds.size.is_zero_approx() else feet
|
||||
var query := PhysicsRayQueryParameters2D.create(probe_origin + Vector2.UP * 40.0, probe_origin + Vector2.DOWN * 1800.0)
|
||||
query.collision_mask = 0x7FFFFFFF
|
||||
query.collide_with_areas = false
|
||||
var excluded: Array[RID] = []
|
||||
_collect_nonterrain_rids(get_parent(), excluded)
|
||||
query.exclude = excluded
|
||||
var hit: Dictionary = get_world_2d().direct_space_state.intersect_ray(query)
|
||||
var has_ground: bool = not hit.is_empty()
|
||||
return {"feet": feet, "ground": hit.get("position", feet), "has_ground": has_ground, "airborne": airborne or state == RigState.RAGDOLL}
|
||||
|
||||
|
||||
func is_ragdolled() -> bool:
|
||||
return state == RigState.RAGDOLL
|
||||
|
||||
|
||||
func _on_active_action_cancelled(action: Dictionary) -> void:
|
||||
if String(action.get("type", "")) == "recover" and state == RigState.RECOVERING and _ragdoll_component != null:
|
||||
_ragdoll_component.interrupt_recovery_to_ragdoll()
|
||||
|
||||
|
||||
func _with_recover_barrier(action: Dictionary) -> Dictionary:
|
||||
return action.duplicate(true)
|
||||
|
||||
|
||||
func _collect_nonterrain_rids(node: Node, out: Array[RID]) -> void:
|
||||
if node == null:
|
||||
return
|
||||
if node is TerrainBlock:
|
||||
return
|
||||
if node is CollisionObject2D:
|
||||
var rid: RID = (node as CollisionObject2D).get_rid()
|
||||
if rid.is_valid() and not out.has(rid):
|
||||
out.append(rid)
|
||||
for child: Node in node.get_children():
|
||||
_collect_nonterrain_rids(child, out)
|
||||
|
||||
|
||||
func queue_size() -> int:
|
||||
|
||||
Reference in New Issue
Block a user