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)