class_name UrbanDetails extends RefCounted # Visual-only dressing. Every feature is either flush with the ground or outside # the combat routes. Materials are batched into one static mesh per finish. var _groups: Dictionary = {} var _rng := RandomNumberGenerator.new() static func build() -> Node3D: var details := UrbanDetails.new() details._rng.seed = 30174 return details._build() func _build() -> Node3D: var root := Node3D.new() root.name = "DistrictEnvironment" # Perimeter collision and dressing are supplied by the expanded arena. _streets() _platforms() # The quarry escarpment supplies the skyline for this environment. # Sparse war-district dressing replaces the decorative planting. for x in [-28.0, 28.0]: for z in [-28.0, 28.0]: _streetlight(Vector3(x, 0, z), -signf(x)) for material in _groups: var instance := MeshInstance3D.new() instance.name = "EnvironmentBatch_%02d" % root.get_child_count() instance.mesh = (_groups[material] as SurfaceTool).commit() instance.material_override = material root.add_child(instance) return root func _add(mesh: Mesh, at: Vector3, kind: String, tint: Color = Color.WHITE, rotation: Vector3 = Vector3.ZERO, scale: Vector3 = Vector3.ONE) -> void: var finish: Material = UrbanMaterials.get_surface(kind, tint) if not _groups.has(finish): var batch := SurfaceTool.new() batch.begin(Mesh.PRIMITIVE_TRIANGLES) _groups[finish] = batch var source := SurfaceTool.new() source.create_from(mesh, 0) source.deindex() var basis := Basis.from_euler(rotation).scaled(scale) (_groups[finish] as SurfaceTool).append_from(source.commit(), 0, Transform3D(basis, at)) func _box(at: Vector3, size: Vector3, kind: String, tint: Color = Color.WHITE, bevel: float = 0.035, rotation: Vector3 = Vector3.ZERO) -> void: # Octagonal outline and inset end caps give actual bevels that catch sunlight. var half := size * 0.5 var b := minf(bevel, minf(half.x, minf(half.y, half.z)) * 0.7) var tool := SurfaceTool.new() tool.begin(Mesh.PRIMITIVE_TRIANGLES) tool.set_smooth_group(-1) var rings: Array[PackedVector3Array] = [] for i in 4: var outer: bool = i == 0 or i == 3 var inset := b * 0.6 if outer else 0.0 var hx := half.x - inset var hz := half.z - inset var y: float = [-half.y, -half.y + b, half.y - b, half.y][i] var ring := PackedVector3Array([ Vector3(-hx + b, y, -hz), Vector3(hx - b, y, -hz), Vector3(hx, y, -hz + b), Vector3(hx, y, hz - b), Vector3(hx - b, y, hz), Vector3(-hx + b, y, hz), Vector3(-hx, y, hz - b), Vector3(-hx, y, -hz + b)]) rings.append(ring) for r in 3: for i in 8: var n := (i + 1) % 8 _tri(tool, rings[r][i], rings[r][n], rings[r + 1][n]) _tri(tool, rings[r][i], rings[r + 1][n], rings[r + 1][i]) for i in 8: var n := (i + 1) % 8 _tri(tool, Vector3(0, -half.y, 0), rings[0][n], rings[0][i]) _tri(tool, Vector3(0, half.y, 0), rings[3][i], rings[3][n]) tool.generate_normals() _add(tool.commit(), at, kind, tint, rotation) func _tri(tool: SurfaceTool, a: Vector3, b: Vector3, c: Vector3) -> void: for point in [a, b, c]: tool.set_uv(Vector2(point.x + point.z, point.y + point.z)) tool.add_vertex(point) func _panel(at: Vector3, size: Vector3, kind: String, tint: Color) -> void: # Far facade glazing/mullions are truly flat fabricated panels. Their tiny # edge bevel would be subpixel, so reserve bevel geometry for silhouettes. var mesh := BoxMesh.new() mesh.size = size _add(mesh, at, kind, tint) func _cylinder(at: Vector3, radius: float, height: float, kind: String, tint: Color = Color.WHITE, top_radius: float = -1.0) -> void: var mesh := CylinderMesh.new() mesh.height = height mesh.bottom_radius = radius mesh.top_radius = radius if top_radius < 0 else top_radius mesh.radial_segments = 12 mesh.rings = 1 _add(mesh, at, kind, tint) func _rod(a: Vector3, b: Vector3, radius: float, kind: String, tint: Color) -> void: var mesh := CylinderMesh.new() mesh.height = a.distance_to(b) mesh.top_radius = radius * 0.88 mesh.bottom_radius = radius mesh.radial_segments = 10 mesh.rings = 1 var direction := (b - a).normalized() var rotation := Quaternion(Vector3.UP, direction).get_euler() _add(mesh, (a + b) * 0.5, kind, tint, rotation) func _perimeter() -> void: var stone := Color("a2aaa2") var shadow := Color("455953") var cap := Color("bdc4b9") for side in [-1.0, 1.0]: # Narrow joints divide the existing perimeter into believable precast bays. _box(Vector3(side * 36, 4.41, 0), Vector3(1.16, 0.18, 100), "concrete", cap) _box(Vector3(side * 35.46, 0.36, 0), Vector3(0.12, 0.7, 99.6), "concrete", stone) _box(Vector3(side * 35.48, 3.75, 0), Vector3(0.08, 0.055, 99.8), "metal", shadow, 0.01) for z in range(-47, 49, 5): _box(Vector3(side * 35.45, 2.2, z), Vector3(0.13, 4.2, 0.13), "concrete", stone) _box(Vector3(side * 35.48, 2.2, z + 0.14), Vector3(0.025, 4.05, 0.035), "metal", shadow, 0.005) _box(Vector3(0, 4.41, side * 50), Vector3(72, 0.18, 1.16), "concrete", cap) _box(Vector3(0, 0.36, side * 49.46), Vector3(71.6, 0.7, 0.12), "concrete", stone) for x in range(-33, 36, 6): _box(Vector3(x, 2.2, side * 49.44), Vector3(0.15, 4.2, 0.15), "concrete", stone) _box(Vector3(x + 0.16, 2.2, side * 49.48), Vector3(0.03, 4.05, 0.025), "metal", shadow, 0.005) # The cable is overhead and beyond the arena; its gentle sag breaks straight silhouettes. var cable_color := Color("303c3b") for x in [-24.0, 0.0, 24.0]: _cylinder(Vector3(x, 4.6, -51.3), 0.095, 9.2, "metal", shadow, 0.065) _box(Vector3(x, 8.7, -51.3), Vector3(1.5, 0.085, 0.085), "metal", shadow) for x_start in [-24.0, 0.0]: for offset in [-0.5, 0.5]: for step in 16: var t0 := step / 16.0 var t1 := (step + 1) / 16.0 var a := Vector3(x_start + 24 * t0, 8.85 - sin(t0 * PI) * 1.1, -51.3 + offset) var b := Vector3(x_start + 24 * t1, 8.85 - sin(t1 * PI) * 1.1, -51.3 + offset) _rod(a, b, 0.012, "metal", cable_color) func _streets() -> void: var stone := Color("b3b2a3") var iron := Color("414c4b") for x in [-27.1, -16.9, -5.1, 5.1, 16.9, 27.1]: for interval in [Vector2(-41.5, -34.6), Vector2(-27.4, -3.6), Vector2(3.6, 27.4), Vector2(34.6, 41.5)]: var z: float = interval.x while z < interval.y: var length := minf(1.25, interval.y - z) _box(Vector3(x, 0.055, z + length * 0.5), Vector3(0.22, 0.085, length - 0.02), "concrete", stone, 0.015) z += 1.25 for z in [-24.0, -11.0, 13.0, 25.0]: for x in [-4.68, 4.68]: _box(Vector3(x, 0.052, z), Vector3(0.5, 0.045, 0.85), "metal", iron, 0.015) for n in 8: _box(Vector3(x, 0.078, z - 0.35 + n * 0.1), Vector3(0.4, 0.012, 0.026), "rubber", Color("1c2728"), 0.002) for p in [Vector3(-1.3, 0.044, -17), Vector3(2.1, 0.044, 22), Vector3(-21.7, 0.044, 28), Vector3(22, 0.044, -30)]: _cylinder(p, 0.5, 0.022, "metal", iron) _cylinder(p + Vector3(0, 0.014, 0), 0.45, 0.012, "metal", Color("606762")) for i in 7: var x := (i - 3) * 0.1 var length := sqrt(maxf(0.02, 0.39 * 0.39 - x * x)) * 2 _box(p + Vector3(x, 0.026, 0), Vector3(0.018, 0.008, length), "metal", iron, 0.001) func _platforms() -> void: var edge := Color("b7c1b8") var rail := Color("405a59") for x in [-23.0, 23.0]: _box(Vector3(x, 2.214, 0), Vector3(3.94, 0.026, 4.94), "paving", Color("97a59c"), 0.008) for side in [-1.0, 1.0]: _box(Vector3(x + side * 1.95, 1.7, 0), Vector3(0.05, 0.1, 4.9), "metal", rail, 0.01) for end in [-1.0, 1.0]: for step in 11: var h := (step + 1) * 0.2 var z: float = end * (7.94 - step * 0.5) _box(Vector3(x, h + 0.01, z), Vector3(2.96, 0.023, 0.095), "metal", edge, 0.006) for side in [-1.0, 1.0]: _box(Vector3(x + side * 1.45, h + 0.011, z - end * 0.15), Vector3(0.065, 0.02, 0.19), "paint", Color("bcb28b"), 0.003) _box(Vector3(x + signf(x) * 1.8, 3.311, 0), Vector3(0.42, 0.055, 5.08), "metal", edge, 0.02) _box(Vector3(x - signf(x) * 1.8, 3.014, 0), Vector3(0.42, 0.05, 2.07), "metal", edge, 0.02) func _streetlight(at: Vector3, inward: float) -> void: var steel := Color("4a5c5b") _cylinder(at + Vector3(0, 0.11, 0), 0.22, 0.22, "concrete", Color("a2aaa2")) _cylinder(at + Vector3(0, 2.85, 0), 0.1, 5.7, "metal", steel, 0.055) _cylinder(at + Vector3(0, 0.4, 0), 0.16, 0.57, "metal", steel, 0.105) var previous := at + Vector3(0, 5.7, 0) for step in range(1, 9): var angle := step / 8.0 * PI * 0.5 var next := at + Vector3(inward * (1 - cos(angle)) * 0.8, 5.7 + sin(angle) * 0.8, 0) _rod(previous, next, 0.055, "metal", steel) previous = next _rod(previous, previous + Vector3(inward * 0.7, 0, 0), 0.045, "metal", steel) _box(at + Vector3(inward * 1.6, 6.46, 0), Vector3(0.9, 0.13, 0.36), "metal", steel, 0.06) _box(at + Vector3(inward * 1.6, 6.396, 0), Vector3(0.66, 0.018, 0.25), "glass", Color("f3dfb4"), 0.006) # Inspection hatch and two base bolts make the pole read as manufactured hardware. _box(at + Vector3(0, 0.7, 0.096), Vector3(0.073, 0.25, 0.015), "metal", Color("8a9690"), 0.006) for side in [-1.0, 1.0]: _cylinder(at + Vector3(side * 0.145, 0.245, 0), 0.018, 0.035, "metal", Color("8a9690")) func _skyline() -> void: var colors: Array[Color] = [Color("929e98"), Color("b5ad97"), Color("8c9e9c"), Color("a6aaa0")] for side in [-1.0, 1.0]: var index := 0 for z in [-33.0, -10.0, 14.0, 36.0]: _backdrop_building(Vector3(side * (RelayArena.HALF_WIDTH + 10.5), 0, z), Vector3(11, 10.5 + (index % 3) * 2.7, 15), colors[index], index) index += 1 for x in [-22.0, 1.0, 23.0]: _backdrop_building(Vector3(x, 0, side * (RelayArena.HALF_LENGTH + 12)), Vector3(16, 11 + absf(x) * 0.17, 12), colors[index % 4], index) index += 1 _terrain() func _terrain() -> void: # A continuous heightfield grounds the district; no giant sphere silhouettes. var noise := FastNoiseLite.new() noise.seed = 7831 noise.frequency = 0.014 var surface := SurfaceTool.new() surface.begin(Mesh.PRIMITIVE_TRIANGLES) for x in range(-240, 240, 10): for z in range(-240, 240, 10): var corners: Array[Vector3] = [] for offset in [Vector2(0, 0), Vector2(10, 0), Vector2(10, 10), Vector2(0, 10)]: var px: float = x + offset.x var pz: float = z + offset.y var distance := Vector2(px, pz).length() var hill := smoothstep(85.0, 175.0, distance) * (10 + noise.get_noise_2d(px, pz) * 35) corners.append(Vector3(px, -0.45 + maxf(0, hill), pz)) for index in [0, 1, 2, 0, 2, 3]: surface.set_uv(Vector2(corners[index].x, corners[index].z) * 0.04) surface.add_vertex(corners[index]) surface.generate_normals() _add(surface.commit(), Vector3.ZERO, "ground", Color("858e76")) func _backdrop_building(at: Vector3, size: Vector3, tint: Color, variant: int) -> void: var trim := Color("566c68") var glazing := Color("465b60") _box(at + Vector3(0, size.y * 0.5, 0), size, "plaster", tint, 0.12) _box(at + Vector3(0, size.y + 0.12, 0), Vector3(size.x + 0.24, 0.24, size.z + 0.24), "concrete", Color("bdc4b9"), 0.045) _box(at + Vector3(-size.x * 0.15, size.y + 0.55, 0), Vector3(2.5, 0.95, 2), "metal", trim, 0.08) for floor_index in range(1, int(size.y / 2.65)): var y := floor_index * 2.65 + 0.9 for side in [-1.0, 1.0]: # Recess-coloured glazing, thin concrete sills, and asymmetric service fins. # Omit outer faces nobody in the arena can see. if absf(at.x) < 30 or side == -signf(at.x): for j in range(1, int(size.z / 2.4)): var z := -size.z * 0.5 + j * 2.4 _panel(at + Vector3(side * (size.x * 0.5 + 0.015), y, z), Vector3(0.05, 1.5, 1.6), "glass", glazing) _panel(at + Vector3(side * (size.x * 0.5 + 0.09), y - 0.8, z), Vector3(0.18, 0.1, 1.8), "concrete", Color("bdc4b9")) _panel(at + Vector3(side * (size.x * 0.5 + 0.06), y, z), Vector3(0.07, 1.51, 0.055), "metal", trim) if absf(at.z) < 43 or side == -signf(at.z): for j in range(1, int(size.x / 2.5)): var x := -size.x * 0.5 + j * 2.5 _panel(at + Vector3(x, y, side * (size.z * 0.5 + 0.02)), Vector3(1.7, 1.5, 0.055), "glass", glazing) _panel(at + Vector3(x, y - 0.8, side * (size.z * 0.5 + 0.1)), Vector3(1.87, 0.1, 0.2), "concrete", Color("bdc4b9")) _panel(at + Vector3(x, y, side * (size.z * 0.5 + 0.06)), Vector3(0.055, 1.51, 0.07), "metal", trim) if variant % 2 == 0: for side in [-1.0, 1.0]: _box(at + Vector3(side * size.x * 0.38, size.y * 0.55, -size.z * 0.5 - 0.18), Vector3(0.38, size.y * 0.85, 0.42), "paint", Color("667f79"), 0.04) func _planting() -> void: # All trunks and crowns remain outside the perimeter, preserving target visibility. for at in [Vector3(-39.2, 0, -24), Vector3(-39.4, 0, 6), Vector3(39.7, 0, -6), Vector3(39.4, 0, 28), Vector3(-15, 0, 54), Vector3(14, 0, -54)]: _tree(at, _rng.randf_range(0.9, 1.13)) func _tree(at: Vector3, size: float) -> void: var bark := Color("6d6b55") var foliage: Array[Color] = [Color("647b51"), Color("7f8d60"), Color("536d4f")] _rod(at, at + Vector3(0.17, 5.5, 0.12) * size, 0.22 * size, "wood", bark) for i in 8: var angle := i * 2.39996 var height := 4.8 + i * 0.26 var offset := Vector3(cos(angle) * _rng.randf_range(0.8, 1.7), height, sin(angle) * _rng.randf_range(0.7, 1.7)) _rod(at + Vector3(0.1, 3.6 + i * 0.23, 0.1) * size, at + offset * size, 0.075 * size, "wood", bark) var crown := _crown_mesh(i) _add(crown, at + offset * size, "foliage", foliage[i % 3], Vector3(0, angle, 0), Vector3(1.45, 1.3, 1.4) * size) func _crown_mesh(seed_value: int) -> ArrayMesh: # Deformed, layered crowns replace primitive green balls. Geometry-only leaves # avoid transparent alpha overdraw in the compatibility renderer. var mesh := SphereMesh.new() mesh.radius = 1 mesh.height = 2 mesh.radial_segments = 14 mesh.rings = 9 var arrays := mesh.get_mesh_arrays() var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] var noise := FastNoiseLite.new() noise.seed = 601 + seed_value noise.frequency = 3.7 for i in vertices.size(): var point := vertices[i] vertices[i] = point * (1.0 + noise.get_noise_3dv(point) * 0.28 + sin(point.y * 19.0) * 0.045) arrays[Mesh.ARRAY_VERTEX] = vertices var altered := ArrayMesh.new() altered.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) var tool := SurfaceTool.new() tool.create_from(altered, 0) tool.generate_normals() return tool.commit()