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:
@@ -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)
|
||||
Reference in New Issue
Block a user