Phase 14 - Organized directories.

This commit is contained in:
2026-10-03 19:13:39 -04:00
parent 6666173c4f
commit d950fd9d9a
307 changed files with 1452 additions and 1110 deletions
+122
View File
@@ -0,0 +1,122 @@
@tool
class_name TerrainBlock
extends StaticBody2D
## TerrainBlock - Reusable StaticBody2D vector-terrain component (Phase 1).
##
## A self-contained terrain block: a StaticBody2D root with three children built
## in code — a Polygon2D (interior fill), a Line2D (crisp vector outline, closed
## by appending the first vertex, rounded joints/caps), and a CollisionPolygon2D
## configured with BUILD_SOLIDS so concave terrain blocks collide correctly.
## Fully @tool: exported properties update the children live in the editor.
# ---------------------------------------------------------------------------
# Child node names
# ---------------------------------------------------------------------------
const POLYGON_NODE_NAME := "Polygon2D"
const OUTLINE_NODE_NAME := "Outline"
const COLLISION_NODE_NAME := "CollisionPolygon2D"
# ---------------------------------------------------------------------------
# Exported properties
# ---------------------------------------------------------------------------
## The terrain polygon's vertices (local space). The unified setter pushes the
## array to the Polygon2D and CollisionPolygon2D, and closes the Line2D loop by
## appending the first vertex to the end.
@export var polygon_points: PackedVector2Array = PackedVector2Array():
set(value):
polygon_points = value
_apply_points()
## Interior fill color (Polygon2D).
@export var fill_color: Color = Color(0.25, 0.55, 0.25, 1.0):
set(value):
fill_color = value
if _polygon != null:
_polygon.color = value
## Outline color (Line2D).
@export var outline_color: Color = Color(0.05, 0.10, 0.05, 1.0):
set(value):
outline_color = value
if _outline != null:
_outline.default_color = value
## Outline width in pixels (Line2D).
@export var outline_width: float = 2.0:
set(value):
outline_width = value
if _outline != null:
_outline.width = value
## Registry id of the spawn template that produced this block (Phase 4b). Used
## by the terrain drag-painting three-state overlap query to detect same-type
## overlaps. Plain var (not inspector-editable).
var spawn_id: String = ""
# ---------------------------------------------------------------------------
# Internal node references (built in _ready, @tool-safe)
# ---------------------------------------------------------------------------
var _polygon: Polygon2D
var _outline: Line2D
var _collision: CollisionPolygon2D
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _ready() -> void:
_ensure_children()
_apply_points()
_apply_style()
# ---------------------------------------------------------------------------
# Internal build / apply
# ---------------------------------------------------------------------------
func _ensure_children() -> void:
_polygon = get_node_or_null(NodePath(POLYGON_NODE_NAME)) as Polygon2D
if _polygon == null:
_polygon = Polygon2D.new()
_polygon.name = POLYGON_NODE_NAME
add_child(_polygon)
_outline = get_node_or_null(NodePath(OUTLINE_NODE_NAME)) as Line2D
if _outline == null:
_outline = Line2D.new()
_outline.name = OUTLINE_NODE_NAME
_outline.joint_mode = Line2D.LINE_JOINT_ROUND
_outline.begin_cap_mode = Line2D.LINE_CAP_ROUND
_outline.end_cap_mode = Line2D.LINE_CAP_ROUND
add_child(_outline)
_collision = get_node_or_null(NodePath(COLLISION_NODE_NAME)) as CollisionPolygon2D
if _collision == null:
_collision = CollisionPolygon2D.new()
_collision.name = COLLISION_NODE_NAME
_collision.build_mode = CollisionPolygon2D.BUILD_SOLIDS
add_child(_collision)
## Pushes the current polygon_points to the Polygon2D and CollisionPolygon2D,
## and appends the first vertex to the Line2D to close the border loop.
func _apply_points() -> void:
if _polygon != null:
_polygon.polygon = polygon_points
if _collision != null:
_collision.polygon = polygon_points if polygon_points.size() >= 3 else PackedVector2Array()
if _outline != null:
var outline_points := polygon_points.duplicate()
if not outline_points.is_empty():
outline_points.append(polygon_points[0])
_outline.points = outline_points
func _apply_style() -> void:
if _polygon != null:
_polygon.color = fill_color
if _outline != null:
_outline.default_color = outline_color
_outline.width = outline_width
+1
View File
@@ -0,0 +1 @@
uid://bw31hvuc1uako
+104
View File
@@ -0,0 +1,104 @@
class_name TerrainUtils
extends RefCounted
## TerrainUtils - Static geometry engine for vector terrain (Phase 3).
##
## Two responsibilities: sanitize raw input points into a clean, grid-snapped,
## clockwise-ordered polygon; and spawn a configured TerrainBlock into a target
## container. Consumed by the standalone physics test harness; not referenced by
## the editor.
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
const DEFAULT_GRID_SIZE: float = 16.0
const DEFAULT_FILL_COLOR := Color(0.25, 0.55, 0.25, 1.0)
const DEFAULT_OUTLINE_COLOR := Color(0.05, 0.10, 0.05, 1.0)
const DEFAULT_OUTLINE_WIDTH: float = 2.0
# ---------------------------------------------------------------------------
# Point sanitization
# ---------------------------------------------------------------------------
## Snap raw points to a grid, drop redundant nodes, and enforce clockwise
## winding (required for solid collision decomposition). Returns a new array;
## the input is not modified.
static func sanitize_points(points: PackedVector2Array, grid_size: float = DEFAULT_GRID_SIZE) -> PackedVector2Array:
var cleaned := PackedVector2Array()
for p: Vector2 in points:
cleaned.append(_snap_to_grid(p, grid_size))
cleaned = _simplify_polyline(cleaned)
if cleaned.size() >= 3 and not Geometry2D.is_polygon_clockwise(cleaned):
cleaned.reverse()
return cleaned
# ---------------------------------------------------------------------------
# Factory spawner
# ---------------------------------------------------------------------------
## Sanitize `points`, create a new TerrainBlock, apply the cleaned points and
## styling, and add it to `container`. Returns the spawned block.
static func spawn_block(
container: Node,
points: PackedVector2Array,
grid_size: float = DEFAULT_GRID_SIZE,
fill_color: Color = DEFAULT_FILL_COLOR,
outline_color: Color = DEFAULT_OUTLINE_COLOR,
outline_width: float = DEFAULT_OUTLINE_WIDTH
) -> TerrainBlock:
var block := TerrainBlock.new()
block.name = "TerrainBlock"
block.polygon_points = sanitize_points(points, grid_size)
block.fill_color = fill_color
block.outline_color = outline_color
block.outline_width = outline_width
container.add_child(block)
return block
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
static func _snap_to_grid(p: Vector2, grid_size: float) -> Vector2:
if grid_size <= 0.0:
return p
return Vector2(roundf(p.x / grid_size) * grid_size, roundf(p.y / grid_size) * grid_size)
## Remove redundant nodes: consecutive duplicates (e.g. collapsed by grid
## snapping) and collinear middle vertices (redundant for both render and
## collision). NOTE: Geometry2D.simplify_polyline() does not exist in Godot
## 4.7.1, so this local pass stands in for it.
static func _simplify_polyline(points: PackedVector2Array) -> PackedVector2Array:
var deduped := PackedVector2Array()
for p: Vector2 in points:
if not deduped.is_empty() and deduped[deduped.size() - 1].is_equal_approx(p):
continue
deduped.append(p)
# Drop a closing duplicate (last vertex == first vertex). Polygons are
# treated as open here; otherwise the collinear pass below would treat the
# first/last vertices as "collinear" with each other and erase the first
# corner, producing a degenerate polygon.
if deduped.size() >= 2 and deduped[0].is_equal_approx(deduped[deduped.size() - 1]):
deduped.remove_at(deduped.size() - 1)
if deduped.size() < 3:
return deduped
var simplified := PackedVector2Array()
var n: int = deduped.size()
for i: int in n:
var prev := deduped[(i - 1 + n) % n]
var curr := deduped[i]
var next := deduped[(i + 1) % n]
if _is_collinear(prev, curr, next):
continue
simplified.append(curr)
return simplified if simplified.size() >= 3 else deduped
static func _is_collinear(a: Vector2, b: Vector2, c: Vector2) -> bool:
return absf((b - a).cross(c - b)) < 0.001
+1
View File
@@ -0,0 +1 @@
uid://cduiqhk7xpm4m