@tool class_name PropBlock extends RigidBody2D ## PropBlock - Reusable dynamic vector prop (RigidBody2D). ## ## A root RigidBody2D with three children built in code — a Polygon2D (interior ## fill), a Line2D (crisp vector outline, round joints/caps), and a collision ## node: a CollisionPolygon2D with BUILD_SOLIDS for polygon props, or a ## CollisionShape2D with a CircleShape2D for circle props. Fully @tool: exported ## properties update the children live. A material preset selector sets mass, ## friction/bounce (PhysicsMaterial), and themed visuals. # --------------------------------------------------------------------------- # Enums # --------------------------------------------------------------------------- enum ShapeType { POLYGON, CIRCLE } enum MaterialPreset { NONE, WOOD, RUBBER, CARDBOARD, METAL } # --------------------------------------------------------------------------- # Child node names # --------------------------------------------------------------------------- const POLYGON_NODE_NAME := "Polygon2D" const OUTLINE_NODE_NAME := "Outline" const COLLISION_POLYGON_NODE_NAME := "CollisionPolygon2D" const COLLISION_SHAPE_NODE_NAME := "CollisionShape2D" ## Segment count for the generated circle polygon/outline. const CIRCLE_SEGMENTS: int = 48 const DEFAULT_FILL_COLOR := Color(0.55, 0.35, 0.15, 1.0) const DEFAULT_OUTLINE_COLOR := Color(0.15, 0.08, 0.02, 1.0) const DEFAULT_OUTLINE_WIDTH: float = 2.0 # --------------------------------------------------------------------------- # Exported properties # --------------------------------------------------------------------------- @export_enum("Polygon", "Circle") var shape_type: int = ShapeType.POLYGON: set(value): shape_type = value _apply_shape() @export var polygon_points: PackedVector2Array = PackedVector2Array(): set(value): polygon_points = value if shape_type == ShapeType.POLYGON: _apply_polygon_geometry() @export var radius: float = 32.0: set(value): radius = value if shape_type == ShapeType.CIRCLE: _apply_circle_geometry() @export var fill_color: Color = DEFAULT_FILL_COLOR: set(value): fill_color = value if _polygon != null: _polygon.color = value @export var outline_color: Color = DEFAULT_OUTLINE_COLOR: set(value): outline_color = value if _outline != null: _outline.default_color = value @export var outline_width: float = DEFAULT_OUTLINE_WIDTH: set(value): outline_width = value if _outline != null: _outline.width = value @export_enum("None", "Wood", "Rubber", "Cardboard", "Metal") var material_preset: int = MaterialPreset.NONE: set(value): material_preset = value _apply_material_preset() # --------------------------------------------------------------------------- # Internal node references (built in _ready, @tool-safe) # --------------------------------------------------------------------------- var _polygon: Polygon2D var _outline: Line2D var _collision_polygon: CollisionPolygon2D var _collision_shape: CollisionShape2D var _circle_shape: CircleShape2D # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: _ensure_children() _apply_shape() _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_polygon = get_node_or_null(NodePath(COLLISION_POLYGON_NODE_NAME)) as CollisionPolygon2D if _collision_polygon == null: _collision_polygon = CollisionPolygon2D.new() _collision_polygon.name = COLLISION_POLYGON_NODE_NAME _collision_polygon.build_mode = CollisionPolygon2D.BUILD_SOLIDS add_child(_collision_polygon) _collision_shape = get_node_or_null(NodePath(COLLISION_SHAPE_NODE_NAME)) as CollisionShape2D if _collision_shape == null: _collision_shape = CollisionShape2D.new() _collision_shape.name = COLLISION_SHAPE_NODE_NAME _circle_shape = CircleShape2D.new() _circle_shape.radius = radius _collision_shape.shape = _circle_shape add_child(_collision_shape) else: # A pre-existing collision shape (e.g. persisted in a scene) may already # hold a circle shape; reuse it so radius updates keep working. _circle_shape = _collision_shape.shape as CircleShape2D func _apply_shape() -> void: if shape_type == ShapeType.CIRCLE: _apply_circle_geometry() _set_collision_polygon_enabled(false) _set_collision_shape_enabled(true) else: _apply_polygon_geometry() _set_collision_polygon_enabled(true) _set_collision_shape_enabled(false) func _apply_polygon_geometry() -> void: if _polygon != null: _polygon.polygon = polygon_points if _collision_polygon != null: _collision_polygon.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_circle_geometry() -> void: var loop := PackedVector2Array() for i: int in CIRCLE_SEGMENTS: var angle: float = TAU * float(i) / float(CIRCLE_SEGMENTS) loop.append(Vector2(cos(angle), sin(angle)) * radius) if _polygon != null: _polygon.polygon = loop if _outline != null: var outline_points := loop.duplicate() if not outline_points.is_empty(): outline_points.append(loop[0]) _outline.points = outline_points if _circle_shape != null: _circle_shape.radius = radius func _set_collision_polygon_enabled(enabled: bool) -> void: if _collision_polygon != null: _collision_polygon.disabled = not enabled func _set_collision_shape_enabled(enabled: bool) -> void: if _collision_shape != null: _collision_shape.disabled = not enabled func _apply_style() -> void: if _polygon != null: _polygon.color = fill_color if _outline != null: _outline.default_color = outline_color _outline.width = outline_width func _apply_material_preset() -> void: mass = mass_for(material_preset) physics_material_override = physics_material(material_preset) if material_preset != MaterialPreset.NONE: fill_color = tint_for(material_preset) outline_color = tint_for(material_preset).darkened(0.55) # --------------------------------------------------------------------------- # Static preset factories # --------------------------------------------------------------------------- static func physics_material(preset: int) -> PhysicsMaterial: var mat := PhysicsMaterial.new() match preset: MaterialPreset.WOOD: mat.friction = 0.6 mat.bounce = 0.1 MaterialPreset.RUBBER: mat.friction = 0.9 mat.bounce = 0.85 MaterialPreset.CARDBOARD: mat.friction = 0.3 mat.bounce = 0.05 MaterialPreset.METAL: mat.friction = 0.9 mat.bounce = 0.0 _: mat.friction = 0.5 mat.bounce = 0.05 return mat static func mass_for(preset: int) -> float: match preset: MaterialPreset.WOOD: return 3.0 MaterialPreset.RUBBER: return 0.5 MaterialPreset.CARDBOARD: return 0.4 MaterialPreset.METAL: return 8.0 _: return 1.0 static func tint_for(preset: int) -> Color: match preset: MaterialPreset.WOOD: return Color(0.55, 0.38, 0.2, 1.0) MaterialPreset.RUBBER: return Color(0.9, 0.2, 0.2, 1.0) MaterialPreset.CARDBOARD: return Color(0.85, 0.72, 0.45, 1.0) MaterialPreset.METAL: return Color(0.5, 0.55, 0.6, 1.0) _: return DEFAULT_FILL_COLOR