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:
@@ -300,6 +300,43 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated
|
||||
`set_ragdoll` calls are idempotent. `is_in_ragdoll()` stays
|
||||
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman").
|
||||
- **Phase 3a director functionality:** adds navigation/walking, speech, an action queue, and a
|
||||
queue-runner state machine. Enums `RunnerState { IDLE, EXECUTING }` and `ActionPhase { NONE,
|
||||
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`
|
||||
(feet → root; matches `StageSpawner.STICKMAN_FOOT_OFFSET`), `NAV_AGENT_LOCAL_POS := (0, 385)`
|
||||
(`== -FOOT_OFFSET`), `ARRIVE_DISTANCE` / `NAV_PATH_DESIRED_DISTANCE` / `NAV_TARGET_DESIRED_DISTANCE`
|
||||
/ `SPEECH_BUBBLE_OFFSET`. New signals `arrived`, `action_started(action, index)`,
|
||||
`action_finished(action, index)`, `queue_finished` (on completion, not stop), `queue_changed`
|
||||
(any queue mutation), `speech_finished`. Exports `walk_speed` (300.0). Public API: `walk_to(
|
||||
target, speed = -1.0)` (feet/ground destination; no-op unless `state == ANIMATED`), `is_walking()`,
|
||||
`speak(text, duration)` (lazily creates a `SpeechBubble` child at `SPEECH_BUBBLE_OFFSET`, auto-hides
|
||||
+ emits `speech_finished`), the queue API `queue_action` / `clear_queue` / `get_queue` /
|
||||
`remove_action` / `insert_action` / `queue_size` (all mutations emit `queue_changed`), and the
|
||||
runner API `start_queue` / `stop_queue` / `is_queue_running`. `_ready()` builds a
|
||||
`NavigationAgent2D` child at `NAV_AGENT_LOCAL_POS` (feet, on the ground-level nav mesh;
|
||||
`avoidance_enabled = false`, `max_speed = walk_speed`, shared default nav map layer 1). A
|
||||
`_physics_process` ordering of `_track_momentum` → `_update_rest_detection` → `_update_walking`
|
||||
→ `_update_speech` → `_update_runner` drives the runner state machine per `ActionPhase`
|
||||
(`walk_to` → wait for `_walk_done`; `speak` → wait for `!_speech_active`; `wait` → `_phase_timer`
|
||||
countdown; `ragdoll` → `set_ragdoll(true)` then wait `is_ragdoll_at_rest()`; `recover` →
|
||||
`request_recovery()` then wait `state == ANIMATED`). `is_ragdoll_at_rest()` is a new public
|
||||
query exposing the rest result **regardless of `auto_recover`** (the runner polls it; auto-recovery
|
||||
logic is unchanged). `_enter_ragdoll()` additionally calls `_cancel_walking()` (no stale walk/path
|
||||
state) and resets `_ragdoll_at_rest = false`. **Walk fix (2026-08-29, hybrid policy):**
|
||||
`_update_walking` defers all nav reads until `NavigationServer2D.map_get_iteration_id(...) != 0`
|
||||
(map-sync guard), forces the path query via `get_next_path_position()` **before** any empty-path /
|
||||
finished check (the read-only `get_current_navigation_path()` alone never triggers a
|
||||
repath), then branches on `is_target_reachable()`: an **on-mesh target** follows the nav path
|
||||
(`_walk_mode = "nav"`), while an **off-mesh/unreachable target** switches to **direct
|
||||
straight-line steering** toward the clicked waypoint (`_walk_mode = "direct"`, root target =
|
||||
waypoint + `FOOT_OFFSET`) — a supported case with **no** `push_warning` (the old "warn + finish
|
||||
in place" policy was replaced; the rig never stands still at a waypoint). There is **no**
|
||||
`_walk_path_grace` variable (the map-sync guard + forced path query replace it) and **no
|
||||
unreachable warning**; the debug trace now carries a `mode=nav|direct` field. Off-by-default
|
||||
diagnostics: `DEBUG_WALK` + `_walk_dbg()` (rig) and `DEBUG_STAGE` + `_stage_dbg()` (sandbox_stage).
|
||||
Walking is kinematic
|
||||
(`global_position.move_toward`); movement composes with the `walk_left`/`walk_right` in-place limb
|
||||
animations (each has a discrete `.:facing_profile` track).
|
||||
- `scripts/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static
|
||||
factory** and the **runtime entry point** (Phase 9, **not used by the editor**) that turns a
|
||||
`.stk` file into a live, rigged `master_rig.tscn` instance:
|
||||
@@ -562,6 +599,44 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
Grid/Snap/Size controls, status label. The build controls (spawn palette + Grid/Snap/Size) are
|
||||
hidden in PLAY via `_set_build_controls_visible()`, called from `_enter_edit_mode()` /
|
||||
`_enter_play_mode()` (the mode toggle and status label stay visible).
|
||||
- **Phase 3a director tool:** adds a **"Direct"** palette toggle button (mutually exclusive with
|
||||
placement) and a `PopupMenu` (`_action_popup`, items `Walk To`/`Speak`/`Wait`/`Ragdoll`/`Recover`,
|
||||
ids `ACT_WALK`…`ACT_RECOVER`) opened by clicking a stickman (`_handle_direct_click`); speak/wait
|
||||
`AcceptDialog`s append `speak`/`wait` actions; `Walk To` enters a pending target-capture mode whose
|
||||
next stage click appends `{"type":"walk_to","target":world_pos}` and which **Esc** cancels (Esc
|
||||
priority: pending target → exit direct mode → existing placement clears). Builds a code-built
|
||||
`NavigationRegion2D` (`_build_navigation()`, child of the stage **not** `World` so it is never
|
||||
hit-tested) carrying a procedural `NavigationPolygon` from per-`TerrainBlock` convex decomposition
|
||||
(`_rebake_navigation()`, `Geometry2D.decompose_polygon_in_convex` + fan triangulation, world-space
|
||||
via `block.transform * p`); a `_nav_dirty` flag re-bakes once per frame (`_process`) on terrain
|
||||
place / move / rotate (`transform_committed`) / delete. Instantiates a `StageDirectorVisuals`
|
||||
overlay (`_build_director_visuals()`). **Play mode change (D3):** `_enter_play_mode()` no longer
|
||||
auto-ragdolls stickmen — it now sets each rig `auto_recover = false` and calls `start_queue()`;
|
||||
`ragdoll`/`recover` are explicit queue actions; props still unfreeze. `_enter_edit_mode()`
|
||||
`stop_queue()`s then `snap_to_standing()`s stickmen and re-enables the visuals. Wires
|
||||
`node.queue_changed → _director_visuals.mark_dirty` when a stickman is placed; `object_deleted` →
|
||||
`mark_dirty()`. `_set_build_controls_visible()` also hides the Direct button in PLAY.
|
||||
- `scripts/stickman_speech_bubble.gd` — `class_name SpeechBubble`, `extends Node2D`; a **world-space
|
||||
speech bubble** drawn in `_draw()` (Phase 3a, **not used by the editor**). Child of a `StickmanRig`
|
||||
at `SPEECH_BUBBLE_OFFSET` (above the head), so it follows the figure and scales with the camera.
|
||||
Consts `FONT_SIZE`/`PADDING`/`TAIL_HEIGHT`/`MAX_WIDTH`/`BG_COLOR`/`BORDER_COLOR`/`TEXT_COLOR`.
|
||||
Measures text via `ThemeDB.fallback_font.get_string_size(...)`, draws a rounded-rect background +
|
||||
a downward tail triangle centered on the rig-local origin, then `draw_string(...)`; `visible = false`
|
||||
by default, no hit-testing. Public API: `show_text(text)` (store, `visible = true`, `queue_redraw()`),
|
||||
`hide_bubble()`. Driven by `StickmanRig.speak()`; the rig owns hiding + the `speech_finished` signal.
|
||||
- `scripts/stage_director_visuals.gd` — `class_name StageDirectorVisuals`, `extends Node2D`; the
|
||||
**Edit-mode director overlay** (Phase 3a, **not used by the editor**), mirroring `StageGrid`/
|
||||
`StageGizmos`. State `camera`/`world`/`enabled`/`_dirty`; public `set_enabled(value)` /
|
||||
`mark_dirty()`. `_process()` redraws on a dirty flag; `_draw()` reads each rig's queue via
|
||||
`_collect_rigs()` (direct `World` children filtered `is StickmanRig`). Per action in order:
|
||||
`walk_to` → a blue waypoint dot (`WAYPOINT_RADIUS_PX / _zoom()`) with white outline + order number at
|
||||
`action["target"]`; dashed connectors (`draw_dashed_line`, `DASH_*` / `_zoom()`) between consecutive
|
||||
dots (and from the rig's current feet position to the first dot); non-walk actions (`speak`/`wait`/
|
||||
`ragdoll`/`recover`) → a badge (speech bubble / clock / X / up-arrow glyph + order number) anchored
|
||||
at the **stickman's position at that point in the sequence** — derived by simulating the queue
|
||||
(start at rig feet + `FOOT_OFFSET`; each `walk_to` advances the anchor; non-walk anchors at the
|
||||
current position), consecutive badges stacking `(0, -28)/zoom`. All sizes divided by `_zoom()` so
|
||||
markers stay screen-constant; pure `_draw()`, no hit-testing; hidden in PLAY via `set_enabled(false)`.
|
||||
- `scripts/stage_spawner.gd` — `class_name StageSpawner`, `extends RefCounted`; registry-driven
|
||||
spawner (Phase 2). A `_registry: Array[Dictionary]` maps ids to terrain/prop/stickman templates;
|
||||
adding a type = appending an entry (no hard-coded id `match`). Reuses `TerrainUtils.spawn_block`,
|
||||
|
||||
Reference in New Issue
Block a user