@tool extends Node3D ## Hollow planet collision. Attach to Moon or Earth. ## SphereShape3D is a SOLID ball — anything inside the radius gets ejected. ## This builds a concave surface (triangles only) with an optional station hole. ## Visual mesh stays on SurfaceHigh. Collision matches that radius. @export var surface_mesh_path: NodePath = ^"SurfaceHigh" @export var old_body_path: NodePath = ^"MoonBody" @export var hole_anchor_path: NodePath = ^"LovellAnchor" @export var hole_radius_m: float = 205.0 @export var segments: int = 48 @export var rings: int = 32 @export var rebuild_shell: bool = false: set(v): rebuild_shell = false if v: build() func _ready() -> void: build() func build() -> void: var radius: float = _visual_radius() if radius < 10.0: push_warning("PlanetShell: no radius from SurfaceHigh") return _disable_solid_body() var hole_dir := Vector3.ZERO var hole_cos := 1.0 var anchor := get_node_or_null(hole_anchor_path) as Node3D if anchor and hole_radius_m > 1.0: var to_a: Vector3 = anchor.global_position - global_position if to_a.length() > 1.0: hole_dir = global_transform.basis.inverse() * to_a.normalized() hole_dir = hole_dir.normalized() var ang: float = atan(hole_radius_m / radius) hole_cos = cos(ang) var faces := _make_faces(radius, hole_dir, hole_cos) var body := get_node_or_null("ShellBody") as StaticBody3D if body == null: body = StaticBody3D.new() body.name = "ShellBody" add_child(body) body.owner = _scene_root() body.transform = Transform3D() body.collision_layer = 1 body.collision_mask = 1 var col := body.get_node_or_null("CollisionShape3D") as CollisionShape3D if col == null: col = CollisionShape3D.new() col.name = "CollisionShape3D" body.add_child(col) col.owner = body.owner var shape := ConcavePolygonShape3D.new() shape.set_faces(faces) col.shape = shape col.transform = Transform3D() print("PlanetShell ", name, " radius=", radius, " faces=", faces.size() / 3, " hole=", hole_dir != Vector3.ZERO, " anchor=", anchor) func _visual_radius() -> float: var high := get_node_or_null(surface_mesh_path) as MeshInstance3D if high and high.mesh is SphereMesh: var sm := high.mesh as SphereMesh var rad: float = sm.radius if sm.height > rad * 1.5: rad = sm.height * 0.5 var scl: float = maxf(high.scale.x, maxf(high.scale.y, high.scale.z)) rad *= scl if rad > 100.0: return rad return 173700.0 func _disable_solid_body() -> void: var old := get_node_or_null(old_body_path) if old == null: return var col := old.get_node_or_null("CollisionShape3D") as CollisionShape3D if col: col.disabled = true if old is CollisionObject3D: (old as CollisionObject3D).collision_layer = 0 (old as CollisionObject3D).collision_mask = 0 print("PlanetShell: MoonBody DISABLED (solid sphere ejects)") func _make_faces(radius: float, hole_dir: Vector3, hole_cos: float) -> PackedVector3Array: var faces := PackedVector3Array() var segs: int = maxi(12, segments) var rgs: int = maxi(8, rings) var use_hole: bool = hole_dir.length_squared() > 0.5 for lat in range(rgs): var t0: float = float(lat) / float(rgs) * PI - PI * 0.5 var t1: float = float(lat + 1) / float(rgs) * PI - PI * 0.5 var y0: float = sin(t0) var y1: float = sin(t1) var r0: float = cos(t0) var r1: float = cos(t1) for lon in range(segs): var a0: float = float(lon) / float(segs) * TAU var a1: float = float(lon + 1) / float(segs) * TAU var p00 := Vector3(r0 * cos(a0), y0, r0 * sin(a0)) * radius var p10 := Vector3(r0 * cos(a1), y0, r0 * sin(a1)) * radius var p01 := Vector3(r1 * cos(a0), y1, r1 * sin(a0)) * radius var p11 := Vector3(r1 * cos(a1), y1, r1 * sin(a1)) * radius _add_tri(faces, p00, p01, p11, hole_dir, hole_cos, use_hole) _add_tri(faces, p00, p11, p10, hole_dir, hole_cos, use_hole) return faces func _add_tri(faces: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3, hole_dir: Vector3, hole_cos: float, use_hole: bool) -> void: if use_hole: var mid: Vector3 = (a + b + c) / 3.0 if mid.length_squared() < 0.001: return if mid.normalized().dot(hole_dir) >= hole_cos: return # Both windings. Concave shells are one-sided; a flipped face is a hole you fall through. faces.append(a) faces.append(b) faces.append(c) faces.append(a) faces.append(c) faces.append(b) func _scene_root() -> Node: if Engine.is_editor_hint() and get_tree() and get_tree().edited_scene_root: return get_tree().edited_scene_root return owner if owner else self