|
|
|
@@ -5,11 +5,18 @@ extends Node
|
|
|
|
|
## locomotion and signals requests back to the facade.
|
|
|
|
|
|
|
|
|
|
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
|
|
|
|
const STAGE_ACTION_PLAN := preload("res://scripts/stage/stage_action_plan.gd")
|
|
|
|
|
const STAGE_ACTION_IDENTITY := preload("res://scripts/stage/stage_action_identity.gd")
|
|
|
|
|
const MISSING_PROP_PLACEHOLDER := preload("res://scripts/props/missing_prop_placeholder.gd")
|
|
|
|
|
const PROP_INTERACTION_REACH: float = 110.0
|
|
|
|
|
const PROP_PURSUIT_MIN_SHIFT: float = 16.0
|
|
|
|
|
const PROP_PURSUIT_RETARGET_INTERVAL: float = 0.1
|
|
|
|
|
const PROP_PURSUIT_BUDGET: float = 12.0
|
|
|
|
|
const SPEECH_BUBBLE_SCRIPT := preload("res://scripts/speech/stickman_speech_bubble.gd")
|
|
|
|
|
const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd")
|
|
|
|
|
|
|
|
|
|
enum RunnerState { IDLE, EXECUTING }
|
|
|
|
|
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING, PROP_INTERACTION }
|
|
|
|
|
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING, PROP_INTERACTION, GROUP_BARRIER, PURSUIT_WAIT }
|
|
|
|
|
enum QueueStatus { NOT_STARTED, COMPLETED, INTERRUPTED, ACTIVE, PENDING }
|
|
|
|
|
|
|
|
|
|
signal action_started(action: Dictionary, index: int)
|
|
|
|
@@ -20,8 +27,12 @@ signal speech_finished
|
|
|
|
|
signal ragdoll_requested(enabled: bool)
|
|
|
|
|
signal recovery_requested
|
|
|
|
|
signal active_action_cancelled(action: Dictionary)
|
|
|
|
|
signal planned_action_started(action_id: String, action: Dictionary, step_index: int)
|
|
|
|
|
signal planned_action_ended(action_id: String, outcome: StringName, step_index: int)
|
|
|
|
|
signal plan_changed
|
|
|
|
|
|
|
|
|
|
var _authored_queue: Array[Dictionary] = []
|
|
|
|
|
var _authored_steps: Array[Dictionary] = []
|
|
|
|
|
var action_queue: Array[Dictionary]:
|
|
|
|
|
get:
|
|
|
|
|
return _authored_queue.duplicate(true)
|
|
|
|
@@ -43,7 +54,26 @@ var _speech_bubble: Node2D = null
|
|
|
|
|
var _speech_preview_suppressed: bool = false
|
|
|
|
|
var _action_ids: Array[int] = []
|
|
|
|
|
var _next_action_id: int = 1
|
|
|
|
|
var _stable_to_numeric_ids: Dictionary = {}
|
|
|
|
|
var _stable_status: Dictionary = {}
|
|
|
|
|
var _current_action_id: int = -1
|
|
|
|
|
var _current_stable_action_id: String = ""
|
|
|
|
|
var _current_step_index: int = -1
|
|
|
|
|
var _active_step_entry: Dictionary = {}
|
|
|
|
|
var _group_say_action_id: String = ""
|
|
|
|
|
var _group_say_numeric_id: int = -1
|
|
|
|
|
var _group_say_step_index: int = -1
|
|
|
|
|
var _group_say_action: Dictionary = {}
|
|
|
|
|
var _group_say_barrier: bool = false
|
|
|
|
|
var _group_primary_done: bool = false
|
|
|
|
|
var _parallel_active_numeric_ids: Dictionary = {}
|
|
|
|
|
var _pursuit_action_id: String = ""
|
|
|
|
|
var _pursuit_prop_id: String = ""
|
|
|
|
|
var _pursuit_offset: Vector2 = Vector2.ZERO
|
|
|
|
|
var _pursuit_last_prop_position: Vector2 = Vector2.ZERO
|
|
|
|
|
var _pursuit_approach: Vector2 = Vector2.INF
|
|
|
|
|
var _pursuit_elapsed: float = 0.0
|
|
|
|
|
var _pursuit_retarget_elapsed: float = 0.0
|
|
|
|
|
var _completed_ids: Dictionary = {}
|
|
|
|
|
var _interrupted_ids: Dictionary = {}
|
|
|
|
|
var _paused: bool = false
|
|
|
|
@@ -70,8 +100,14 @@ func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
|
|
|
|
|
_advance_to_next_action()
|
|
|
|
|
ActionPhase.WALKING:
|
|
|
|
|
if _locomotion != null:
|
|
|
|
|
if _locomotion.consume_walk_failed() or _locomotion.consume_walk_completed():
|
|
|
|
|
_finish_action()
|
|
|
|
|
if _locomotion.consume_walk_failed():
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
elif _locomotion.consume_walk_completed():
|
|
|
|
|
if _pursuit_action_id == _current_stable_action_id:
|
|
|
|
|
_check_prop_pursuit_arrival()
|
|
|
|
|
else:
|
|
|
|
|
_finish_action(&"succeeded")
|
|
|
|
|
ActionPhase.SPEAKING:
|
|
|
|
|
if _primary_speech.is_empty():
|
|
|
|
|
_finish_action()
|
|
|
|
@@ -79,6 +115,11 @@ func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
|
|
|
|
|
_phase_timer -= delta
|
|
|
|
|
if _phase_timer <= 0.0:
|
|
|
|
|
_finish_action()
|
|
|
|
|
ActionPhase.GROUP_BARRIER:
|
|
|
|
|
if not _group_say_barrier or _group_say_action_id.is_empty():
|
|
|
|
|
_complete_active_step()
|
|
|
|
|
ActionPhase.PURSUIT_WAIT:
|
|
|
|
|
pass
|
|
|
|
|
ActionPhase.RAGDOLLING:
|
|
|
|
|
if ragdoll_at_rest:
|
|
|
|
|
_finish_action()
|
|
|
|
@@ -91,6 +132,48 @@ func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
|
|
|
|
|
_finish_action()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func pre_movement(delta: float) -> void:
|
|
|
|
|
if _paused or _pursuit_action_id.is_empty() or _action_phase not in [ActionPhase.WALKING, ActionPhase.PURSUIT_WAIT]:
|
|
|
|
|
return
|
|
|
|
|
_pursuit_elapsed += delta
|
|
|
|
|
_pursuit_retarget_elapsed += delta
|
|
|
|
|
if _pursuit_elapsed >= PROP_PURSUIT_BUDGET:
|
|
|
|
|
if _locomotion != null:
|
|
|
|
|
_locomotion.cancel_walk(false)
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
return
|
|
|
|
|
var prop: PropBlock = _resolve_pursuit_prop(_pursuit_prop_id)
|
|
|
|
|
if prop == null:
|
|
|
|
|
if _locomotion != null:
|
|
|
|
|
_locomotion.cancel_walk(false)
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
return
|
|
|
|
|
var prop_position: Vector2 = prop.global_position
|
|
|
|
|
if prop_position.distance_to(_pursuit_last_prop_position) < PROP_PURSUIT_MIN_SHIFT or _pursuit_retarget_elapsed < PROP_PURSUIT_RETARGET_INTERVAL:
|
|
|
|
|
return
|
|
|
|
|
var projected: Vector2 = _locomotion.project_target_to_navigation(prop_position + _pursuit_offset) if _locomotion != null else Vector2.INF
|
|
|
|
|
var move_style: String = ACTION_REGISTRY.checked_style(String(_current_run_item.get("action", {}).get("style", ACTION_REGISTRY.WALK)))
|
|
|
|
|
var retargeted: bool = false
|
|
|
|
|
if projected.is_finite() and _locomotion != null:
|
|
|
|
|
retargeted = _locomotion.retarget_walk(projected) if _locomotion.is_walking() else _locomotion.move_to(projected, move_style)
|
|
|
|
|
if not projected.is_finite() or not retargeted:
|
|
|
|
|
if _locomotion != null:
|
|
|
|
|
_locomotion.cancel_walk(false)
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
return
|
|
|
|
|
_pursuit_last_prop_position = prop_position
|
|
|
|
|
_pursuit_approach = projected
|
|
|
|
|
_pursuit_retarget_elapsed = 0.0
|
|
|
|
|
_action_phase = ActionPhase.WALKING
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_prop_pursuit_active() -> bool:
|
|
|
|
|
return not _pursuit_action_id.is_empty()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func speak(text: String, duration: float) -> void:
|
|
|
|
|
speak_action({"type": "speak", "text": text, "duration": maxf(duration, 0.0)})
|
|
|
|
|
|
|
|
|
@@ -134,111 +217,286 @@ func sync_facing(profile: int) -> void:
|
|
|
|
|
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())
|
|
|
|
|
var steps: Array[Dictionary] = []
|
|
|
|
|
for source_action: Dictionary in actions:
|
|
|
|
|
var action: Dictionary = source_action.duplicate(true)
|
|
|
|
|
action["action_id"] = _allocate_stable_action_id()
|
|
|
|
|
if ACTION_REGISTRY.is_move_action(action) and action.get("target", null) is Vector2:
|
|
|
|
|
action["destination"] = {"kind": "place", "position": action["target"]}
|
|
|
|
|
action.erase("target")
|
|
|
|
|
steps.append({"actions": [action]})
|
|
|
|
|
_authored_steps = steps
|
|
|
|
|
_sync_legacy_queue()
|
|
|
|
|
queue_changed.emit()
|
|
|
|
|
plan_changed.emit()
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_action_plan() -> Array[Dictionary]:
|
|
|
|
|
return _authored_steps.duplicate(true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func set_action_plan(steps: Array[Dictionary]) -> bool:
|
|
|
|
|
if _session_started or not STAGE_ACTION_PLAN.validate_steps(steps).is_empty():
|
|
|
|
|
return false
|
|
|
|
|
_authored_steps = steps.duplicate(true)
|
|
|
|
|
_sync_legacy_queue()
|
|
|
|
|
queue_changed.emit()
|
|
|
|
|
plan_changed.emit()
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func find_action(action_id: String) -> Dictionary:
|
|
|
|
|
return STAGE_ACTION_PLAN.find_action(_authored_steps, action_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_plan_view() -> Array[Dictionary]:
|
|
|
|
|
var result: Array[Dictionary] = []
|
|
|
|
|
for step_index: int in _authored_steps.size():
|
|
|
|
|
var actions_value: Variant = _authored_steps[step_index].get("actions", [])
|
|
|
|
|
if not actions_value is Array:
|
|
|
|
|
continue
|
|
|
|
|
for raw_action: Variant in actions_value:
|
|
|
|
|
if not raw_action is Dictionary:
|
|
|
|
|
continue
|
|
|
|
|
var action: Dictionary = raw_action as Dictionary
|
|
|
|
|
var action_id: String = String(action.get("action_id", ""))
|
|
|
|
|
result.append({
|
|
|
|
|
"step_index": step_index,
|
|
|
|
|
"action_id": action_id,
|
|
|
|
|
"action": action.duplicate(true),
|
|
|
|
|
"status": _status_for_stable_id(action_id),
|
|
|
|
|
"numeric_id": _numeric_for_stable_id(action_id),
|
|
|
|
|
})
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func add_plan_step(action: Dictionary) -> String:
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return ""
|
|
|
|
|
var created: Dictionary = _prepare_new_plan_action(action)
|
|
|
|
|
var next: Array[Dictionary] = STAGE_ACTION_PLAN.append_step(_authored_steps, created)
|
|
|
|
|
return String(created.get("action_id", "")) if _commit_plan_edit(next) else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func add_plan_action_together(step_index: int, action: Dictionary) -> String:
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return ""
|
|
|
|
|
if _session_started and _paused and not _plan_step_is_future(step_index):
|
|
|
|
|
return ""
|
|
|
|
|
var created: Dictionary = _prepare_new_plan_action(action)
|
|
|
|
|
var next: Array[Dictionary] = STAGE_ACTION_PLAN.append_together(_authored_steps, step_index, created)
|
|
|
|
|
return String(created.get("action_id", "")) if _commit_plan_edit(next) else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func replace_plan_action(action_id: String, action: Dictionary) -> bool:
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return false
|
|
|
|
|
var status: StringName = _status_for_stable_id(action_id)
|
|
|
|
|
if _session_started and _paused and status in [&"succeeded", &"failed", &"cancelled"]:
|
|
|
|
|
return false
|
|
|
|
|
var next: Array[Dictionary] = STAGE_ACTION_PLAN.replace_action(_authored_steps, action_id, action)
|
|
|
|
|
if next.is_empty() and not _authored_steps.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
if _session_started and _paused and status == &"active":
|
|
|
|
|
_cancel_stable_action(action_id)
|
|
|
|
|
_stable_status[action_id] = &"pending"
|
|
|
|
|
return _commit_plan_edit(next)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func remove_plan_action(action_id: String) -> bool:
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return false
|
|
|
|
|
var location: Dictionary = find_action(action_id)
|
|
|
|
|
if location.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
if _status_for_stable_id(action_id) == &"active":
|
|
|
|
|
_cancel_stable_action(action_id)
|
|
|
|
|
var next: Array[Dictionary] = STAGE_ACTION_PLAN.remove_action(_authored_steps, action_id)
|
|
|
|
|
_stable_status.erase(action_id)
|
|
|
|
|
return _commit_plan_edit(next, true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func move_plan_step(from_index: int, to_index: int) -> bool:
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return false
|
|
|
|
|
if _session_started and _paused and (not _plan_step_is_future(from_index) or not _plan_step_is_future(to_index)):
|
|
|
|
|
return false
|
|
|
|
|
var next: Array[Dictionary] = STAGE_ACTION_PLAN.move_step(_authored_steps, from_index, to_index)
|
|
|
|
|
return _commit_plan_edit(next) if from_index != to_index else true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_add_plan_action_together(step_index: int, action_type: String) -> bool:
|
|
|
|
|
if _session_started and _paused and not _plan_step_is_future(step_index):
|
|
|
|
|
return false
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return false
|
|
|
|
|
return step_index >= 0 and step_index < _authored_steps.size() and STAGE_ACTION_PLAN.can_add_together(_authored_steps[step_index], action_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_reorder_plan_step(step_index: int) -> bool:
|
|
|
|
|
if step_index < 0 or step_index >= _authored_steps.size():
|
|
|
|
|
return false
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return false
|
|
|
|
|
return not _session_started or _plan_step_is_future(step_index)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _prepare_new_plan_action(source_action: Dictionary) -> Dictionary:
|
|
|
|
|
var action: Dictionary = source_action.duplicate(true)
|
|
|
|
|
action["action_id"] = _allocate_stable_action_id()
|
|
|
|
|
if ACTION_REGISTRY.is_move_action(action) and not action.has("destination") and action.get("target", null) is Vector2:
|
|
|
|
|
action["destination"] = {"kind": "place", "position": action["target"]}
|
|
|
|
|
action.erase("target")
|
|
|
|
|
return action
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _commit_plan_edit(next: Array[Dictionary], allow_empty: bool = false) -> bool:
|
|
|
|
|
if next.is_empty() and not allow_empty:
|
|
|
|
|
return false
|
|
|
|
|
if not STAGE_ACTION_PLAN.validate_steps(next).is_empty():
|
|
|
|
|
return false
|
|
|
|
|
_authored_steps = next.duplicate(true)
|
|
|
|
|
_sync_legacy_queue()
|
|
|
|
|
if _session_started:
|
|
|
|
|
_rebuild_pending_runtime_queue()
|
|
|
|
|
queue_changed.emit()
|
|
|
|
|
plan_changed.emit()
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _sync_legacy_queue() -> void:
|
|
|
|
|
_authored_queue = STAGE_ACTION_PLAN.flatten(_authored_steps)
|
|
|
|
|
_action_ids.clear()
|
|
|
|
|
for action: Dictionary in _authored_queue:
|
|
|
|
|
var stable_id: String = String(action.get("action_id", ""))
|
|
|
|
|
if not _stable_to_numeric_ids.has(stable_id):
|
|
|
|
|
_stable_to_numeric_ids[stable_id] = _allocate_action_id()
|
|
|
|
|
_action_ids.append(int(_stable_to_numeric_ids[stable_id]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _allocate_stable_action_id() -> String:
|
|
|
|
|
var identity := STAGE_ACTION_IDENTITY.new() as StageActionIdentity
|
|
|
|
|
var world: Node2D = _rig.get_parent() as Node2D if _rig != null else null
|
|
|
|
|
return identity.allocate(world)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _numeric_for_stable_id(action_id: String) -> int:
|
|
|
|
|
if not _stable_to_numeric_ids.has(action_id):
|
|
|
|
|
_stable_to_numeric_ids[action_id] = _allocate_action_id()
|
|
|
|
|
return int(_stable_to_numeric_ids[action_id])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _status_for_stable_id(action_id: String) -> StringName:
|
|
|
|
|
if _stable_status.has(action_id):
|
|
|
|
|
return StringName(String(_stable_status[action_id]))
|
|
|
|
|
return &"pending" if _session_started else &"not_started"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _plan_step_is_future(step_index: int) -> bool:
|
|
|
|
|
if step_index < 0 or step_index >= _authored_steps.size():
|
|
|
|
|
return false
|
|
|
|
|
var actions: Array = _authored_steps[step_index].get("actions", [])
|
|
|
|
|
for raw_action: Variant in actions:
|
|
|
|
|
if not raw_action is Dictionary:
|
|
|
|
|
return false
|
|
|
|
|
if _status_for_stable_id(String((raw_action as Dictionary).get("action_id", ""))) != &"pending":
|
|
|
|
|
return false
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _cancel_stable_action(action_id: String) -> void:
|
|
|
|
|
if action_id == _current_stable_action_id:
|
|
|
|
|
_cancel_current_primary(true)
|
|
|
|
|
_stable_status[action_id] = &"cancelled"
|
|
|
|
|
elif action_id == _group_say_action_id:
|
|
|
|
|
_overlay_speech.clear()
|
|
|
|
|
_parallel_active_numeric_ids.erase(_group_say_numeric_id)
|
|
|
|
|
_stable_status[action_id] = &"cancelled"
|
|
|
|
|
planned_action_ended.emit(action_id, &"cancelled", _group_say_step_index)
|
|
|
|
|
_group_say_action_id = ""
|
|
|
|
|
_group_say_numeric_id = -1
|
|
|
|
|
_group_say_action.clear()
|
|
|
|
|
if _group_say_barrier:
|
|
|
|
|
_group_say_barrier = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func queue_action(action: Dictionary) -> void:
|
|
|
|
|
insert_action(action_queue.size(), action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func clear_queue() -> void:
|
|
|
|
|
_authored_queue.clear()
|
|
|
|
|
_action_ids.clear()
|
|
|
|
|
if not _current_run_item.is_empty():
|
|
|
|
|
_cancel_current_primary(true)
|
|
|
|
|
_current_action_id = -1
|
|
|
|
|
_current_stable_action_id = ""
|
|
|
|
|
_group_say_action_id = ""
|
|
|
|
|
_group_say_action.clear()
|
|
|
|
|
_overlay_speech.clear()
|
|
|
|
|
_authored_steps.clear()
|
|
|
|
|
_sync_legacy_queue()
|
|
|
|
|
_run_queue.clear()
|
|
|
|
|
_run_cursor = 0
|
|
|
|
|
if _session_started:
|
|
|
|
|
_runner_state = RunnerState.IDLE
|
|
|
|
|
queue_changed.emit()
|
|
|
|
|
plan_changed.emit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_queue() -> Array[Dictionary]:
|
|
|
|
|
return action_queue.duplicate(true)
|
|
|
|
|
return _authored_queue.duplicate(true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func remove_action(index: int) -> void:
|
|
|
|
|
if index < 0 or index >= action_queue.size():
|
|
|
|
|
if index < 0 or index >= _action_ids.size():
|
|
|
|
|
push_warning("StickmanActionRunner: remove_action index %d out of range." % index)
|
|
|
|
|
return
|
|
|
|
|
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()
|
|
|
|
|
var stable_id: String = _stable_for_numeric_id(_action_ids[index])
|
|
|
|
|
if not stable_id.is_empty():
|
|
|
|
|
remove_plan_action(stable_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func insert_action(index: int, action: Dictionary) -> void:
|
|
|
|
|
if _session_started and not _paused:
|
|
|
|
|
return
|
|
|
|
|
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()
|
|
|
|
|
var step_target: int = _step_boundary_for_flat_index(target)
|
|
|
|
|
if _session_started and _paused and not _plan_step_is_future(step_target):
|
|
|
|
|
return
|
|
|
|
|
var created: Dictionary = _prepare_new_plan_action(action)
|
|
|
|
|
var next: Array[Dictionary] = _authored_steps.duplicate(true)
|
|
|
|
|
next.insert(clampi(step_target, 0, next.size()), {"actions": [created]})
|
|
|
|
|
_commit_plan_edit(next)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
var stable_id: String = String(_authored_queue[index].get("action_id", ""))
|
|
|
|
|
var replacement: Dictionary = action.duplicate(true)
|
|
|
|
|
if ACTION_REGISTRY.is_move_action(replacement) and replacement.get("target", null) is Vector2:
|
|
|
|
|
replacement["destination"] = {"kind": "place", "position": replacement["target"]}
|
|
|
|
|
replacement.erase("target")
|
|
|
|
|
return replace_plan_action(stable_id, replacement)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
var source_id: String = String(_authored_queue[from_index].get("action_id", ""))
|
|
|
|
|
var target_id: String = String(_authored_queue[to_index].get("action_id", ""))
|
|
|
|
|
var source: Dictionary = find_action(source_id)
|
|
|
|
|
var target: Dictionary = find_action(target_id)
|
|
|
|
|
if source.is_empty() or target.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
return move_plan_step(int(source["step_index"]), int(target["step_index"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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})
|
|
|
|
|
result.append({"action": _authored_queue[index].duplicate(true), "authored_index": index, "status": _status_for_action_id(action_id), "action_id": action_id, "stable_action_id": String(_authored_queue[index].get("action_id", "")), "step_index": int(_authored_queue[index].get("step_index", -1))})
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -266,7 +524,10 @@ func clear_pause_state() -> void:
|
|
|
|
|
_paused = false
|
|
|
|
|
_completed_ids.clear()
|
|
|
|
|
_interrupted_ids.clear()
|
|
|
|
|
_stable_status.clear()
|
|
|
|
|
_parallel_active_numeric_ids.clear()
|
|
|
|
|
_current_action_id = -1
|
|
|
|
|
_current_stable_action_id = ""
|
|
|
|
|
_session_started = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -360,10 +621,17 @@ func stop_queue() -> void:
|
|
|
|
|
_action_phase = ActionPhase.NONE
|
|
|
|
|
_current_index = -1
|
|
|
|
|
_current_action_id = -1
|
|
|
|
|
_current_stable_action_id = ""
|
|
|
|
|
_current_step_index = -1
|
|
|
|
|
_active_step_entry.clear()
|
|
|
|
|
_group_say_action_id = ""
|
|
|
|
|
_group_say_action.clear()
|
|
|
|
|
_parallel_active_numeric_ids.clear()
|
|
|
|
|
_paused = false
|
|
|
|
|
_session_started = false
|
|
|
|
|
_completed_ids.clear()
|
|
|
|
|
_interrupted_ids.clear()
|
|
|
|
|
_stable_status.clear()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_queue_running() -> bool:
|
|
|
|
@@ -381,8 +649,12 @@ func _advance_to_next_action() -> void:
|
|
|
|
|
_current_index = -1
|
|
|
|
|
queue_finished.emit()
|
|
|
|
|
return
|
|
|
|
|
_current_run_item = _run_queue[_run_cursor]
|
|
|
|
|
var next_item: Dictionary = _run_queue[_run_cursor]
|
|
|
|
|
_run_cursor += 1
|
|
|
|
|
if next_item.has("actions"):
|
|
|
|
|
_start_plan_step(next_item)
|
|
|
|
|
return
|
|
|
|
|
_current_run_item = next_item
|
|
|
|
|
_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
|
|
|
|
@@ -418,6 +690,121 @@ func _advance_to_next_action() -> void:
|
|
|
|
|
_begin_action(action, legacy_speed_override)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _start_plan_step(step_entry: Dictionary) -> void:
|
|
|
|
|
_active_step_entry = step_entry.duplicate(true)
|
|
|
|
|
_current_step_index = int(step_entry.get("step_index", -1))
|
|
|
|
|
_group_primary_done = false
|
|
|
|
|
_group_say_barrier = false
|
|
|
|
|
_group_say_action_id = ""
|
|
|
|
|
_group_say_numeric_id = -1
|
|
|
|
|
_group_say_action.clear()
|
|
|
|
|
var raw_actions: Array = step_entry.get("actions", [])
|
|
|
|
|
var primary_action: Dictionary = {}
|
|
|
|
|
var primary_id: String = ""
|
|
|
|
|
var primary_numeric_id: int = -1
|
|
|
|
|
var speech_started: bool = false
|
|
|
|
|
for action_index: int in raw_actions.size():
|
|
|
|
|
var raw_action: Variant = raw_actions[action_index]
|
|
|
|
|
if not raw_action is Dictionary:
|
|
|
|
|
continue
|
|
|
|
|
var authored_action: Dictionary = raw_action as Dictionary
|
|
|
|
|
var action_id: String = String(authored_action.get("action_id", ""))
|
|
|
|
|
var numeric_id: int = _numeric_for_stable_id(action_id)
|
|
|
|
|
var runtime_action: Dictionary = _runtime_action_for_plan(authored_action)
|
|
|
|
|
_stable_status[action_id] = &"active"
|
|
|
|
|
_parallel_active_numeric_ids[numeric_id] = true
|
|
|
|
|
var flat_index: int = _flat_index_for_action(action_id)
|
|
|
|
|
planned_action_started.emit(action_id, runtime_action.duplicate(true), _current_step_index)
|
|
|
|
|
action_started.emit(runtime_action.duplicate(true), flat_index)
|
|
|
|
|
if String(authored_action.get("type", "")) == "speak" and raw_actions.size() == 2:
|
|
|
|
|
speech_started = true
|
|
|
|
|
speak_overlay_action(runtime_action)
|
|
|
|
|
if bool(authored_action.get("legacy_nonblocking", false)):
|
|
|
|
|
_stable_status[action_id] = &"succeeded"
|
|
|
|
|
_completed_ids[numeric_id] = true
|
|
|
|
|
_parallel_active_numeric_ids.erase(numeric_id)
|
|
|
|
|
planned_action_ended.emit(action_id, &"succeeded", _current_step_index)
|
|
|
|
|
action_finished.emit(runtime_action.duplicate(true), flat_index)
|
|
|
|
|
else:
|
|
|
|
|
_group_say_action_id = action_id
|
|
|
|
|
_group_say_numeric_id = numeric_id
|
|
|
|
|
_group_say_step_index = _current_step_index
|
|
|
|
|
_group_say_action = runtime_action.duplicate(true)
|
|
|
|
|
_group_say_barrier = true
|
|
|
|
|
elif primary_action.is_empty():
|
|
|
|
|
primary_action = runtime_action
|
|
|
|
|
primary_id = action_id
|
|
|
|
|
primary_numeric_id = numeric_id
|
|
|
|
|
if primary_action.is_empty():
|
|
|
|
|
_complete_active_step()
|
|
|
|
|
return
|
|
|
|
|
_current_run_item = {
|
|
|
|
|
"action": primary_action.duplicate(true),
|
|
|
|
|
"queue_index": _flat_index_for_action(primary_id),
|
|
|
|
|
"action_id": primary_numeric_id,
|
|
|
|
|
"stable_action_id": primary_id,
|
|
|
|
|
"step_index": _current_step_index,
|
|
|
|
|
"reactive": false,
|
|
|
|
|
}
|
|
|
|
|
_current_action_id = primary_numeric_id
|
|
|
|
|
_current_stable_action_id = primary_id
|
|
|
|
|
_current_index = int(_current_run_item.get("queue_index", -1))
|
|
|
|
|
var action: Dictionary = primary_action.duplicate(true)
|
|
|
|
|
if ACTION_REGISTRY.is_move_action(action):
|
|
|
|
|
action["type"] = ACTION_REGISTRY.MOVE_TO
|
|
|
|
|
var style: String = ACTION_REGISTRY.checked_style(action.get("style", null))
|
|
|
|
|
action["style"] = style if not style.is_empty() else ACTION_REGISTRY.WALK
|
|
|
|
|
_begin_action(action)
|
|
|
|
|
if not speech_started and String(primary_action.get("type", "")) == "speak":
|
|
|
|
|
# Singleton Say is executed by the primary speech state, not an overlay.
|
|
|
|
|
_group_say_action_id = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _runtime_action_for_plan(action: Dictionary) -> Dictionary:
|
|
|
|
|
var runtime_action: Dictionary = action.duplicate(true)
|
|
|
|
|
if ACTION_REGISTRY.is_move_action(runtime_action):
|
|
|
|
|
var destination_value: Variant = runtime_action.get("destination", {})
|
|
|
|
|
if destination_value is Dictionary:
|
|
|
|
|
var destination: Dictionary = destination_value
|
|
|
|
|
var point_value: Variant = destination.get("position", destination.get("last_known_approach", Vector2.ZERO))
|
|
|
|
|
runtime_action["target"] = point_value
|
|
|
|
|
return runtime_action
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _complete_active_step() -> void:
|
|
|
|
|
_active_step_entry.clear()
|
|
|
|
|
_current_step_index = -1
|
|
|
|
|
_group_primary_done = false
|
|
|
|
|
_group_say_barrier = false
|
|
|
|
|
_action_phase = ActionPhase.NONE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _flat_index_for_action(action_id: String) -> int:
|
|
|
|
|
for index: int in _authored_queue.size():
|
|
|
|
|
if String(_authored_queue[index].get("action_id", "")) == action_id:
|
|
|
|
|
return index
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _stable_for_numeric_id(numeric_id: int) -> String:
|
|
|
|
|
for stable_id: Variant in _stable_to_numeric_ids:
|
|
|
|
|
if int(_stable_to_numeric_ids[stable_id]) == numeric_id:
|
|
|
|
|
return String(stable_id)
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _step_boundary_for_flat_index(flat_index: int) -> int:
|
|
|
|
|
var cursor: int = 0
|
|
|
|
|
for step_index: int in _authored_steps.size():
|
|
|
|
|
var actions: Array = _authored_steps[step_index].get("actions", [])
|
|
|
|
|
if flat_index <= cursor:
|
|
|
|
|
return step_index
|
|
|
|
|
if flat_index < cursor + actions.size():
|
|
|
|
|
return step_index + 1
|
|
|
|
|
cursor += actions.size()
|
|
|
|
|
return _authored_steps.size()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _begin_action(action: Dictionary, legacy_speed_override: float = -1.0) -> void:
|
|
|
|
|
match String(action.get("type", "")):
|
|
|
|
|
"move_to":
|
|
|
|
@@ -425,8 +812,15 @@ func _begin_action(action: Dictionary, legacy_speed_override: float = -1.0) -> v
|
|
|
|
|
var style: String = ACTION_REGISTRY.checked_style(action.get("style", null))
|
|
|
|
|
if style.is_empty():
|
|
|
|
|
style = ACTION_REGISTRY.WALK
|
|
|
|
|
if _locomotion == null or not _locomotion.move_to(action.get("target", Vector2.ZERO), style, legacy_speed_override):
|
|
|
|
|
_finish_action()
|
|
|
|
|
var destination_value: Variant = action.get("destination", {})
|
|
|
|
|
var move_started: bool = false
|
|
|
|
|
if destination_value is Dictionary and String((destination_value as Dictionary).get("kind", "")) == "prop":
|
|
|
|
|
move_started = _start_prop_pursuit(action, destination_value as Dictionary, style, legacy_speed_override)
|
|
|
|
|
elif _locomotion != null:
|
|
|
|
|
move_started = _locomotion.move_to(action.get("target", Vector2.ZERO), style, legacy_speed_override)
|
|
|
|
|
if not move_started:
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
"speak":
|
|
|
|
|
if bool(action.get("keep_acting", false)):
|
|
|
|
|
speak_overlay_action(action)
|
|
|
|
@@ -445,34 +839,142 @@ func _begin_action(action: Dictionary, legacy_speed_override: float = -1.0) -> v
|
|
|
|
|
recovery_requested.emit()
|
|
|
|
|
"pick_up_prop", "drop_prop", "throw_prop", "set_down_prop", "use_prop":
|
|
|
|
|
if _rig == null or not _rig.start_prop_action(action):
|
|
|
|
|
_finish_action()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
else:
|
|
|
|
|
_prop_action_finished = false
|
|
|
|
|
_action_phase = ActionPhase.PROP_INTERACTION
|
|
|
|
|
_:
|
|
|
|
|
push_warning("StickmanActionRunner: unknown action type '%s'; skipped." % String(action.get("type", "")))
|
|
|
|
|
_finish_action()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _finish_action() -> void:
|
|
|
|
|
func _start_prop_pursuit(action: Dictionary, destination: Dictionary, style: String, speed_override: float) -> bool:
|
|
|
|
|
var prop_id_value: Variant = destination.get("target_prop_id", null)
|
|
|
|
|
var offset_value: Variant = destination.get("approach_offset", null)
|
|
|
|
|
if not prop_id_value is String or not offset_value is Vector2 or not (offset_value as Vector2).is_finite():
|
|
|
|
|
return false
|
|
|
|
|
var prop: PropBlock = _resolve_pursuit_prop(String(prop_id_value))
|
|
|
|
|
if prop == null or _locomotion == null:
|
|
|
|
|
return false
|
|
|
|
|
var approach: Vector2 = prop.global_position + (offset_value as Vector2)
|
|
|
|
|
var projected: Vector2 = _locomotion.project_target_to_navigation(approach)
|
|
|
|
|
if not projected.is_finite():
|
|
|
|
|
return false
|
|
|
|
|
_pursuit_action_id = String(action.get("action_id", _current_stable_action_id))
|
|
|
|
|
_pursuit_prop_id = String(prop_id_value)
|
|
|
|
|
_pursuit_offset = offset_value as Vector2
|
|
|
|
|
_pursuit_last_prop_position = prop.global_position
|
|
|
|
|
_pursuit_approach = projected
|
|
|
|
|
_pursuit_elapsed = 0.0
|
|
|
|
|
_pursuit_retarget_elapsed = 0.0
|
|
|
|
|
return _locomotion.move_to(projected, style, speed_override)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _resolve_pursuit_prop(prop_id: String) -> PropBlock:
|
|
|
|
|
if prop_id.is_empty() or _rig == null or not is_instance_valid(_rig):
|
|
|
|
|
return null
|
|
|
|
|
var world: Node = _rig.get_parent()
|
|
|
|
|
if world == null:
|
|
|
|
|
return null
|
|
|
|
|
for child: Node in world.get_children():
|
|
|
|
|
if child is PropBlock and not child.is_queued_for_deletion() and String((child as PropBlock).stage_object_id) == prop_id:
|
|
|
|
|
return child as PropBlock
|
|
|
|
|
if child.get_script() == MISSING_PROP_PLACEHOLDER and String(child.get("stage_object_id")) == prop_id:
|
|
|
|
|
return null
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _check_prop_pursuit_arrival() -> void:
|
|
|
|
|
var prop: PropBlock = _resolve_pursuit_prop(_pursuit_prop_id)
|
|
|
|
|
if prop == null or _locomotion == null or _rig == null:
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
return
|
|
|
|
|
var projected: Vector2 = _locomotion.project_target_to_navigation(prop.global_position + _pursuit_offset)
|
|
|
|
|
if not projected.is_finite():
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
return
|
|
|
|
|
var actor_feet: Vector2 = _rig.global_position - _locomotion.get_foot_offset()
|
|
|
|
|
if actor_feet.distance_to(projected) <= 28.0 and actor_feet.distance_to(prop.global_position) <= PROP_INTERACTION_REACH:
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"succeeded")
|
|
|
|
|
return
|
|
|
|
|
if actor_feet.distance_to(projected) > 28.0:
|
|
|
|
|
var move_style: String = ACTION_REGISTRY.checked_style(String(_current_run_item.get("action", {}).get("style", ACTION_REGISTRY.WALK)))
|
|
|
|
|
var restarted: bool = _locomotion.retarget_walk(projected) if _locomotion.is_walking() else _locomotion.move_to(projected, move_style)
|
|
|
|
|
if restarted:
|
|
|
|
|
_pursuit_approach = projected
|
|
|
|
|
_pursuit_last_prop_position = prop.global_position
|
|
|
|
|
_pursuit_retarget_elapsed = 0.0
|
|
|
|
|
_action_phase = ActionPhase.WALKING
|
|
|
|
|
return
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
_finish_action(&"failed")
|
|
|
|
|
return
|
|
|
|
|
_locomotion.cancel_walk(false)
|
|
|
|
|
_pursuit_approach = projected
|
|
|
|
|
_pursuit_last_prop_position = prop.global_position
|
|
|
|
|
_pursuit_retarget_elapsed = 0.0
|
|
|
|
|
_action_phase = ActionPhase.PURSUIT_WAIT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _end_prop_pursuit() -> void:
|
|
|
|
|
_pursuit_action_id = ""
|
|
|
|
|
_pursuit_prop_id = ""
|
|
|
|
|
_pursuit_offset = Vector2.ZERO
|
|
|
|
|
_pursuit_last_prop_position = Vector2.ZERO
|
|
|
|
|
_pursuit_approach = Vector2.INF
|
|
|
|
|
_pursuit_elapsed = 0.0
|
|
|
|
|
_pursuit_retarget_elapsed = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _finish_action(outcome: StringName = &"succeeded") -> 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))
|
|
|
|
|
var stable_id: String = String(_current_run_item.get("stable_action_id", _current_stable_action_id))
|
|
|
|
|
if stable_id == _pursuit_action_id:
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
if action_id >= 0:
|
|
|
|
|
_completed_ids[action_id] = true
|
|
|
|
|
index = _index_for_action_id(action_id) if action_id >= 0 else -1
|
|
|
|
|
if outcome == &"cancelled":
|
|
|
|
|
_interrupted_ids[action_id] = true
|
|
|
|
|
else:
|
|
|
|
|
_completed_ids[action_id] = true
|
|
|
|
|
if not stable_id.is_empty():
|
|
|
|
|
_stable_status[stable_id] = outcome
|
|
|
|
|
_parallel_active_numeric_ids.erase(action_id)
|
|
|
|
|
planned_action_ended.emit(stable_id, outcome, int(_current_run_item.get("step_index", _current_step_index)))
|
|
|
|
|
index = _flat_index_for_action(stable_id) if not stable_id.is_empty() else (_index_for_action_id(action_id) if action_id >= 0 else -1)
|
|
|
|
|
_action_phase = ActionPhase.NONE
|
|
|
|
|
if not finished.is_empty():
|
|
|
|
|
action_finished.emit(finished, index)
|
|
|
|
|
_current_run_item.clear()
|
|
|
|
|
_current_index = -1
|
|
|
|
|
_current_action_id = -1
|
|
|
|
|
if not finished.is_empty():
|
|
|
|
|
action_finished.emit(finished, index)
|
|
|
|
|
_current_stable_action_id = ""
|
|
|
|
|
if not _active_step_entry.is_empty():
|
|
|
|
|
_group_primary_done = true
|
|
|
|
|
if _group_say_barrier and not _group_say_action_id.is_empty():
|
|
|
|
|
_action_phase = ActionPhase.GROUP_BARRIER
|
|
|
|
|
else:
|
|
|
|
|
_complete_active_step()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _cancel_current_primary(notify: bool = false) -> void:
|
|
|
|
|
var stable_id: String = String(_current_run_item.get("stable_action_id", _current_stable_action_id))
|
|
|
|
|
var step_index: int = int(_current_run_item.get("step_index", _current_step_index))
|
|
|
|
|
var numeric_id: int = int(_current_run_item.get("action_id", _current_action_id))
|
|
|
|
|
if stable_id == _pursuit_action_id:
|
|
|
|
|
_end_prop_pursuit()
|
|
|
|
|
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 not stable_id.is_empty():
|
|
|
|
|
_stable_status[stable_id] = &"cancelled"
|
|
|
|
|
_parallel_active_numeric_ids.erase(numeric_id)
|
|
|
|
|
_interrupted_ids[numeric_id] = true
|
|
|
|
|
planned_action_ended.emit(stable_id, &"cancelled", step_index)
|
|
|
|
|
if _locomotion != null:
|
|
|
|
|
_locomotion.cancel_walk(not _is_blocked())
|
|
|
|
|
if _rig != null:
|
|
|
|
@@ -484,6 +986,7 @@ func _cancel_current_primary(notify: bool = false) -> void:
|
|
|
|
|
_current_run_item.clear()
|
|
|
|
|
_current_index = -1
|
|
|
|
|
_current_action_id = -1
|
|
|
|
|
_current_stable_action_id = ""
|
|
|
|
|
if _overlay_speech.is_empty() and _speech_bubble != null and is_instance_valid(_speech_bubble):
|
|
|
|
|
_speech_bubble.hide_bubble()
|
|
|
|
|
|
|
|
|
@@ -502,13 +1005,25 @@ 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())
|
|
|
|
|
_stable_status.clear()
|
|
|
|
|
_parallel_active_numeric_ids.clear()
|
|
|
|
|
for action: Dictionary in _authored_queue:
|
|
|
|
|
_stable_status[String(action.get("action_id", ""))] = &"pending"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 _runtime_step_item(step_index: int, step: Dictionary) -> Dictionary:
|
|
|
|
|
var actions: Array[Dictionary] = []
|
|
|
|
|
var stable_ids: Array[String] = []
|
|
|
|
|
var numeric_ids: Array[int] = []
|
|
|
|
|
for raw_action: Variant in step.get("actions", []):
|
|
|
|
|
if not raw_action is Dictionary:
|
|
|
|
|
continue
|
|
|
|
|
var action: Dictionary = raw_action as Dictionary
|
|
|
|
|
var stable_id: String = String(action.get("action_id", ""))
|
|
|
|
|
actions.append(action.duplicate(true))
|
|
|
|
|
stable_ids.append(stable_id)
|
|
|
|
|
numeric_ids.append(_numeric_for_stable_id(stable_id))
|
|
|
|
|
return {"step_index": step_index, "actions": actions, "stable_ids": stable_ids, "numeric_ids": numeric_ids, "reactive": false}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _rebuild_pending_runtime_queue() -> void:
|
|
|
|
@@ -518,11 +1033,19 @@ func _rebuild_pending_runtime_queue() -> void:
|
|
|
|
|
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))
|
|
|
|
|
for step_index: int in _authored_steps.size():
|
|
|
|
|
var step: Dictionary = _authored_steps[step_index]
|
|
|
|
|
var all_pending: bool = true
|
|
|
|
|
for raw_action: Variant in step.get("actions", []):
|
|
|
|
|
if not raw_action is Dictionary:
|
|
|
|
|
all_pending = false
|
|
|
|
|
break
|
|
|
|
|
var status: StringName = _status_for_stable_id(String((raw_action as Dictionary).get("action_id", "")))
|
|
|
|
|
if status not in [&"pending", &"not_started"]:
|
|
|
|
|
all_pending = false
|
|
|
|
|
break
|
|
|
|
|
if all_pending:
|
|
|
|
|
rebuilt.append(_runtime_step_item(step_index, step))
|
|
|
|
|
_run_queue = retained_reactive + rebuilt
|
|
|
|
|
_run_cursor = 0
|
|
|
|
|
if not _run_queue.is_empty() and _session_started:
|
|
|
|
@@ -545,15 +1068,14 @@ func _index_for_action_id(action_id: int) -> int:
|
|
|
|
|
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
|
|
|
|
|
var stable_id: String = _stable_for_numeric_id(action_id)
|
|
|
|
|
var status: StringName = _status_for_stable_id(stable_id) if not stable_id.is_empty() else &"not_started"
|
|
|
|
|
match status:
|
|
|
|
|
&"succeeded", &"failed": return QueueStatus.COMPLETED
|
|
|
|
|
&"cancelled": return QueueStatus.INTERRUPTED
|
|
|
|
|
&"active": return QueueStatus.ACTIVE
|
|
|
|
|
&"pending": return QueueStatus.PENDING
|
|
|
|
|
_: return QueueStatus.NOT_STARTED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _is_blocked() -> bool:
|
|
|
|
@@ -565,6 +1087,7 @@ func _update_speech(delta: float) -> void:
|
|
|
|
|
_overlay_speech["elapsed"] = float(_overlay_speech.get("elapsed", 0.0)) + delta
|
|
|
|
|
if float(_overlay_speech.get("elapsed", 0.0)) >= float(_overlay_speech.get("duration", 0.0)):
|
|
|
|
|
_overlay_speech.clear()
|
|
|
|
|
_finish_group_speech()
|
|
|
|
|
speech_finished.emit()
|
|
|
|
|
elif not _primary_speech.is_empty() and not _is_blocked():
|
|
|
|
|
_primary_speech["elapsed"] = float(_primary_speech.get("elapsed", 0.0)) + delta
|
|
|
|
@@ -578,6 +1101,25 @@ func _update_speech(delta: float) -> void:
|
|
|
|
|
_show_visible_speech()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _finish_group_speech() -> void:
|
|
|
|
|
if _group_say_action_id.is_empty():
|
|
|
|
|
return
|
|
|
|
|
var action_id: String = _group_say_action_id
|
|
|
|
|
var numeric_id: int = _group_say_numeric_id
|
|
|
|
|
var step_index: int = _group_say_step_index
|
|
|
|
|
var action: Dictionary = _group_say_action.duplicate(true)
|
|
|
|
|
_stable_status[action_id] = &"succeeded"
|
|
|
|
|
_completed_ids[numeric_id] = true
|
|
|
|
|
_parallel_active_numeric_ids.erase(numeric_id)
|
|
|
|
|
planned_action_ended.emit(action_id, &"succeeded", step_index)
|
|
|
|
|
action_finished.emit(action, _flat_index_for_action(action_id))
|
|
|
|
|
_group_say_action_id = ""
|
|
|
|
|
_group_say_numeric_id = -1
|
|
|
|
|
_group_say_step_index = -1
|
|
|
|
|
_group_say_action.clear()
|
|
|
|
|
_group_say_barrier = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _show_visible_speech() -> void:
|
|
|
|
|
if _rig == null:
|
|
|
|
|
return
|
|
|
|
|