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:
@@ -444,7 +444,82 @@ Clicking a palette button enters **placement mode**, which shows a translucent *
|
||||
|
||||
**Status bar:** shows `Mode: EDIT/PLAY | Objects: N | Selected: <name or count>` and updates live on spawn, selection, deletion, and mode changes.
|
||||
|
||||
> **Extendability contract:** the spawner uses a `Dictionary` registry (no hard-coded `match` on ids), the `World` container accepts any `Node2D`, gizmos work on any object via `global_position`/`global_rotation`, and the root exposes `mode_changed` / `object_placed` / `object_selected` / `object_deselected` / `object_deleted` signals — all hooks for the future Action Queue, Trigger, and Save/Load phases. Ramps/stairs can be placed and props/ragdolls will slide on them, but stickmen do **not** autonomously walk up/down them yet (planned for a later phase with `NavigationAgent2D` + IK).
|
||||
> **Extendability contract:** the spawner uses a `Dictionary` registry (no hard-coded `match` on ids), the `World` container accepts any `Node2D`, gizmos work on any object via `global_position`/`global_rotation`, and the root exposes `mode_changed` / `object_placed` / `object_selected` / `object_deselected` / `object_deleted` signals — all hooks for the future Action Queue, Trigger, and Save/Load phases. Ramps/stairs can be placed and props/ragdolls will slide on them, and (Phase 3a) stickmen now walk up/down them via `NavigationAgent2D` — see §19.
|
||||
|
||||
### 19. Director Tool (Phase 3a)
|
||||
|
||||
The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's workspace: click a stickman, choose actions from a popup, see the script as waypoints/badges in Edit mode, then press **Play** to run every stickman's action queue. It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`.
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `res://scripts/stickman_rig.gd` | Extended with navigation/walking, speech, an action queue, and the queue runner state machine (see API below). |
|
||||
| `res://scripts/stickman_speech_bubble.gd` | `class_name SpeechBubble`, `extends Node2D` — a world-space speech bubble drawn via `_draw()` (`ThemeDB.fallback_font`), a child of the rig above the head. |
|
||||
| `res://scripts/stage_director_visuals.gd` | `class_name StageDirectorVisuals`, `extends Node2D` — Edit-mode director overlay (waypoint dots, dashed connectors, action badges, order numbers); hidden in Play. |
|
||||
| `res://scripts/sandbox_stage.gd` | Extended with the **Direct** palette button, the action popup + speak/wait dialogs, a code-built `NavigationRegion2D` re-baked on terrain edits, and Play mode now starting each stickman's queue. |
|
||||
|
||||
**Direct tool workflow (Edit):**
|
||||
|
||||
1. Press the **Direct** toggle button (mutually exclusive with palette placement). A status hint prompts "Click a stickman".
|
||||
2. **Left-click a stickman** → an action popup opens at the cursor with **Walk To / Speak / Wait / Ragdoll / Recover**.
|
||||
3. Choose an action:
|
||||
- **Walk To** → enters pending mode; the **next left-click on the stage** appends `{"type":"walk_to","target":click_pos}`. **Esc** cancels the pending target.
|
||||
- **Speak** → a text dialog (`AcceptDialog` + `LineEdit`); confirms append `{"type":"speak","text":...,"duration":2.0}`.
|
||||
- **Wait** → a duration dialog (`SpinBox`, 0.1–10 s); confirms append `{"type":"wait","duration":...}`.
|
||||
- **Ragdoll / Recover** → append `{"type":"ragdoll"}` / `{"type":"recover"}` immediately.
|
||||
4. Waypoints/badges update immediately (`queue_changed` → dirty flag). **Esc** exits Direct mode.
|
||||
|
||||
**Action queue semantics:**
|
||||
|
||||
- Each `StickmanRig` owns its own `action_queue: Array[Dictionary]`; index order = execution order = visual order (no separate ids). Unknown `type` → `push_warning` + the action is skipped (treated as completed).
|
||||
- Action shapes (the `type` key is the discriminator):
|
||||
|
||||
| type | required keys | optional keys |
|
||||
|---|---|---|
|
||||
| `"walk_to"` | `"target": Vector2` (feet destination) | `"speed": float` |
|
||||
| `"speak"` | `"text": String` | `"duration": float` |
|
||||
| `"wait"` | `"duration": float` | — |
|
||||
| `"ragdoll"` | — | — |
|
||||
| `"recover"` | — | — |
|
||||
|
||||
- Queues are **in-memory only** in 3a (no serialization; lost on scene reload).
|
||||
|
||||
**Waypoint & badge visuals (Edit only):** `StageDirectorVisuals` reads each rig's queue and, per action in order, draws a **blue waypoint dot** (white outline, order number) at each `walk_to` target, **dashed connectors** between consecutive dots (and from the rig's current feet position to the first dot), and **badges** (speech bubble / clock / X / up-arrow glyph + order number) for non-walk actions anchored to the **stickman's position at that point in the sequence** — the position derived by simulating the queue (start at the rig's feet; each `walk_to` advances the anchor; a non-walk action anchors at the position when it is reached). Consecutive badges at the same point stack upward. All sizes divide by the camera zoom so markers stay screen-constant. Pure `_draw()` — no hit-testing. Hidden in Play.
|
||||
|
||||
**Play execution:**
|
||||
|
||||
- **Play mode now runs the director script** — stickmen stay **ANIMATED** and each rig's `start_queue()` is called (previously they auto-ragdolled on Play). `ragdoll` / `recover` are now **explicit queue actions**; a stickman only falls when directed. Props still unfreeze and tumble (and can knock a *directed* ragdoll). `auto_recover = false` in Play (the director owns recovery).
|
||||
- On **return to Edit**: each stickman `stop_queue()` then `snap_to_standing()`, and the waypoint overlay is re-enabled.
|
||||
- Multiple stickmen act simultaneously and independently (per-rig queues + per-rig runners, no shared state).
|
||||
|
||||
**Nav-mesh behavior:**
|
||||
|
||||
- A code-built `NavigationRegion2D` (child of the stage, not `World`, so it is never hit-tested) carries a procedural `NavigationPolygon` generated by per-`TerrainBlock` convex decomposition (`Geometry2D.decompose_polygon_in_convex` + fan triangulation) of each block's world-space polygon — robust to concavity and rotation.
|
||||
- **Auto re-bake** on any terrain edit: the nav mesh is marked dirty on terrain place / move / rotate (`transform_committed`) / delete and re-baked once per frame (coalescing bursts) in `_process`.
|
||||
- Each stickman's `NavigationAgent2D` is a child of the rig **at the feet** (local `(0, +385)` = `-FOOT_OFFSET`), so it sits on the ground-level mesh; agent and region share the default navigation map, layer 1.
|
||||
- `walk_to(target)` treats `target` as a **feet/ground destination**; the rig converts ground-level path points back to root positions with `FOOT_OFFSET := (0, -385)`. Walking is **hybrid**: an **on-mesh target** follows the nav path (`_walk_mode = "nav"`), while an **off-mesh / unreachable target** (empty path **or** `is_target_reachable() == false`) switches to **direct straight-line steering** toward the clicked waypoint (`_walk_mode = "direct"`, root target = waypoint + `FOOT_OFFSET`) — it is **not** rejected with a warning and the rig does **not** stand still. Nav reads are deferred until the map has synced (`map_get_iteration_id(...) != 0`) and the path query is forced via `get_next_path_position()` before any reachability/finished check, so a fresh target is never misjudged as finished/unreachable after a single step.
|
||||
- Pathing is kinematic (`global_position.move_toward`); `avoidance_enabled = false` in 3a, so stickmen path through props and each other (deferred — see `docs/tech_debt_and_optimizations.md` #13).
|
||||
- **Walk/nav debugging (off by default):** `const DEBUG_WALK` in `stickman_rig.gd` (per-frame walk trace with `mode=nav|direct`, plus one-shot `walk_to`/finish/cancel prints) and `const DEBUG_STAGE` in `sandbox_stage.gd` (`[stage]` target capture, `[nav]` baked verts/polys, `[stage] PLAY rigs`).
|
||||
|
||||
**StickmanRig director API surface:**
|
||||
|
||||
| Member | Signature | Behavior |
|
||||
|---|---|---|
|
||||
| `RunnerState` / `ActionPhase` | `enum { IDLE, EXECUTING }` / `enum { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }` | The queue-runner state machine advanced in `_physics_process`. |
|
||||
| `FOOT_OFFSET` | `const := Vector2(0.0, -385.0)` | Feet → root (ground point → hips). |
|
||||
| `walk_speed` | `@export var walk_speed: float = 300.0` | Kinematic walk speed. |
|
||||
| `walk_to` | `func walk_to(target: Vector2, speed: float = -1.0) -> void` | Start walking so the feet land at `target`; no-op unless `state == ANIMATED`. |
|
||||
| `is_walking` | `func is_walking() -> bool` | Whether a walk is in progress. |
|
||||
| `speak` | `func speak(text: String, duration: float) -> void` | Show a `SpeechBubble` for `duration` s; auto-hides + emits `speech_finished`. |
|
||||
| `queue_action` / `clear_queue` / `get_queue` / `remove_action` / `insert_action` / `queue_size` | — | The mutable action queue; all mutations emit `queue_changed`. |
|
||||
| `start_queue` / `stop_queue` / `is_queue_running` | — | Runner control. `stop_queue()` aborts without emitting `queue_finished`. |
|
||||
| `is_ragdoll_at_rest` | `func is_ragdoll_at_rest() -> bool` | Whether the ragdoll has rested (independent of `auto_recover`); the runner waits on it for the `ragdoll` action. |
|
||||
| `arrived` | `signal arrived` | `walk_to` reached its destination. |
|
||||
| `action_started` / `action_finished` | `signal(action: Dictionary, index: int)` | Emitted per action as the runner begins/completes it. |
|
||||
| `queue_finished` | `signal queue_finished` | The queue ran to completion (not on stop). |
|
||||
| `queue_changed` | `signal queue_changed` | Any queue mutation — drives the visuals dirty flag. |
|
||||
| `speech_finished` | `signal speech_finished` | The speech bubble auto-hid after `speak()`. |
|
||||
|
||||
`recover` waits for `state == RigState.ANIMATED` via the existing `state_changed` signal; `_enter_ragdoll()` calls `_cancel_walking()` so a ragdolled rig has no stale walk/path state.
|
||||
|
||||
## File format (`.stk`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user