- Added support for custom props in the sandbox, including loading, saving, and rendering. - Implemented `PropLibrary` for managing custom prop entries and scanning for `.prop` files. - Introduced `PropVisuals` for shared visual and collision representation of custom props. - Enhanced `StageSpawner` to handle spawning of custom props with layered visuals and collision hulls. - Created `MissingPropPlaceholder` to manage missing custom props during scene reloads. - Updated thumbnail generation to support custom props with unique keys based on file paths. - Added tests for custom prop lifecycle, authored visual values, and missing prop handling. - Refactored existing code to accommodate new prop functionalities and ensure backward compatibility.
90 lines
3.1 KiB
GDScript
90 lines
3.1 KiB
GDScript
class_name PropVisuals
|
|
extends RefCounted
|
|
## Shared custom-prop drawing and collision approximation.
|
|
|
|
const CIRCLE_SEGMENTS: int = 24
|
|
|
|
static func build(parent: Node2D, shapes: Array[Dictionary]) -> Rect2:
|
|
var bounds := Rect2()
|
|
var has_bounds := false
|
|
for index: int in shapes.size():
|
|
var shape: Dictionary = shapes[index]
|
|
var points := _shape_points(shape)
|
|
if points.size() < 2:
|
|
push_warning("PropVisuals: skipped shape with insufficient points.")
|
|
continue
|
|
var group := Node2D.new()
|
|
group.name = "Shape_%d" % index
|
|
parent.add_child(group)
|
|
var closed := bool(shape.get("closed", String(shape.get("shape_type", "")) != "line"))
|
|
var fill := Color.from_string(String(shape.get("fill_color", "")), Color.WHITE)
|
|
var outline := Color.from_string(String(shape.get("outline_color", "")), Color.BLACK)
|
|
var width: float = maxf(0.0, float(shape.get("outline_width", 2.0)))
|
|
if closed and points.size() >= 3:
|
|
var polygon := Polygon2D.new()
|
|
polygon.name = "Polygon2D"
|
|
polygon.polygon = points
|
|
polygon.color = fill
|
|
group.add_child(polygon)
|
|
var line := Line2D.new()
|
|
line.name = "Line2D"
|
|
var loop: PackedVector2Array = points.duplicate()
|
|
if closed:
|
|
loop.append(points[0])
|
|
line.points = loop
|
|
line.default_color = outline
|
|
line.width = width
|
|
line.joint_mode = Line2D.LINE_JOINT_ROUND
|
|
line.begin_cap_mode = Line2D.LINE_CAP_ROUND
|
|
line.end_cap_mode = Line2D.LINE_CAP_ROUND
|
|
group.add_child(line)
|
|
for point: Vector2 in points:
|
|
if not has_bounds:
|
|
bounds = Rect2(point, Vector2.ZERO)
|
|
has_bounds = true
|
|
else:
|
|
bounds = bounds.expand(point)
|
|
return bounds
|
|
|
|
|
|
static func collision_hull(shapes: Array[Dictionary]) -> PackedVector2Array:
|
|
var points := PackedVector2Array()
|
|
for shape: Dictionary in shapes:
|
|
var closed := bool(shape.get("closed", String(shape.get("shape_type", "")) != "line"))
|
|
if closed:
|
|
points.append_array(_shape_points(shape))
|
|
if points.size() < 3:
|
|
return PackedVector2Array()
|
|
var hull: PackedVector2Array = Geometry2D.convex_hull(points)
|
|
if hull.size() < 3 or absf(_polygon_area(hull)) <= 0.001:
|
|
return PackedVector2Array()
|
|
return hull
|
|
|
|
|
|
static func _shape_points(shape: Dictionary) -> PackedVector2Array:
|
|
var raw: Variant = shape.get("points", PackedVector2Array())
|
|
var points := PackedVector2Array()
|
|
if raw is PackedVector2Array:
|
|
points = raw as PackedVector2Array
|
|
elif raw is Array:
|
|
for item: Variant in raw as Array:
|
|
if item is Vector2:
|
|
points.append(item as Vector2)
|
|
elif item is Dictionary:
|
|
var data: Dictionary = item as Dictionary
|
|
points.append(Vector2(float(data.get("x", 0.0)), float(data.get("y", 0.0))))
|
|
if points.is_empty() and String(shape.get("shape_type", "")) == "circle":
|
|
var radius: float = maxf(0.0, float(shape.get("radius", 24.0)))
|
|
for index: int in CIRCLE_SEGMENTS:
|
|
var angle: float = TAU * float(index) / float(CIRCLE_SEGMENTS)
|
|
points.append(Vector2(cos(angle), sin(angle)) * radius)
|
|
return points
|
|
|
|
|
|
static func _polygon_area(points: PackedVector2Array) -> float:
|
|
var area: float = 0.0
|
|
for index: int in points.size():
|
|
var next: Vector2 = points[(index + 1) % points.size()]
|
|
area += points[index].cross(next)
|
|
return area * 0.5
|