Add Phase 4 Final Bugs Specification and Tests

- Introduced a new architecture specification for Phase 4 final bugs, detailing scope, decisions, runtime data contracts, action roles, execution lanes, speech ownership, and signal flow.
- Implemented changes in `scripts/action_registry.gd`, `scripts/action_editor.gd`, `scripts/sandbox_stage.gd`, `scripts/stage_gizmos.gd`, `scripts/stickman_factory.gd`, and `scripts/stickman_rig.gd` to address identified bugs.
- Created a new test suite `tests/test_phase4_final_bugs.gd` to validate the implementation, covering action roles, grid snapping, prop manipulation, reactive runtime lane, spawn pose preservation, and more.
- Added a unique identifier for the test file to ensure proper tracking and management.
This commit is contained in:
2026-09-13 23:20:01 -04:00
parent ce6bcecd00
commit 3a1d522ec9
11 changed files with 1281 additions and 79 deletions
+35 -3
View File
@@ -7,6 +7,8 @@ extends RefCounted
## the ActionEditor and the panels generate their UI from this registry, so no
## other code changes are required.
enum ReactiveRole { SEQUENTIAL, OVERLAY, TAKEOVER }
const ACTION_TEMPLATES := {
"walk_to": {
"label": "Walk To",
@@ -14,6 +16,7 @@ const ACTION_TEMPLATES := {
"params": [
{ "key": "target", "type": "position", "required": true },
],
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
},
"speak": {
"label": "Speak",
@@ -22,6 +25,7 @@ const ACTION_TEMPLATES := {
{ "key": "text", "type": "text", "required": true },
{ "key": "duration", "type": "float", "default": 2.0 },
],
"reactive_role": ReactiveRole.OVERLAY, "overlay_eligible": true,
},
"wait": {
"label": "Wait",
@@ -29,16 +33,19 @@ const ACTION_TEMPLATES := {
"params": [
{ "key": "duration", "type": "float", "required": true },
],
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
},
"ragdoll": {
"label": "Ragdoll",
"icon": "💥",
"params": [],
"reactive_role": ReactiveRole.TAKEOVER, "overlay_eligible": false,
},
"recover": {
"label": "Recover",
"icon": "🔄",
"params": [],
"reactive_role": ReactiveRole.TAKEOVER, "overlay_eligible": false,
},
}
@@ -66,6 +73,21 @@ static func icon(type: String) -> String:
return String(tpl.get("icon", ""))
static func is_overlay_eligible(type: String) -> bool:
var tpl: Dictionary = ACTION_TEMPLATES.get(type, {})
return bool(tpl.get("overlay_eligible", false))
static func reactive_role(action: Dictionary) -> ReactiveRole:
var type: String = String(action.get("type", ""))
var tpl: Dictionary = ACTION_TEMPLATES.get(type, {})
var role: ReactiveRole = int(tpl.get("reactive_role", ReactiveRole.SEQUENTIAL))
var params: Dictionary = action.get("params", {})
if type == "speak" and bool(params.get("stop_current", action.get("stop_current", false))):
return ReactiveRole.SEQUENTIAL
return role
## Converts a flat queue action into a rule-action shape (adds `target` + nests
## params). `target_id` is the acting stickman's instance id.
static func to_rule_action(action: Dictionary, target_id: int) -> Dictionary:
@@ -76,6 +98,8 @@ static func to_rule_action(action: Dictionary, target_id: int) -> Dictionary:
"speak":
params["text"] = String(action.get("text", ""))
params["duration"] = float(action.get("duration", 2.0))
if bool(action.get("stop_current", false)):
params["stop_current"] = true
"wait":
params["duration"] = float(action.get("duration", 0.0))
return {
@@ -92,11 +116,14 @@ static func from_rule_action(rule_action: Dictionary) -> Dictionary:
"walk_to":
return { "type": "walk_to", "target": params.get("target", Vector2.ZERO) }
"speak":
return {
var speak: Dictionary = {
"type": "speak",
"text": String(params.get("text", "")),
"duration": float(params.get("duration", 2.0)),
}
if bool(params.get("stop_current", false)):
speak["stop_current"] = true
return speak
"wait":
return { "type": "wait", "duration": float(params.get("duration", 0.0)) }
_:
@@ -110,12 +137,17 @@ static func summarize(action: Dictionary) -> String:
var params: Dictionary = action.get("params", {})
match type:
"walk_to":
var t: Vector2 = action.get("target", params.get("target", Vector2.ZERO))
# Rule actions use top-level `target` for the actor instance id and
# nested `params.target` for the waypoint. Prefer the nested value and
# type-guard legacy/malformed payloads so summaries never crash.
var raw_target: Variant = params.get("target", action.get("target", Vector2.ZERO))
var t: Vector2 = raw_target as Vector2 if raw_target is Vector2 else Vector2.ZERO
return "Walk To (%d, %d)" % [int(roundf(t.x)), int(roundf(t.y))]
"speak":
var text := String(action.get("text", params.get("text", "")))
var dur := float(action.get("duration", params.get("duration", 2.0)))
return "Speak \"%s\" (%ss)" % [text, _fmt_duration(dur)]
var suffix: String = " — stops current" if bool(params.get("stop_current", action.get("stop_current", false))) else ""
return "Speak \"%s\" (%ss)%s" % [text, _fmt_duration(dur), suffix]
"wait":
var wd := float(action.get("duration", params.get("duration", 0.0)))
return "Wait %ss" % _fmt_duration(wd)