Add Phase 3a Core Director Functionality
- Introduced `PHASE_3a_CORE_DIRECTOR.md` detailing the core functionality for directors, including navigation, action queue, UI, waypoint visualization, and action execution. - Implemented `StageDirectorVisuals` for drawing stickman action queues in edit mode, including waypoints and action badges. - Created `SpeechBubble` class for displaying speech bubbles above stickmen, with customizable text and styling.
This commit is contained in:
+422
-9
@@ -25,6 +25,12 @@ enum BendDirection { NORMAL, INVERTED }
|
||||
## IK targets to the standing pose before returning to ANIMATED.
|
||||
enum RigState { ANIMATED, RAGDOLL, RECOVERING }
|
||||
|
||||
## Director action-runner execution state (Phase 3a).
|
||||
enum RunnerState { IDLE, EXECUTING }
|
||||
|
||||
## Which kind of queued action is currently executing (Phase 3a).
|
||||
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -142,6 +148,33 @@ const IK_TARGET_PATHS: Dictionary = {
|
||||
"Right_Leg": "IK_Targets/Right_Leg",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Director / navigation constants (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Feet -> root translation (a ground point -> the hip/root position).
|
||||
const FOOT_OFFSET := Vector2(0.0, -385.0)
|
||||
## The navigation agent sits at the feet, on the nav mesh.
|
||||
const NAV_AGENT_LOCAL_POS := Vector2(0.0, 385.0)
|
||||
## Fallback arrival distance (px) to the root destination.
|
||||
const ARRIVE_DISTANCE := 8.0
|
||||
const NAV_PATH_DESIRED_DISTANCE := 8.0
|
||||
const NAV_TARGET_DESIRED_DISTANCE := 12.0
|
||||
## Rig-local anchor for the speech bubble, above the head.
|
||||
const SPEECH_BUBBLE_OFFSET := Vector2(0.0, -640.0)
|
||||
|
||||
## Debug gate for the Phase 3a walk/runner trace. Ship OFF.
|
||||
const DEBUG_WALK := false
|
||||
|
||||
## Prints a `[walk] `-prefixed message only when DEBUG_WALK is on.
|
||||
func _walk_dbg(msg: String) -> void:
|
||||
if DEBUG_WALK:
|
||||
print("[walk] ", msg)
|
||||
|
||||
## Preloaded (not a class_name type) so this script compiles even when the
|
||||
## editor's global class cache is stale.
|
||||
const SPEECH_BUBBLE_SCRIPT := preload("res://scripts/stickman_speech_bubble.gd")
|
||||
|
||||
## Ragdoll body definitions, ordered parent-before-child. `node_path` is
|
||||
## Skeleton2D-relative for bones and rig-root-relative for the head visual.
|
||||
## `kind` is "bone" (capsule along a Bone2D) or "visual" (circle at Body/Head).
|
||||
@@ -216,6 +249,10 @@ const RAGDOLL_JOINTS: Array[Dictionary] = [
|
||||
## ragdoll stays down until request_recovery() is called manually.
|
||||
@export var auto_recover: bool = true
|
||||
|
||||
@export_group("Director")
|
||||
## Walk speed (px/s) used by walk_to when no per-action speed is given.
|
||||
@export var walk_speed: float = 300.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -231,6 +268,17 @@ signal bend_flag_changed(joint: String, flipped: bool)
|
||||
## the RigState enum value.
|
||||
signal state_changed(new_state: int)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Director signals (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
signal arrived # walk_to reached its destination
|
||||
signal action_started(action: Dictionary, index: int)
|
||||
signal action_finished(action: Dictionary, index: int)
|
||||
signal queue_finished # queue ran to completion (not on stop)
|
||||
signal queue_changed # any queue mutation
|
||||
signal speech_finished # bubble auto-hid after speak()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -265,6 +313,38 @@ var _stabilize_timer: float = 0.0
|
||||
var _captured_pose: Dictionary = {} # { String : {pos, rot, half} } (rig-local)
|
||||
var _stand_up_tween: Tween = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Director / navigation state (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var action_queue: Array[Dictionary] = []
|
||||
|
||||
var _runner_state: RunnerState = RunnerState.IDLE
|
||||
var _action_phase: ActionPhase = ActionPhase.NONE
|
||||
var _current_index: int = -1
|
||||
var _stop_requested: bool = false
|
||||
|
||||
var _nav_agent: NavigationAgent2D = null
|
||||
## NavigationAgent2D is a plain Node (not Node2D): it has no `position`; it
|
||||
## derives its agent position from its parent Node2D's global position. This
|
||||
## anchor sits at the feet (NAV_AGENT_LOCAL_POS) so the agent paths from
|
||||
## ground level.
|
||||
var _nav_anchor: Node2D = null
|
||||
|
||||
var _walking: bool = false
|
||||
var _walk_target_feet: Vector2 = Vector2.ZERO
|
||||
var _walk_speed_current: float = 300.0
|
||||
var _walk_mode: String = "nav" # "nav" (follow mesh) | "direct" (off-mesh straight line)
|
||||
var _walk_done: bool = false
|
||||
|
||||
var _ragdoll_at_rest: bool = false
|
||||
|
||||
var _speech_bubble = null # SpeechBubble (preloaded script)
|
||||
var _speech_active: bool = false
|
||||
var _speech_time_left: float = 0.0
|
||||
|
||||
var _phase_timer: float = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -305,6 +385,25 @@ func _ready() -> void:
|
||||
# setters only stored values).
|
||||
_nodes_ready = true
|
||||
_apply_profile()
|
||||
|
||||
# Build the navigation agent at the feet (Phase 3a). It shares the default
|
||||
# navigation map/layer with the stage's NavigationRegion2D.
|
||||
# NavigationAgent2D is a plain Node: it has no `position`; it derives its
|
||||
# agent position from its parent Node2D's global position, so we anchor it
|
||||
# under a Node2D placed at the feet.
|
||||
_nav_anchor = Node2D.new()
|
||||
_nav_anchor.name = "NavigationAgentAnchor"
|
||||
_nav_anchor.position = NAV_AGENT_LOCAL_POS
|
||||
add_child(_nav_anchor)
|
||||
_nav_agent = NavigationAgent2D.new()
|
||||
_nav_agent.name = "NavigationAgent2D"
|
||||
_nav_agent.path_desired_distance = NAV_PATH_DESIRED_DISTANCE
|
||||
_nav_agent.target_desired_distance = NAV_TARGET_DESIRED_DISTANCE
|
||||
_nav_agent.path_max_distance = 100.0
|
||||
_nav_agent.max_speed = walk_speed
|
||||
_nav_agent.avoidance_enabled = false
|
||||
_nav_anchor.add_child(_nav_agent)
|
||||
|
||||
_prev_global_pos = global_position
|
||||
_prev_global_rot = global_rotation
|
||||
|
||||
@@ -312,6 +411,9 @@ func _ready() -> void:
|
||||
func _physics_process(delta: float) -> void:
|
||||
_track_momentum(delta)
|
||||
_update_rest_detection(delta)
|
||||
_update_walking(delta)
|
||||
_update_speech(delta)
|
||||
_update_runner(delta)
|
||||
|
||||
|
||||
func _track_momentum(delta: float) -> void:
|
||||
@@ -340,6 +442,9 @@ func _update_rest_detection(delta: float) -> void:
|
||||
_rest_timer = 0.0
|
||||
_stabilize_timer = 0.0
|
||||
return
|
||||
# Publish rest regardless of auto_recover (the director runner polls it).
|
||||
if not _ragdoll_at_rest:
|
||||
_ragdoll_at_rest = true
|
||||
if not auto_recover:
|
||||
_rest_timer = 0.0
|
||||
return
|
||||
@@ -446,15 +551,7 @@ func snap_to_standing() -> void:
|
||||
_destroy_ragdoll()
|
||||
else:
|
||||
_cancel_recovery()
|
||||
# Set the IK targets directly to the standing pose (no tween).
|
||||
for marker_name: String in STAND_POSE:
|
||||
var marker := _get_ik_marker(marker_name)
|
||||
if marker == null:
|
||||
continue
|
||||
var target: Dictionary = STAND_POSE[marker_name]
|
||||
marker.position = target.get("pos", marker.position)
|
||||
if marker_name == "Torso":
|
||||
marker.rotation = target.get("rot", marker.rotation)
|
||||
_restore_standing_markers()
|
||||
# Re-show the kinematic puppet and re-enable IK.
|
||||
if _skeleton != null and is_instance_valid(_skeleton) and _skeleton.modification_stack != null:
|
||||
_skeleton.modification_stack.enabled = true
|
||||
@@ -465,6 +562,18 @@ func snap_to_standing() -> void:
|
||||
state_changed.emit(int(state))
|
||||
|
||||
|
||||
## Writes STAND_POSE onto the 6 IK-target markers (no tween, no state change).
|
||||
func _restore_standing_markers() -> void:
|
||||
for marker_name: String in STAND_POSE:
|
||||
var marker := _get_ik_marker(marker_name)
|
||||
if marker == null:
|
||||
continue
|
||||
var target: Dictionary = STAND_POSE[marker_name]
|
||||
marker.position = target.get("pos", marker.position)
|
||||
if marker_name == "Torso":
|
||||
marker.rotation = target.get("rot", marker.rotation)
|
||||
|
||||
|
||||
## Applies the same velocity delta to every ragdoll body via a mass-scaled
|
||||
## central impulse, preserving the ragdoll's internal structure. No-op outside
|
||||
## RAGDOLL mode. Used by the physics harness "Knock Up" button.
|
||||
@@ -570,6 +679,8 @@ func _enter_ragdoll() -> void:
|
||||
if _skeleton == null or _body_container == null:
|
||||
push_warning("StickmanRig: cannot enter ragdoll; missing rig nodes.")
|
||||
return
|
||||
_cancel_walking()
|
||||
_ragdoll_at_rest = false
|
||||
# Instant handoff: stop the player without resetting it (keep_state) and
|
||||
# build the ragdoll from the CURRENT solved bone positions while the IK
|
||||
# stack is still enabled (disabling it first would revert the bones to the
|
||||
@@ -911,3 +1022,305 @@ func _destroy_ragdoll() -> void:
|
||||
_ragdoll_root.queue_free()
|
||||
_ragdoll_root = null
|
||||
_ragdoll_bodies.clear()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Navigation / walking (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Start walking so the feet land at `target` (world/ground space). `speed <= 0`
|
||||
## uses walk_speed. No-op (push_warning) unless state == ANIMATED.
|
||||
func walk_to(target: Vector2, speed: float = -1.0) -> void:
|
||||
if state != RigState.ANIMATED:
|
||||
push_warning("StickmanRig: walk_to ignored; not ANIMATED.")
|
||||
return
|
||||
_walk_target_feet = target
|
||||
_walk_speed_current = speed if speed > 0.0 else walk_speed
|
||||
_nav_agent.max_speed = _walk_speed_current
|
||||
_nav_agent.target_position = target
|
||||
|
||||
_walk_dbg("walk_to target=(%.1f, %.1f) speed=%.1f root=(%.1f, %.1f) feet=(%.1f, %.1f)" % [
|
||||
target.x, target.y, _walk_speed_current,
|
||||
global_position.x, global_position.y,
|
||||
_nav_anchor.global_position.x, _nav_anchor.global_position.y,
|
||||
])
|
||||
|
||||
var dx := target.x - global_position.x
|
||||
var anim_name: String
|
||||
if dx < -0.5:
|
||||
set_facing_profile(FacingProfile.LEFT)
|
||||
anim_name = "walk_left"
|
||||
elif dx > 0.5:
|
||||
set_facing_profile(FacingProfile.RIGHT)
|
||||
anim_name = "walk_right"
|
||||
else:
|
||||
anim_name = "walk_right"
|
||||
if _anim_player != null and is_instance_valid(_anim_player) and _anim_player.has_animation(anim_name):
|
||||
_anim_player.play(anim_name)
|
||||
_walking = true
|
||||
_walk_done = false
|
||||
|
||||
|
||||
func is_walking() -> bool:
|
||||
return _walking
|
||||
|
||||
|
||||
func _update_walking(delta: float) -> void:
|
||||
if not _walking:
|
||||
return
|
||||
if state != RigState.ANIMATED:
|
||||
_cancel_walking()
|
||||
return
|
||||
# Defer all nav reads until the map has actually synchronized. An unsynced
|
||||
# agent reports an empty, finished path (map iteration id == 0), which would
|
||||
# otherwise end the walk after a single move_toward step (~5 px).
|
||||
if NavigationServer2D.map_get_iteration_id(_nav_agent.get_navigation_map()) == 0:
|
||||
_walk_dbg("sync pending")
|
||||
return
|
||||
# Ask the agent for its next waypoint FIRST. This forces the agent's internal
|
||||
# path update (_update_navigation), which re-queries the map whenever the
|
||||
# stored path is empty (set_target_position resets it via _request_repath).
|
||||
# The read-only get_current_navigation_path() accessor alone never triggers a
|
||||
# repath, so checking it directly would leave the path empty forever.
|
||||
var next_feet := _nav_agent.get_next_path_position()
|
||||
var root_target: Vector2
|
||||
if _nav_agent.is_target_reachable():
|
||||
# NAV branch: the target lies on the mesh — follow the path to it.
|
||||
_walk_mode = "nav"
|
||||
if _nav_agent.is_navigation_finished():
|
||||
_finish_walk("finished")
|
||||
return
|
||||
root_target = next_feet + FOOT_OFFSET
|
||||
else:
|
||||
# DIRECT branch: an off-mesh waypoint is now a normal, supported case —
|
||||
# steer straight at the clicked point, ignoring the nav mesh.
|
||||
if _walk_mode != "direct":
|
||||
_walk_dbg("off-mesh waypoint: switching to direct steering")
|
||||
_walk_mode = "direct"
|
||||
root_target = _walk_target_feet + FOOT_OFFSET
|
||||
global_position = global_position.move_toward(root_target, _walk_speed_current * delta)
|
||||
if global_position.distance_to(_walk_target_feet + FOOT_OFFSET) <= ARRIVE_DISTANCE:
|
||||
_finish_walk("arrive")
|
||||
return
|
||||
_walk_dbg("frame=%d idx=%d mode=%s root=(%.1f, %.1f) feet=(%.1f, %.1f) target=(%.1f, %.1f) dist=%.1f finished=%s reachable=%s final=(%.1f, %.1f) pts=%d next=(%.1f, %.1f) map_iter=%d" % [
|
||||
Engine.get_physics_frames(),
|
||||
_current_index,
|
||||
_walk_mode,
|
||||
global_position.x, global_position.y,
|
||||
_nav_anchor.global_position.x, _nav_anchor.global_position.y,
|
||||
_walk_target_feet.x, _walk_target_feet.y,
|
||||
global_position.distance_to(_walk_target_feet + FOOT_OFFSET),
|
||||
str(_nav_agent.is_navigation_finished()),
|
||||
str(_nav_agent.is_target_reachable()),
|
||||
_nav_agent.get_final_position().x, _nav_agent.get_final_position().y,
|
||||
_nav_agent.get_current_navigation_path().size(),
|
||||
next_feet.x, next_feet.y,
|
||||
NavigationServer2D.map_get_iteration_id(_nav_agent.get_navigation_map()),
|
||||
])
|
||||
|
||||
|
||||
func _finish_walk(reason: String = "") -> void:
|
||||
if DEBUG_WALK:
|
||||
var map_iter := -1
|
||||
if _nav_agent != null:
|
||||
map_iter = NavigationServer2D.map_get_iteration_id(_nav_agent.get_navigation_map())
|
||||
_walk_dbg("finish reason=%s root=(%.1f, %.1f) dist_to_target=%.1f map_iter=%d" % [
|
||||
reason,
|
||||
global_position.x, global_position.y,
|
||||
global_position.distance_to(_walk_target_feet + FOOT_OFFSET),
|
||||
map_iter,
|
||||
])
|
||||
if _anim_player != null and is_instance_valid(_anim_player):
|
||||
_anim_player.stop()
|
||||
_restore_standing_markers()
|
||||
_walk_done = true
|
||||
_walking = false
|
||||
arrived.emit()
|
||||
|
||||
|
||||
func _cancel_walking() -> void:
|
||||
_walk_dbg("cancel (state=%s)" % RigState.keys()[state])
|
||||
if _anim_player != null and is_instance_valid(_anim_player):
|
||||
_anim_player.stop()
|
||||
if _nav_agent != null and _nav_anchor != null:
|
||||
_nav_agent.target_position = _nav_anchor.global_position
|
||||
_walking = false
|
||||
_walk_done = false
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Speech (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Show the speech bubble with `text` for `duration` seconds; auto-hides and
|
||||
## emits speech_finished. Lazily creates the SpeechBubble child on first use.
|
||||
func speak(text: String, duration: float) -> void:
|
||||
if _speech_bubble == null or not is_instance_valid(_speech_bubble):
|
||||
_speech_bubble = SPEECH_BUBBLE_SCRIPT.new()
|
||||
_speech_bubble.name = "SpeechBubble"
|
||||
_speech_bubble.position = SPEECH_BUBBLE_OFFSET
|
||||
add_child(_speech_bubble)
|
||||
_speech_bubble.show_text(text)
|
||||
_speech_active = true
|
||||
_speech_time_left = maxf(duration, 0.0)
|
||||
|
||||
|
||||
func _update_speech(delta: float) -> void:
|
||||
if not _speech_active:
|
||||
return
|
||||
_speech_time_left -= delta
|
||||
if _speech_time_left <= 0.0:
|
||||
_hide_speech()
|
||||
speech_finished.emit()
|
||||
|
||||
|
||||
func _hide_speech() -> void:
|
||||
_speech_active = false
|
||||
_speech_time_left = 0.0
|
||||
if _speech_bubble != null and is_instance_valid(_speech_bubble):
|
||||
_speech_bubble.hide_bubble()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Action queue (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func queue_action(action: Dictionary) -> void:
|
||||
action_queue.append(action)
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func clear_queue() -> void:
|
||||
action_queue.clear()
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func get_queue() -> Array[Dictionary]:
|
||||
return action_queue.duplicate()
|
||||
|
||||
|
||||
func remove_action(index: int) -> void:
|
||||
if index < 0 or index >= action_queue.size():
|
||||
push_warning("StickmanRig: remove_action index %d out of range." % index)
|
||||
return
|
||||
action_queue.remove_at(index)
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func insert_action(index: int, action: Dictionary) -> void:
|
||||
action_queue.insert(clampi(index, 0, action_queue.size()), action)
|
||||
queue_changed.emit()
|
||||
|
||||
|
||||
func queue_size() -> int:
|
||||
return action_queue.size()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runner state machine (Phase 3a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Start running the queue. Empty queue emits queue_finished immediately.
|
||||
func start_queue() -> void:
|
||||
if _runner_state == RunnerState.EXECUTING:
|
||||
return
|
||||
if DEBUG_WALK:
|
||||
print("[runner] start_queue size=%d" % action_queue.size())
|
||||
if action_queue.is_empty():
|
||||
queue_finished.emit()
|
||||
return
|
||||
_runner_state = RunnerState.EXECUTING
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_index = -1
|
||||
_stop_requested = false
|
||||
|
||||
|
||||
## Abort the current action and return to IDLE. Does not emit queue_finished.
|
||||
func stop_queue() -> void:
|
||||
_stop_requested = true
|
||||
_cancel_walking()
|
||||
_hide_speech()
|
||||
_runner_state = RunnerState.IDLE
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_index = -1
|
||||
|
||||
|
||||
func is_queue_running() -> bool:
|
||||
return _runner_state == RunnerState.EXECUTING
|
||||
|
||||
|
||||
func is_ragdoll_at_rest() -> bool:
|
||||
return _ragdoll_at_rest
|
||||
|
||||
|
||||
func _update_runner(delta: float) -> void:
|
||||
if _runner_state != RunnerState.EXECUTING:
|
||||
return
|
||||
match _action_phase:
|
||||
ActionPhase.NONE:
|
||||
_advance_to_next_action()
|
||||
ActionPhase.WALKING:
|
||||
if _walk_done:
|
||||
_finish_action()
|
||||
ActionPhase.SPEAKING:
|
||||
if not _speech_active:
|
||||
_finish_action()
|
||||
ActionPhase.WAITING:
|
||||
_phase_timer -= delta
|
||||
if _phase_timer <= 0.0:
|
||||
_finish_action()
|
||||
ActionPhase.RAGDOLLING:
|
||||
if is_ragdoll_at_rest():
|
||||
_finish_action()
|
||||
ActionPhase.RECOVERING:
|
||||
if state == RigState.ANIMATED:
|
||||
_finish_action()
|
||||
|
||||
|
||||
func _advance_to_next_action() -> void:
|
||||
_current_index += 1
|
||||
if _current_index >= action_queue.size():
|
||||
_runner_state = RunnerState.IDLE
|
||||
_action_phase = ActionPhase.NONE
|
||||
_current_index = -1
|
||||
queue_finished.emit()
|
||||
return
|
||||
var action := action_queue[_current_index]
|
||||
if DEBUG_WALK:
|
||||
print("[runner] start idx=%d type=%s" % [_current_index, String(action.get("type", ""))])
|
||||
action_started.emit(action, _current_index)
|
||||
_begin_action(action)
|
||||
|
||||
|
||||
func _begin_action(action: Dictionary) -> void:
|
||||
match String(action.get("type", "")):
|
||||
"walk_to":
|
||||
_action_phase = ActionPhase.WALKING
|
||||
_walk_done = false
|
||||
walk_to(action.get("target", Vector2.ZERO), float(action.get("speed", -1.0)))
|
||||
if not _walking:
|
||||
# walk_to self-guarded (e.g. not ANIMATED): complete immediately
|
||||
# so the runner never hangs.
|
||||
_walk_done = true
|
||||
"speak":
|
||||
_action_phase = ActionPhase.SPEAKING
|
||||
speak(String(action.get("text", "")), float(action.get("duration", 2.0)))
|
||||
"wait":
|
||||
_phase_timer = float(action.get("duration", 0.0))
|
||||
_action_phase = ActionPhase.WAITING
|
||||
"ragdoll":
|
||||
_action_phase = ActionPhase.RAGDOLLING
|
||||
set_ragdoll(true)
|
||||
"recover":
|
||||
_action_phase = ActionPhase.RECOVERING
|
||||
request_recovery()
|
||||
_:
|
||||
push_warning("StickmanRig: unknown action type '%s'; skipped." % String(action.get("type", "")))
|
||||
_finish_action()
|
||||
|
||||
|
||||
func _finish_action() -> void:
|
||||
if DEBUG_WALK and _current_index >= 0 and _current_index < action_queue.size():
|
||||
print("[runner] finish idx=%d type=%s" % [_current_index, String(action_queue[_current_index].get("type", ""))])
|
||||
if _current_index >= 0 and _current_index < action_queue.size():
|
||||
action_finished.emit(action_queue[_current_index], _current_index)
|
||||
_action_phase = ActionPhase.NONE
|
||||
|
||||
Reference in New Issue
Block a user