feat: Add project roadmap and initial specifications for dynamic vector props and terrain system

- Created ROADMAP.md outlining core features, implementation phases, and detailed breakdown for the Stickman Sandbox Builder project.
- Introduced plans for dynamic vector props with `PropBlock` specification, including reusable components and utility functions for prop creation.
- Developed a vector terrain system plan detailing the reusable `TerrainBlock` component and associated utility functions for geometry handling.
- Implemented `PhysicsTestHarness` scene for testing physics interactions with dynamic props and terrain.
- Added scripts for `PropBlock`, `PropUtils`, `TerrainBlock`, and `TerrainUtils` to support dynamic prop creation and terrain management.
This commit is contained in:
2026-08-26 09:35:09 -04:00
parent 4d490eef6c
commit 6edf3e53e3
16 changed files with 1139 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
class_name PhysicsTestHarness
extends Node2D
## PhysicsTestHarness - Standalone vector-terrain physics test scene (Phase 2).
##
## Builds flat ground, an angled ramp and stepped terrain via the TerrainUtils
## factory, spawns a master_rig.tscn instance standing on the flat ground, and
## provides camera zoom/pan input. NOT wired into the editor — run standalone
## via F6 on res://scenes/physics_test_harness.tscn.
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
const MIN_ZOOM: float = 0.25
const MAX_ZOOM: float = 3.0
const ZOOM_STEP: float = 1.10
const RIG_SCENE := preload("res://master_rig.tscn")
## Preloaded prop scripts: resolved via preload (not the global class registry)
## so the harness compiles even when the editor's class cache is stale.
const PROP_UTILS_SCRIPT := preload("res://scripts/prop_utils.gd")
const PROP_BLOCK_SCRIPT := preload("res://scripts/prop_block.gd")
## World-space Y of the flat ground's top surface.
const GROUND_TOP_Y: float = 0.0
## Rig root placement: the rig's feet rest ~385 px below its root, so placing
## the root 385 px above the ground puts the feet on the top surface.
const RIG_SPAWN_POSITION := Vector2(0.0, -385.0)
## Spawn point for dynamic props: above the angled ramp so they tumble down.
const PROP_SPAWN_POSITION := Vector2(300.0, -300.0)
## Best-effort collision proxy for the rig (which has no physics bodies): a
## static box matching the standing figure's world bounds (x ±120, y 0..-1000).
const RIG_PROXY_SIZE := Vector2(240.0, 1000.0)
const RIG_PROXY_CENTER := Vector2(0.0, -500.0)
# ---------------------------------------------------------------------------
# Node references
# ---------------------------------------------------------------------------
@onready var _camera: Camera2D = $Camera2D
@onready var _environment: Node2D = $Environment
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
var _is_panning: bool = false
var _pan_last: Vector2 = Vector2.ZERO
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _ready() -> void:
_camera.make_current()
_build_environment()
_spawn_rig()
# ---------------------------------------------------------------------------
# Input (camera zoom / pan)
# ---------------------------------------------------------------------------
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_handle_mouse_button(event as InputEventMouseButton)
elif event is InputEventMouseMotion:
_handle_mouse_motion(event as InputEventMouseMotion)
elif event is InputEventKey:
_handle_key(event as InputEventKey)
func _handle_key(key: InputEventKey) -> void:
if not key.pressed or key.echo:
return
match key.keycode:
KEY_1:
_spawn_prop_crate()
KEY_2:
_spawn_prop_ball()
KEY_3:
_spawn_prop_plank()
func _handle_mouse_button(mb: InputEventMouseButton) -> void:
match mb.button_index:
MOUSE_BUTTON_WHEEL_UP:
if mb.pressed:
_set_zoom(_camera.zoom.x * ZOOM_STEP)
MOUSE_BUTTON_WHEEL_DOWN:
if mb.pressed:
_set_zoom(_camera.zoom.x / ZOOM_STEP)
MOUSE_BUTTON_MIDDLE:
_is_panning = mb.pressed
_pan_last = mb.position
func _handle_mouse_motion(mm: InputEventMouseMotion) -> void:
if _is_panning:
_camera.position -= mm.relative / _camera.zoom.x
func _set_zoom(value: float) -> void:
var z := clampf(value, MIN_ZOOM, MAX_ZOOM)
_camera.zoom = Vector2(z, z)
# ---------------------------------------------------------------------------
# Environment / rig construction
# ---------------------------------------------------------------------------
func _build_environment() -> void:
# Flat ground: top surface at GROUND_TOP_Y, extending downward (solid).
TerrainUtils.spawn_block(_environment, PackedVector2Array([
Vector2(-800.0, GROUND_TOP_Y),
Vector2(800.0, GROUND_TOP_Y),
Vector2(800.0, GROUND_TOP_Y + 64.0),
Vector2(-800.0, GROUND_TOP_Y + 64.0),
]))
# Angled ramp (a sloped quad rising 128 px over its 192 px run).
TerrainUtils.spawn_block(_environment, PackedVector2Array([
Vector2(208.0, GROUND_TOP_Y),
Vector2(400.0, GROUND_TOP_Y - 128.0),
Vector2(400.0, GROUND_TOP_Y - 64.0),
Vector2(208.0, GROUND_TOP_Y + 64.0),
]))
# Stepped terrain (a single concave staircase — exercises BUILD_SOLIDS).
TerrainUtils.spawn_block(_environment, PackedVector2Array([
Vector2(496.0, GROUND_TOP_Y + 64.0),
Vector2(752.0, GROUND_TOP_Y + 64.0),
Vector2(752.0, GROUND_TOP_Y - 192.0),
Vector2(688.0, GROUND_TOP_Y - 192.0),
Vector2(688.0, GROUND_TOP_Y - 128.0),
Vector2(624.0, GROUND_TOP_Y - 128.0),
Vector2(624.0, GROUND_TOP_Y - 64.0),
Vector2(560.0, GROUND_TOP_Y - 64.0),
Vector2(560.0, GROUND_TOP_Y),
Vector2(496.0, GROUND_TOP_Y),
]))
func _spawn_rig() -> void:
var rig := RIG_SCENE.instantiate() as Node2D
if rig == null:
push_warning("PhysicsTestHarness: failed to instantiate master_rig.tscn.")
return
rig.position = RIG_SPAWN_POSITION
add_child(rig)
_add_rig_collision_proxy()
# ---------------------------------------------------------------------------
# Dynamic prop spawning (keys 1/2/3)
# ---------------------------------------------------------------------------
func _spawn_prop_crate() -> void:
PROP_UTILS_SCRIPT.spawn_prop(
_environment,
PROP_SPAWN_POSITION + Vector2(-24.0, 0.0),
PROP_UTILS_SCRIPT.create_box(),
PROP_BLOCK_SCRIPT.MaterialPreset.WOOD,
Vector2(60.0, 0.0)
)
func _spawn_prop_ball() -> void:
PROP_UTILS_SCRIPT.spawn_prop(
_environment,
PROP_SPAWN_POSITION,
PROP_UTILS_SCRIPT.create_ball(),
PROP_BLOCK_SCRIPT.MaterialPreset.RUBBER,
Vector2(-80.0, 0.0)
)
func _spawn_prop_plank() -> void:
PROP_UTILS_SCRIPT.spawn_prop(
_environment,
PROP_SPAWN_POSITION + Vector2(24.0, 0.0),
PROP_UTILS_SCRIPT.create_plank(),
PROP_BLOCK_SCRIPT.MaterialPreset.METAL,
Vector2(30.0, -40.0)
)
## The rig has no physics bodies, so a code-only StaticBody2D proxy provides a
## collision surface matching its standing bounds. Props bounce/rest against it.
func _add_rig_collision_proxy() -> void:
var proxy := StaticBody2D.new()
proxy.name = "RigCollisionProxy"
proxy.position = RIG_PROXY_CENTER
var shape := CollisionShape2D.new()
shape.name = "CollisionShape2D"
var rect := RectangleShape2D.new()
rect.size = RIG_PROXY_SIZE
shape.shape = rect
proxy.add_child(shape)
add_child(proxy)