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
+26 -6
View File
@@ -22,6 +22,8 @@ enum Handle { NONE, TRANSLATE, ROTATE }
# ---------------------------------------------------------------------------
signal transform_committed(nodes: Array[Node2D])
signal transform_started(nodes: Array[Node2D])
signal transform_updated(nodes: Array[Node2D])
# ---------------------------------------------------------------------------
# Style constants (screen-space px; divided by zoom for world-space drawing)
@@ -53,6 +55,8 @@ var _hovered: Node2D = null
var _dragging: Handle = Handle.NONE
var _drag_last: Vector2 = Vector2.ZERO
var _drag_anchor: Node2D = null
var _translate_drag_origin: Vector2 = Vector2.ZERO
var _translate_start_positions: Dictionary = {}
var _rotate_start_rotation: float = 0.0
var _rotate_start_angle: float = 0.0
var _rotate_center: Vector2 = Vector2.ZERO
@@ -78,6 +82,10 @@ func set_enabled(enabled: bool) -> void:
queue_redraw()
func set_snap_size(size: float) -> void:
snap_size = maxf(size, 0.0)
func set_box_rect(rect: Rect2) -> void:
_box_rect = rect
queue_redraw()
@@ -105,31 +113,39 @@ func begin_drag(world_pos: Vector2) -> void:
_rotate_center = STAGE_SELECTION.get_world_aabb(_targets[0]).get_center()
_rotate_start_rotation = _targets[0].global_rotation
_rotate_start_angle = (world_pos - _rotate_center).angle()
transform_started.emit(_targets.duplicate())
## Begins a translate drag anchored on `node` (used when an object body is
## clicked). The drag moves the whole current selection (`_targets`); if `node`
## is not currently selected, the drag targets just `node`.
func begin_translate_drag(node: Node2D, world_pos: Vector2) -> void:
if node == null or not is_instance_valid(node):
return
_drag_anchor = node
if not _targets.has(node):
_targets = [node]
_dragging = Handle.TRANSLATE
_drag_last = world_pos
_translate_drag_origin = world_pos
_translate_start_positions.clear()
for target: Node2D in _targets:
if target != null and is_instance_valid(target):
_translate_start_positions[target] = target.global_position
transform_started.emit(_targets.duplicate())
func drag_to(world_pos: Vector2) -> void:
if _dragging == Handle.NONE or _targets.is_empty():
return
var delta := world_pos - _drag_last
if _dragging == Handle.TRANSLATE:
var actual := delta
var actual := world_pos - _translate_drag_origin
if snap_size > 0.0 and _drag_anchor != null and is_instance_valid(_drag_anchor):
var old := _drag_anchor.global_position
actual = _snap(old + delta) - old
var anchor_start: Vector2 = _translate_start_positions.get(_drag_anchor, _drag_anchor.global_position)
actual = _snap(anchor_start + actual) - anchor_start
for node: Node2D in _targets:
if is_instance_valid(node):
node.global_position += actual
if is_instance_valid(node) and _translate_start_positions.has(node):
node.global_position = (_translate_start_positions[node] as Vector2) + actual
elif _dragging == Handle.ROTATE:
if _targets.size() == 1 and is_instance_valid(_targets[0]):
var target := _targets[0]
@@ -140,6 +156,7 @@ func drag_to(world_pos: Vector2) -> void:
new_rotation = snappedf(new_rotation, deg_to_rad(15.0))
target.global_rotation = new_rotation
_drag_last = world_pos
transform_updated.emit(_targets.duplicate())
queue_redraw()
@@ -148,6 +165,7 @@ func end_drag() -> void:
transform_committed.emit(_targets.duplicate())
_dragging = Handle.NONE
_drag_anchor = null
_translate_start_positions.clear()
func is_dragging() -> bool:
@@ -194,4 +212,6 @@ func _rotate_ring_radius(rect: Rect2) -> float:
func _snap(v: Vector2) -> Vector2:
if snap_size <= 0.0:
return v
return Vector2(roundf(v.x / snap_size) * snap_size, roundf(v.y / snap_size) * snap_size)