- 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.
204 lines
6.5 KiB
GDScript
204 lines
6.5 KiB
GDScript
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)
|