extends RefCounted ## Prototype QA: two short bumps on StaticBody3D siblings of Floor (same NavigationRegion3D parent). ## Avoids one body with floor + cylinder (compound internal edges caused idle jitter beside bumps). ## Bump mesh group and collision fudge: random_floor_bump_collision_constants.gd. ## Spawner called from main.gd before nav bake. ## Collision radius > mesh radius; spawn before `NavigationRegion3D.bake_navigation_mesh()`. const BUMP_COLLISION_CONSTS_SCRIPT: Script = preload( "res://scripts/random_floor_bump_collision_constants.gd" ) const COUNT: int = 2 const HEIGHT_MIN: float = 0.08 const HEIGHT_MAX: float = 0.14 const RADIUS_MIN: float = 0.36 const RADIUS_MAX: float = 0.62 const FLOOR_SINK: float = 0.008 const XZ_RANGE: float = 8.15 const MIN_PAIR_SEPARATION: float = 2.85 const MAX_PLACE_ATTEMPTS: int = 64 const EXCLUSION_MARGIN: float = 0.72 static func spawn_short_random_bumps(floor_node: StaticBody3D) -> void: var rng := RandomNumberGenerator.new() rng.randomize() var mat := StandardMaterial3D.new() mat.albedo_color = Color(0.22, 0.52, 0.32, 1.0) mat.roughness = 1.0 mat.metallic = 0.0 mat.specular_mode = 2 var placed_xz: Array[Vector2] = [] for idx: int in COUNT: var xz: Vector2 = Vector2.ZERO var ok := false for _a: int in MAX_PLACE_ATTEMPTS: xz = Vector2(rng.randf_range(-XZ_RANGE, XZ_RANGE), rng.randf_range(-XZ_RANGE, XZ_RANGE)) if _xz_in_exclusion(xz): continue var far_enough := true for p: Vector2 in placed_xz: if xz.distance_to(p) < MIN_PAIR_SEPARATION: far_enough = false break if not far_enough: continue ok = true break if not ok: push_warning( ( "RandomFloorBumps: could not place bump %d after %d tries" % [idx, MAX_PLACE_ATTEMPTS] ) ) continue placed_xz.append(xz) var h: float = rng.randf_range(HEIGHT_MIN, HEIGHT_MAX) var r: float = rng.randf_range(RADIUS_MIN, RADIUS_MAX) _add_cylinder_bump(floor_node, xz.x, xz.y, h, r, mat, idx) static func _xz_in_exclusion(xz: Vector2) -> bool: var ex: Array[Vector2] = [ Vector2(-5.0, -5.0), Vector2(0.0, 0.0), Vector2(6.0, 5.0), Vector2(7.5, -6.5), Vector2(9.0, 9.0), ] var rad: Array[float] = [2.65, 1.2, 2.25, 1.65, 1.85] for i: int in ex.size(): if xz.distance_to(ex[i]) < rad[i] + EXCLUSION_MARGIN: return true return false static func _add_cylinder_bump( floor_node: StaticBody3D, x: float, z: float, height: float, radius: float, mat: StandardMaterial3D, idx: int ) -> void: var cy: float = height * 0.5 - FLOOR_SINK var local_center := Vector3(x, cy, z) var nav_parent := floor_node.get_parent() if nav_parent == null: push_error("RandomFloorBumps: Floor has no parent; cannot spawn bump") return var bump_root := StaticBody3D.new() bump_root.name = "RandomTestBump%d" % idx bump_root.add_to_group("walkable") bump_root.collision_layer = floor_node.collision_layer bump_root.collision_mask = floor_node.collision_mask nav_parent.add_child(bump_root) bump_root.global_position = floor_node.to_global(local_center) var mesh_inst := MeshInstance3D.new() mesh_inst.name = "RandomTestBump%d_mesh" % idx mesh_inst.add_to_group(BUMP_COLLISION_CONSTS_SCRIPT.RANDOM_FLOOR_BUMP_MESH_GROUP) var cm := CylinderMesh.new() cm.height = height cm.top_radius = radius cm.bottom_radius = radius cm.radial_segments = 24 mesh_inst.mesh = cm mesh_inst.set_surface_override_material(0, mat) mesh_inst.position = Vector3.ZERO bump_root.add_child(mesh_inst) var col := CollisionShape3D.new() col.name = "RandomTestBump%d_shape" % idx var csh := CylinderShape3D.new() csh.height = height csh.radius = minf( radius + BUMP_COLLISION_CONSTS_SCRIPT.COLLISION_RADIUS_EXTRA, BUMP_COLLISION_CONSTS_SCRIPT.COLLISION_RADIUS_MAX ) col.shape = csh col.position = Vector3.ZERO bump_root.add_child(col)