Add headless regression tests for Phase 4b features
- Implement test for popup anchor behavior in rule-builder menus to ensure consistent anchor positioning during menu transitions. - Create tests for stage logic, including mode transitions, toolbar visibility, and status bar updates. - Add terrain drag-painting tests to verify correct block placement behavior and conflict handling. - Introduce walk waypoint tests to check for arrival conditions and position stability after navigation.
This commit is contained in:
+61
-16
@@ -14,6 +14,7 @@ extends RefCounted
|
||||
const TERRAIN_UTILS := preload("res://scripts/terrain_utils.gd")
|
||||
const PROP_UTILS := preload("res://scripts/prop_utils.gd")
|
||||
const PROP_BLOCK := preload("res://scripts/prop_block.gd")
|
||||
const PROP_LIBRARY := preload("res://scripts/prop_library.gd")
|
||||
const STICKMAN_FACTORY := preload("res://scripts/stickman_factory.gd")
|
||||
const TRIGGER_AREA := preload("res://scripts/trigger_area.gd")
|
||||
|
||||
@@ -36,7 +37,9 @@ const TERRAIN_GRID_SIZE: float = 16.0
|
||||
|
||||
var _world: Node2D
|
||||
var _registry: Array[Dictionary] = []
|
||||
var _stickman_data: Dictionary = {}
|
||||
var selected_stickman_path: String = DEFAULT_STICKMAN_PATH
|
||||
var selected_prop_id: String = "crate"
|
||||
var _stickman_cache: Dictionary = {}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
@@ -44,8 +47,8 @@ var _stickman_data: Dictionary = {}
|
||||
|
||||
func _init(world: Node2D) -> void:
|
||||
_world = world
|
||||
_stickman_data = STICKMAN_FACTORY.load_stk(DEFAULT_STICKMAN_PATH)
|
||||
if _stickman_data.is_empty():
|
||||
_stickman_cache[DEFAULT_STICKMAN_PATH] = STICKMAN_FACTORY.load_stk(DEFAULT_STICKMAN_PATH)
|
||||
if (_stickman_cache[DEFAULT_STICKMAN_PATH] as Dictionary).is_empty():
|
||||
push_warning("StageSpawner: failed to load default stickman '%s'." % DEFAULT_STICKMAN_PATH)
|
||||
_build_registry()
|
||||
|
||||
@@ -72,6 +75,47 @@ func get_spawn_offset(id: String) -> Vector2:
|
||||
return entry.get("spawn_offset", Vector2.ZERO)
|
||||
|
||||
|
||||
func get_selected_stickman_path() -> String:
|
||||
return selected_stickman_path
|
||||
|
||||
|
||||
func get_selected_prop_id() -> String:
|
||||
return selected_prop_id
|
||||
|
||||
|
||||
## True when `id` names a terrain template (ground/ramp/step).
|
||||
func is_terrain_id(id: String) -> bool:
|
||||
var entry := _find_entry(id)
|
||||
return String(entry.get("kind", "")) == "terrain"
|
||||
|
||||
|
||||
## Local-space AABB of a terrain template's centered polygon (before placement),
|
||||
## used to size ghosts and rasterize the terrain-painting occupancy cells.
|
||||
## The AABB is computed over the SAME sanitized geometry that `_spawn_terrain`
|
||||
## actually places (TerrainUtils.sanitize_points at TERRAIN_GRID_SIZE), so the
|
||||
## D1 paint stride matches the real block footprint (e.g. the 200px-wide Ground
|
||||
## template sanitizes to a 192px footprint and therefore a 192px stride, which
|
||||
## makes horizontal runs tile edge-to-edge with no gaps).
|
||||
func get_template_aabb(id: String) -> Rect2:
|
||||
var entry := _find_entry(id)
|
||||
if entry.is_empty() or String(entry.get("kind", "")) != "terrain":
|
||||
return Rect2()
|
||||
var template: PackedVector2Array = entry["points"]
|
||||
if template.is_empty():
|
||||
return Rect2()
|
||||
var center := _points_center(template)
|
||||
var centered := PackedVector2Array()
|
||||
for p: Vector2 in template:
|
||||
centered.append(p - center)
|
||||
var cleaned := TERRAIN_UTILS.sanitize_points(centered, TERRAIN_GRID_SIZE)
|
||||
if cleaned.is_empty():
|
||||
return Rect2()
|
||||
var rect := Rect2(cleaned[0], Vector2.ZERO)
|
||||
for p: Vector2 in cleaned:
|
||||
rect = rect.expand(p)
|
||||
return rect
|
||||
|
||||
|
||||
## Spawn the registry type at `world_position`; returns null + push_warning on
|
||||
## an unknown id.
|
||||
func spawn(id: String, world_position: Vector2) -> Node2D:
|
||||
@@ -167,13 +211,7 @@ func _build_registry() -> void:
|
||||
"spawn_offset": Vector2.ZERO,
|
||||
},
|
||||
{
|
||||
"id": "crate", "label": "Crate", "kind": "prop",
|
||||
"payload": PROP_UTILS.create_box(), "preset": PROP_BLOCK.MaterialPreset.WOOD,
|
||||
"spawn_offset": Vector2.ZERO,
|
||||
},
|
||||
{
|
||||
"id": "ball", "label": "Ball", "kind": "prop",
|
||||
"payload": PROP_UTILS.create_ball(), "preset": PROP_BLOCK.MaterialPreset.RUBBER,
|
||||
"id": "prop", "label": "Prop", "kind": "prop",
|
||||
"spawn_offset": Vector2.ZERO,
|
||||
},
|
||||
{
|
||||
@@ -214,13 +252,16 @@ func _spawn_terrain(entry: Dictionary, world_position: Vector2) -> TerrainBlock:
|
||||
float(entry.get("width", 2.0))
|
||||
)
|
||||
block.position = world_position
|
||||
block.spawn_id = String(entry.get("id", ""))
|
||||
return block
|
||||
|
||||
|
||||
func _spawn_prop(entry: Dictionary, world_position: Vector2) -> PropBlock:
|
||||
var payload: Dictionary = entry["payload"]
|
||||
var preset: int = int(entry.get("preset", PROP_BLOCK.MaterialPreset.WOOD))
|
||||
return PROP_UTILS.spawn_prop(_world, world_position, payload, preset, Vector2.ZERO)
|
||||
var t: Dictionary = PROP_LIBRARY.get_entry(selected_prop_id)
|
||||
if t.is_empty():
|
||||
push_warning("StageSpawner: unknown selected prop '%s'." % selected_prop_id)
|
||||
return null
|
||||
return PROP_UTILS.spawn_prop(_world, world_position, t["payload"], int(t["material_preset"]), Vector2.ZERO)
|
||||
|
||||
|
||||
func _spawn_area(world_position: Vector2) -> Node2D:
|
||||
@@ -232,10 +273,14 @@ func _spawn_area(world_position: Vector2) -> Node2D:
|
||||
|
||||
|
||||
func _spawn_stickman(world_position: Vector2) -> StickmanRig:
|
||||
if _stickman_data.is_empty():
|
||||
push_warning("StageSpawner: no stickman data loaded; check '%s'." % DEFAULT_STICKMAN_PATH)
|
||||
var data: Dictionary = _stickman_cache.get(selected_stickman_path, {})
|
||||
if data.is_empty():
|
||||
data = STICKMAN_FACTORY.load_stk(selected_stickman_path)
|
||||
_stickman_cache[selected_stickman_path] = data
|
||||
if data.is_empty():
|
||||
push_warning("StageSpawner: no stickman data for '%s'." % selected_stickman_path)
|
||||
return null
|
||||
var rig: StickmanRig = STICKMAN_FACTORY.spawn_from_data(_stickman_data)
|
||||
var rig: StickmanRig = STICKMAN_FACTORY.spawn_from_data(data)
|
||||
if rig == null:
|
||||
push_warning("StageSpawner: failed to spawn stickman.")
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user