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
+159
View File
@@ -0,0 +1,159 @@
class_name PropUtils
extends RefCounted
## PropUtils - Static factory for dynamic vector props.
##
## Primitive generators (box, ball, plank, triangle) return shape-payload
## dictionaries with default dimensions and color themes. spawn_prop instantiates
## a PropBlock, applies the payload + a physics material preset, sets an initial
## velocity, and adds it to a container. Custom polygon points are sanitized
## through TerrainUtils.sanitize_points().
# ---------------------------------------------------------------------------
# Preloaded dependencies (resolved directly, independent of the global class
# registry, so this script compiles even when the editor's class cache is stale)
# ---------------------------------------------------------------------------
const PropBlockScript := preload("res://scripts/prop_block.gd")
const TerrainUtilsScript := preload("res://scripts/terrain_utils.gd")
# ---------------------------------------------------------------------------
# Color themes
# ---------------------------------------------------------------------------
const WOOD_FILL := Color(0.55, 0.38, 0.2, 1.0)
const WOOD_OUTLINE := Color(0.25, 0.16, 0.06, 1.0)
const RUBBER_FILL := Color(0.9, 0.2, 0.2, 1.0)
const RUBBER_OUTLINE := Color(0.35, 0.05, 0.05, 1.0)
const METAL_FILL := Color(0.5, 0.55, 0.6, 1.0)
const METAL_OUTLINE := Color(0.15, 0.18, 0.22, 1.0)
const CARDBOARD_FILL := Color(0.85, 0.72, 0.45, 1.0)
const CARDBOARD_OUTLINE := Color(0.4, 0.32, 0.18, 1.0)
const DEFAULT_OUTLINE_WIDTH: float = 2.0
# ---------------------------------------------------------------------------
# Primitive generators
# ---------------------------------------------------------------------------
static func create_box(
size: Vector2 = Vector2(48.0, 48.0),
fill_color: Color = WOOD_FILL,
outline_color: Color = WOOD_OUTLINE,
outline_width: float = DEFAULT_OUTLINE_WIDTH
) -> Dictionary:
var half := size * 0.5
return {
"type": PropBlockScript.ShapeType.POLYGON,
"points": PackedVector2Array([
Vector2(-half.x, -half.y),
Vector2(half.x, -half.y),
Vector2(half.x, half.y),
Vector2(-half.x, half.y),
]),
"fill_color": fill_color,
"outline_color": outline_color,
"outline_width": outline_width,
}
static func create_ball(
radius: float = 24.0,
fill_color: Color = RUBBER_FILL,
outline_color: Color = RUBBER_OUTLINE,
outline_width: float = DEFAULT_OUTLINE_WIDTH
) -> Dictionary:
return {
"type": PropBlockScript.ShapeType.CIRCLE,
"radius": radius,
"fill_color": fill_color,
"outline_color": outline_color,
"outline_width": outline_width,
}
static func create_plank(
length: float = 160.0,
thickness: float = 16.0,
fill_color: Color = METAL_FILL,
outline_color: Color = METAL_OUTLINE,
outline_width: float = DEFAULT_OUTLINE_WIDTH
) -> Dictionary:
var half_length := length * 0.5
var half_thickness := thickness * 0.5
return {
"type": PropBlockScript.ShapeType.POLYGON,
"points": PackedVector2Array([
Vector2(-half_length, -half_thickness),
Vector2(half_length, -half_thickness),
Vector2(half_length, half_thickness),
Vector2(-half_length, half_thickness),
]),
"fill_color": fill_color,
"outline_color": outline_color,
"outline_width": outline_width,
}
static func create_triangle(
base: float = 56.0,
height: float = 48.0,
fill_color: Color = CARDBOARD_FILL,
outline_color: Color = CARDBOARD_OUTLINE,
outline_width: float = DEFAULT_OUTLINE_WIDTH
) -> Dictionary:
var half_base := base * 0.5
var half_height := height * 0.5
return {
"type": PropBlockScript.ShapeType.POLYGON,
"points": PackedVector2Array([
Vector2(-half_base, half_height),
Vector2(half_base, half_height),
Vector2(0.0, -half_height),
]),
"fill_color": fill_color,
"outline_color": outline_color,
"outline_width": outline_width,
}
# ---------------------------------------------------------------------------
# Factory spawner
# ---------------------------------------------------------------------------
## Instantiate a PropBlock from a shape payload and material preset, set an
## initial velocity, and add it to `container`. Returns the spawned prop.
static func spawn_prop(
container: Node,
position: Vector2,
shape_payload: Dictionary,
material_preset: int = PropBlockScript.MaterialPreset.WOOD,
initial_velocity: Vector2 = Vector2.ZERO
) -> PropBlockScript:
var prop: PropBlockScript = PropBlockScript.new()
prop.name = "PropBlock"
prop.position = position
prop.material_preset = material_preset
_apply_shape_payload(prop, shape_payload)
container.add_child(prop)
if initial_velocity != Vector2.ZERO:
prop.linear_velocity = initial_velocity
return prop
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
static func _apply_shape_payload(prop: PropBlockScript, payload: Dictionary) -> void:
var shape_type: int = int(payload.get("type", PropBlockScript.ShapeType.POLYGON))
prop.shape_type = shape_type
if shape_type == PropBlockScript.ShapeType.CIRCLE:
prop.radius = float(payload.get("radius", 24.0))
else:
var raw_points: PackedVector2Array = payload.get("points", PackedVector2Array())
prop.polygon_points = TerrainUtilsScript.sanitize_points(raw_points)
if payload.has("fill_color"):
prop.fill_color = payload["fill_color"]
if payload.has("outline_color"):
prop.outline_color = payload["outline_color"]
if payload.has("outline_width"):
prop.outline_width = float(payload["outline_width"])