extends Object ## NEO-25: spawns walkable interactable props + glow marker pairs from server descriptor dicts. ## Returns glow groups for [method InteractionRadiusIndicators.setup_glow_groups]. const PrototypeDistrictVisualCatalog := preload( "res://scripts/prototype_district_visual_catalog.gd" ) const _PROP_SIZE := Vector3(0.9, 1.0, 0.4) const _MARKER_MESH_SIZE := Vector3(0.22, 0.35, 0.22) const _MARKER_OFFSET_A := Vector3(1.1, 0.25, 0.9) const _MARKER_OFFSET_B := Vector3(-0.95, 0.22, -1.05) static func build_from_catalog( descriptors: Array, interactables_root: Node3D, markers_root: Node3D ) -> Array: for c in interactables_root.get_children(): c.free() for c in markers_root.get_children(): c.free() var glow_groups: Array = [] for row in descriptors: if not row is Dictionary: continue var rd: Dictionary = row var id: String = str(rd.get("interactableId", "")) var kind: String = str(rd.get("kind", "")) var anchor: Vector3 = _anchor_from_row(rd) var radius: float = float(rd.get("interactionRadius", 0.0)) var body := StaticBody3D.new() body.name = "Interactable_%s" % id body.collision_mask = 3 body.add_to_group("walkable") interactables_root.add_child(body) body.global_position = anchor _add_prop_visual(body, id, kind, anchor.y) var col := CollisionShape3D.new() var box_shape := BoxShape3D.new() box_shape.size = _PROP_SIZE col.shape = box_shape body.add_child(col) var marker_parent := Node3D.new() marker_parent.name = "Markers_%s" % id markers_root.add_child(marker_parent) marker_parent.global_position = anchor var mi_a := _make_marker_mesh() mi_a.position = _MARKER_OFFSET_A marker_parent.add_child(mi_a) var mi_b := _make_marker_mesh() mi_b.position = _MARKER_OFFSET_B marker_parent.add_child(mi_b) var glow_mat := StandardMaterial3D.new() glow_mat.albedo_color = Color(0.15, 0.4, 1.0, 1.0) glow_mat.emission_enabled = true glow_mat.emission = Color(0.3, 0.6, 1.0) glow_mat.emission_energy_multiplier = 0.12 mi_a.material_override = glow_mat mi_b.material_override = glow_mat glow_groups.append( {"anchor": anchor, "radius": radius, "material": glow_mat, "markers": [mi_a, mi_b]} ) return glow_groups static func _anchor_from_row(rd: Dictionary) -> Vector3: var a: Variant = rd.get("anchor", null) if not a is Dictionary: return Vector3.ZERO var ad: Dictionary = a return Vector3( float(ad.get("x", 0.0)), float(ad.get("y", 0.0)), float(ad.get("z", 0.0)), ) static func _make_marker_mesh() -> MeshInstance3D: var mi := MeshInstance3D.new() var m := BoxMesh.new() m.size = _MARKER_MESH_SIZE mi.mesh = m return mi static func _add_prop_visual( body: StaticBody3D, interactable_id: String, kind: String, anchor_y: float ) -> void: var packed: Variant = PrototypeDistrictVisualCatalog.INTERACTABLE_PROPS.get( interactable_id, null ) if packed is PackedScene: var visual: Node3D = PrototypeDistrictVisualCatalog.instantiate_prop( packed as PackedScene, anchor_y ) visual.name = "PropVisual" body.add_child(visual) return var prop_mesh := MeshInstance3D.new() var box_mesh := BoxMesh.new() box_mesh.size = _PROP_SIZE prop_mesh.mesh = box_mesh prop_mesh.position = Vector3.ZERO var prop_mat := StandardMaterial3D.new() if kind == "resource_node": prop_mat.albedo_color = Color(0.18, 0.55, 0.32, 1.0) else: prop_mat.albedo_color = Color(0.28, 0.38, 0.45, 1.0) prop_mesh.material_override = prop_mat body.add_child(prop_mesh)