extends TestLevelBuilder ## Akiba Crossing — a 10x10-block Akihabara-styled city (100 blocks). ## ## The map is a street grid: 48m buildable cells on a 64m pitch with 16m ## streets. A hand-authored layout assigns each cell a block type so the ## city stays varied ("S" retail is the fabric; landmarks break it up): ## S standard retail T landmark tower J shrine courtyard ## M market street P plaza (2x2 centre = the Crossing) C construction ## A brick rail viaduct runs east-west across the whole city, and a brick ## wall rings the district. ## ## Everything is parametric: buildings derive parapets, storefronts, ## canopies, per-floor sign bands, window bands, corner sign columns, roof ## AC/water tanks/antennas from their footprint; per-block variation comes ## from a seeded RNG so no two blocks repeat exactly. ## ## Collision & acoustics: bodies/canopies/columns/furniture are static and ## tagged (concrete/glass/wood/brick/metal); pure decoration (signs, ## windows, wires) is render-only to keep physics light. const BLOCK := 48.0 const STREET := 16.0 const PITCH := BLOCK + STREET const GRID := 10 const CITY_HALF := GRID * PITCH * 0.5 # 320 const FLOOR_H := 4.0 const RAIL_ROW := 2 # viaduct runs along the street south of row 2 const RAIL_TOP := 9.0 const LAYOUT: Array[String] = [ "SMSTSSMSTS", "STSSJSSCSS", "SSMSPSSTSM", "MSTSSSTSSS", "SSSTPPTSMS", "SJSTPPTSSS", "SSSSTPTSJS", "TSMSSSSSTS", "SSJSSTSSMS", "SMSSTSMSST", ] # Palette const ASPHALT := Color(0.38, 0.38, 0.45) const PAVING := Color(0.56, 0.54, 0.6) const STRIPE := Color(0.72, 0.72, 0.75) const FACADES: Array[Color] = [ Color(0.74, 0.71, 0.66), Color(0.62, 0.63, 0.67), Color(0.68, 0.6, 0.5), Color(0.55, 0.6, 0.66), Color(0.71, 0.66, 0.6), Color(0.6, 0.55, 0.58), ] const SEGA_RED := Color(0.78, 0.16, 0.14) const ELEC_BLUE := Color(0.2, 0.38, 0.62) const GLASS := Color(0.62, 0.8, 0.84) const WINDOW := Color(0.16, 0.2, 0.28) const CANOPY := Color(0.5, 0.34, 0.22) const METAL := Color(0.44, 0.48, 0.54) const BRICK := Color(0.5, 0.34, 0.3) const WOOD := Color(0.52, 0.34, 0.2) const VERMILION := Color(0.88, 0.26, 0.16) const LEAF := Color(0.3, 0.55, 0.32) const TRUNK := Color(0.4, 0.28, 0.18) const SIGN_COLORS: Array[Color] = [ Color(1.0, 0.3, 0.5), Color(0.25, 0.9, 0.95), Color(1.0, 0.8, 0.2), Color(0.6, 0.45, 1.0), Color(0.35, 1.0, 0.55), Color(0.95, 0.95, 0.98), ] var _spawn_points: Array[Vector3] = [] var _rng := RandomNumberGenerator.new() # ── Kenney City Kit catalog (CC0) — base sizes measured at import scale ───── const KIT_DIR := "res://assets/props/citykit/" const KIT_SCALE := 13.0 ## name -> Vector3(facade_width, height, depth) at import scale const KIT_BUILDINGS := { "building-a": Vector3(0.884, 1.293, 0.94), "building-b": Vector3(0.97, 1.293, 0.94), "building-c": Vector3(0.884, 0.893, 1.09), "building-d": Vector3(0.84, 1.293, 0.9), "building-e": Vector3(1.64, 0.893, 1.008), "building-f": Vector3(0.84, 1.693, 1.03), "building-g": Vector3(0.97, 1.693, 0.922), "building-h": Vector3(0.884, 1.293, 1.008), "building-i": Vector3(1.24, 1.68, 1.302), "building-j": Vector3(2.084, 1.693, 1.34), "building-k": Vector3(2.084, 1.47, 0.942), "building-l": Vector3(1.37, 2.27, 1.402), "building-n": Vector3(2.32, 2.48, 1.82), } const KIT_SKYSCRAPERS := { "building-skyscraper-a": Vector3(1.36, 2.88, 1.36), "building-skyscraper-b": Vector3(1.36, 4.48, 1.36), "building-skyscraper-c": Vector3(1.28, 4.08, 1.388), "building-skyscraper-d": Vector3(1.28, 5.47, 1.388), "building-skyscraper-e": Vector3(1.295, 4.08, 1.242), } var _kit_cache: Dictionary = {} func _kit_scene(key: String) -> PackedScene: if not _kit_cache.has(key): _kit_cache[key] = load(KIT_DIR + key + ".glb") return _kit_cache[key] ## Combined rendered bounds expressed in an ancestor's local space. Imported ## kit dimensions are useful for layout, but collision must follow the actual ## imported geometry rather than a rounded catalog number. func _visual_bounds_in(node: Node, relative_to: Node3D) -> AABB: var result := AABB() var has_bounds := false var to_local := relative_to.global_transform.affine_inverse() for child in node.find_children("*", "MeshInstance3D", true, false): var mesh_instance := child as MeshInstance3D if mesh_instance.mesh == null: continue var transformed := _transformed_aabb( mesh_instance.get_aabb(), to_local * mesh_instance.global_transform) if has_bounds: result = result.merge(transformed) else: result = transformed has_bounds = true return result if has_bounds else AABB() func _transformed_aabb(bounds: AABB, xform: Transform3D) -> AABB: var result := AABB(xform * bounds.position, Vector3.ZERO) for x in 2: for y in 2: for z in 2: var corner := bounds.position + Vector3( bounds.size.x * float(x), bounds.size.y * float(y), bounds.size.z * float(z)) result = result.expand(xform * corner) return result func _fit_box_collider(body: StaticBody3D, visual: Node, horizontal_inset: float = 0.04) -> CollisionShape3D: var bounds := _visual_bounds_in(visual, body) var shape := CollisionShape3D.new() shape.name = "CollisionShape3D" var box := BoxShape3D.new() box.size = Vector3( maxf(0.05, bounds.size.x - horizontal_inset), maxf(0.05, bounds.size.y - 0.02), maxf(0.05, bounds.size.z - horizontal_inset)) shape.shape = box shape.position = bounds.get_center() body.add_child(shape) body.set_meta("mesh_fitted_collision", true) return shape ## Exact collision for imported art. One enclosing AABB is not mesh fitting: ## a window sill or roof overhang moves its face away from the playable wall ## and creates an invisible wallrun/bullet plane. Static concave shapes ## preserve every recess, opening and overhang in the rendered mesh. func _fit_mesh_colliders(body: StaticBody3D, visual: Node) -> int: var count := 0 var to_body := body.global_transform.affine_inverse() for child in visual.find_children("*", "MeshInstance3D", true, false): var mesh_instance := child as MeshInstance3D if mesh_instance.mesh == null: continue var triangle_shape := mesh_instance.mesh.create_trimesh_shape() if triangle_shape == null: continue var collision := CollisionShape3D.new() collision.name = "ExactMeshCollision_%03d" % count collision.shape = triangle_shape body.add_child(collision) collision.transform = to_body * mesh_instance.global_transform collision.set_meta("source_mesh_path", str(mesh_instance.get_path())) count += 1 if count == 0: _fit_box_collider(body, visual, 0.06) body.set_meta("exact_collision_fallback", true) else: body.set_meta("mesh_exact_collision", true) body.set_meta("exact_collision_count", count) return count # ── Hero assets (original kitbash, tools/build_hero_assets.py) ────────────── ## Hero piece with exact imported-mesh collision. Front faces -Z before yaw. func _hero(key: String, pos: Vector3, yaw_deg: float, _col_size: Vector3, _col_center_y: float, acoustic: String = "concrete") -> void: var body := StaticBody3D.new() body.name = "Hero_" + key + "_%d" % _rng.randi() body.set_meta("acoustic_material", acoustic) add_child(body) body.global_position = pos body.rotation_degrees.y = yaw_deg var inst: Node3D = _kit_scene_at("res://assets/props/hero/" + key + ".glb").instantiate() body.add_child(inst) LevelMaterials.apply_toon_recursive(inst, 0.005) # Authored origins differ between hero pieces. Seat the rendered bottom on # the requested support plane before deriving collision from that geometry. var bounds := _visual_bounds_in(inst, body) inst.position.y -= bounds.position.y _fit_mesh_colliders(body, inst) ## Small visual-only kit prop (parasols, awnings): no collider needed. func _kit_prop(key: String, pos: Vector3, yaw_deg: float, scale_f: float) -> void: var inst: Node3D = _kit_scene(key).instantiate() inst.scale = Vector3.ONE * scale_f add_child(inst) inst.global_position = pos inst.rotation_degrees.y = yaw_deg inst.set_meta("source_asset_path", KIT_DIR + key + ".glb") LevelMaterials.apply_toon_recursive(inst, 0.007) var bounds := _visual_bounds_in(inst, self) inst.global_position.y += pos.y - bounds.position.y inst.set_meta("ground_fitted", true) ## Place a kit model with exact per-mesh collision. ## front_pos: centre of the facade at ground level. yaw_deg: 0 faces +Z. func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: String, scale_f: float = KIT_SCALE) -> float: var dims: Vector3 = KIT_BUILDINGS.get(key, KIT_SKYSCRAPERS.get(key, Vector3.ONE)) var w := dims.x * scale_f var d := dims.z * scale_f var fwd := Vector3(sin(deg_to_rad(yaw_deg)), 0, cos(deg_to_rad(yaw_deg))) var center := front_pos - fwd * d * 0.5 var body := StaticBody3D.new() body.name = node_name body.set_meta("acoustic_material", "concrete") add_child(body) body.global_position = center body.rotation_degrees.y = yaw_deg var inst: Node3D = _kit_scene(key).instantiate() inst.scale = Vector3.ONE * scale_f body.add_child(inst) LevelMaterials.apply_toon_recursive(inst, 0.0) # Subtle per-building tint so repeated kit models read as different shops _tint_recursive(inst, Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.3, 1.0), 0.16)) _fit_mesh_colliders(body, inst) return w func _tint_recursive(node: Node, tint: Color) -> void: if node is MeshInstance3D: var mi := node as MeshInstance3D var sc: int = mi.mesh.get_surface_count() if mi.mesh else 0 for s in sc: var m := mi.get_surface_override_material(s) if m is ShaderMaterial: var c = m.get_shader_parameter("albedo_color") if c is Color: m.set_shader_parameter("albedo_color", c * tint) for child in node.get_children(): _tint_recursive(child, tint) ## Fill one street-facing edge of a lot with kit buildings + Akiba signage. func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: String) -> void: var yaw := -90.0 if dir > 0 else 90.0 # face the street (-x for east row) var cursor := z0 var idx := 0 var keys := KIT_BUILDINGS.keys() while z1 - cursor > 10.0: var key: String = keys[_rng.randi() % keys.size()] var w: float = KIT_BUILDINGS[key].x * KIT_SCALE if w > z1 - cursor: # find any model that still fits, else stop var fits := false for k2 in keys: if KIT_BUILDINGS[k2].x * KIT_SCALE <= z1 - cursor: key = k2 w = KIT_BUILDINGS[k2].x * KIT_SCALE fits = true break if not fits: break var zc := cursor + w * 0.5 var h: float = KIT_BUILDINGS[key].y * KIT_SCALE _kit_building(key, Vector3(front_x, 0, zc), yaw, "K%s_%d" % [seed_id, idx]) var face_yaw := -90.0 if dir > 0 else 90.0 # ad quads face the street # Kitbashed konbini storefront module on wide buildings if w >= 12.0 and _rng.randi() % 3 == 0: _hero("konbini", Vector3(front_x - float(dir) * 0.75, 0.02, zc), 90.0 * float(dir), Vector3(8.2, 4.2, 1.4), 2.1, "glass") # Storefront awning on some other buildings (kit detail piece) elif _rng.randi() % 5 < 2: _kit_prop_path(KIT_DIR + "detail-awning-wide.glb", Vector3(front_x, 3.1, zc), face_yaw, minf(w * 0.9, 11.0)) # Street-level posters plastered on the facade (graphic density) var posters := 1 + _rng.randi() % 3 for p in posters: var pz := cursor + _rng.randf_range(1.5, maxf(w - 1.5, 2.0)) _ad_quad(Vector3(front_x - float(dir) * 0.15, _rng.randf_range(1.7, 2.4), pz), Vector2(1.3, 2.0) * _rng.randf_range(0.8, 1.1), face_yaw, _poster_tex()) # Protruding double-sided shop signs over the sidewalk (2nd floor) if h >= 8.0: var signs := 1 + _rng.randi() % 2 for sp in signs: var sz := cursor + w * (float(sp) + 0.7) / (float(signs) + 0.7) var spos := Vector3(front_x - float(dir) * 0.9, 5.6 + _rng.randf_range(-0.4, 0.6), sz) var stx := _poster_tex() _deco_box(spos + Vector3(float(dir) * 0.75, 0, 0), Vector3(1.6, 0.12, 0.12), METAL.darkened(0.2)) _ad_quad(spos + Vector3(0.06, 0, 0), Vector2(1.2, 1.7), 90.0, stx) _ad_quad(spos + Vector3(-0.06, 0, 0), Vector2(1.2, 1.7), -90.0, stx) # Akiba signage layer, aligned to the model's measured bounds if h >= 14.0: var zs := cursor + 1.2 var col_h := h * 0.55 _box_static(Vector3(front_x - float(dir) * 0.45, h * 0.35, zs), Vector3(0.7, col_h, 1.4), METAL, "K%s_%d_sc" % [seed_id, idx], "metal") _ad_quad(Vector3(front_x - float(dir) * 0.84, h * 0.35, zs), Vector2(1.25, col_h * 0.9), face_yaw, _column_tex()) if h >= 20.0: var bw := minf(w - 4.0, 10.0) _box_static(Vector3(front_x + float(dir) * 2.0, h + 1.9, zc), Vector3(0.5, 3.0, bw), METAL, "K%s_%d_bf" % [seed_id, idx], "metal") _ad_quad(Vector3(front_x + float(dir) * 1.7, h + 1.9, zc), Vector2(bw - 0.6, 2.5), face_yaw, _board_tex()) cursor += w idx += 1 # ── Cel-styled statics + render-only decoration ────────────────────────────── func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String = "", acoustic: String = "") -> StaticBody3D: var body := super._box_static(pos, size, color, node_name, acoustic) var law := _surface_law(size, node_name, acoustic) for mi in body.find_children("*", "MeshInstance3D", false, false): mi.mesh.surface_set_material(0, LevelMaterials.flat(color, law)) if maxf(size.x, maxf(size.y, size.z)) < 10.0: var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.003, 0.007, 0.018) mi.material_overlay = LevelMaterials.outline(w) return body ## Choose a construction law from what the object is, not from its colour. ## This is the map-side half of LevelMaterials.SURFACE_LAW: it finally applies ## the shared surfacing kit to the actual city instead of only to the fidelity ## probe. Thin horizontal slabs read as paving, structural masonry gets the ## storey-scale wall grid, and machined metal gets tighter panels. func _surface_law(size: Vector3, node_name: String, acoustic: String) -> String: var footprint := minf(absf(size.x), absf(size.z)) if node_name == "Ground" or (absf(size.y) <= 0.5 and footprint >= 2.0): return "ground" match acoustic: "concrete", "brick": if absf(size.y) >= 3.0 and maxf(absf(size.x), absf(size.z)) >= 4.0: return "wall" return "trim" "metal": return "panel" "wood": return "trim" return "" # ── Ad graphics (generated library: tools/generate_ads.py) ────────────────── var _ad_mats: Dictionary = {} func _ad_material(path: String) -> StandardMaterial3D: if _ad_mats.has(path): return _ad_mats[path] var m := StandardMaterial3D.new() m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED var tex: Texture2D = load(path) m.albedo_texture = tex m.emission_enabled = true m.emission_texture = tex m.emission_energy_multiplier = 0.9 _ad_mats[path] = m return m ## A lit ad panel (poster/billboard/sign face). Quad faces +Z before yaw. func _ad_quad(pos: Vector3, size: Vector2, yaw_deg: float, tex_path: String) -> void: var mi := MeshInstance3D.new() var qm := QuadMesh.new() qm.size = size qm.material = _ad_material(tex_path) mi.mesh = qm add_child(mi) mi.global_position = pos mi.rotation_degrees.y = yaw_deg func _poster_tex() -> String: return "res://assets/textures/ads/ad_%02d.png" % (_rng.randi() % 10) func _board_tex() -> String: return "res://assets/textures/ads/board_%02d.png" % (_rng.randi() % 8) func _column_tex() -> String: return "res://assets/textures/ads/column_%02d.png" % (_rng.randi() % 6) func _deco_box(pos: Vector3, size: Vector3, color: Color, emissive: bool = false) -> void: var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = size if emissive: var m := StandardMaterial3D.new() m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED m.albedo_color = color m.emission_enabled = true m.emission = color m.emission_energy_multiplier = 2.2 bm.material = m else: bm.material = LevelMaterials.flat(color) mi.mesh = bm mi.position = pos add_child(mi) # ── Environment ────────────────────────────────────────────────────────────── func _build_environment() -> void: var env := LevelEnvironment.add_to(self, "sunset") # Calibrated for the kit's bright albedo textures (they clip to white at # the energies the old dark flat palette needed). env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.environment.ambient_light_color = Color(0.58, 0.54, 0.66) # Under the filmic shoulder the ambient's lavender tint actually shows # (LINEAR used to clip it to white) — keep it lower so the sun shapes # surfaces and lit faces stay bright. env.environment.ambient_light_energy = 0.92 env.environment.fog_density = 0.0007 # light city haze for depth over 600m env.environment.fog_light_color = Color(0.58, 0.43, 0.66) # Ambient occlusion grounds the modeled facade detail (ZZZ-style depth) env.environment.ssao_enabled = true env.environment.ssao_intensity = 2.0 var sun := get_node_or_null("Sun") if sun: sun.light_energy = 1.85 sun.rotation_degrees = Vector3(-38, 55, 0) sun.directional_shadow_max_distance = 220.0 func _build_geometry() -> void: _build_environment() _build_lighting() # The play volume: just outside the perimeter wall, and tall enough to clear # the skyscrapers. Without one a player who grappled over that wall fell out # of the world with no warning and no death. CombatArea.add_to(self, Vector3(CITY_HALF * 2.0 + 26.0, 220.0, CITY_HALF * 2.0 + 26.0), Vector3(0, 96.0, 0)) # Ground: one asphalt slab for the whole district _box_static(Vector3(0, -0.5, 0), Vector3(CITY_HALF * 2.0 + 16.0, 1.0, CITY_HALF * 2.0 + 16.0), ASPHALT, "Ground") # Perimeter wall for i in 4: var horiz := i < 2 var s := 1.0 if i % 2 == 0 else -1.0 var p := (CITY_HALF + 6.0) * s _box_static(Vector3(0.0 if horiz else p, 8.0, p if horiz else 0.0), Vector3(CITY_HALF * 2.0 + 14.0 if horiz else 1.0, 16.0, 1.0 if horiz else CITY_HALF * 2.0 + 14.0), BRICK.darkened(0.1), "CityWall_%d" % i, "brick") for bz in GRID: for bx in GRID: _build_block(bx, bz) _build_rail_line() _build_streets() _build_avenues() _build_parked_cars() if _spawn_points.is_empty(): _spawn_points.append(Vector3(0, 2, 0)) func _spawn_player(pid: int) -> CharacterBody3D: var player := super._spawn_player(pid) var s: Vector3 = _spawn_points[randi() % _spawn_points.size()] player.position = s + Vector3(randf_range(-1.0, 1.0), 0, randf_range(-1.0, 1.0)) player.rotation.y = atan2(player.position.x, player.position.z) return player # ── Grid plumbing ──────────────────────────────────────────────────────────── func _cell_center(bx: int, bz: int) -> Vector3: return Vector3((float(bx) - float(GRID - 1) * 0.5) * PITCH, 0.0, (float(bz) - float(GRID - 1) * 0.5) * PITCH) func _build_block(bx: int, bz: int) -> void: var c := _cell_center(bx, bz) _rng.seed = hash(Vector2i(bx, bz)) + 77 var t := LAYOUT[bz][bx] # Sidewalk apron under the whole cell _box_static(c + Vector3(0, 0.08, 0), Vector3(BLOCK + 4.0, 0.16, BLOCK + 4.0), PAVING, "Walk_%d_%d" % [bx, bz]) match t: "S": _standard_block(c, bx, bz) "T": _tower_block(c, bx, bz) "J": _shrine_block(c, bx, bz) "M": _market_block(c, bx, bz) "P": _plaza_block(c, bx, bz) "C": _construction_block(c, bx, bz) # ── Buildings (shared parametric unit) ─────────────────────────────────────── ## front_x: world x of the facade plane. dir: +1 body extends +x, -1 extends -x. func _building(front_x: float, dir: int, z0: float, z1: float, depth: float, floors: int, facade: Color, id: String) -> void: var w := z1 - z0 var zc := (z0 + z1) * 0.5 var h := float(floors) * FLOOR_H var cx := front_x + float(dir) * depth * 0.5 var n := "B" + id _box_static(Vector3(cx, h * 0.5, zc), Vector3(depth, h, w), facade, n, "concrete") # Parapet ring var pt := 0.5 _box_static(Vector3(front_x + float(dir) * 0.2, h + pt * 0.5, zc), Vector3(0.4, pt, w), facade.darkened(0.15), n + "pf") _box_static(Vector3(front_x + float(dir) * (depth - 0.2), h + pt * 0.5, zc), Vector3(0.4, pt, w), facade.darkened(0.15), n + "pb") for e in 2: _box_static(Vector3(cx, h + pt * 0.5, (z0 + 0.2) if e == 0 else (z1 - 0.2)), Vector3(depth, pt, 0.4), facade.darkened(0.15), n + "ps%d" % e) # Ground floor: glass storefront + canopy _box_static(Vector3(front_x - float(dir) * 0.12, 2.0, zc), Vector3(0.25, 3.0, w - 1.6), GLASS, n + "g", "glass") _box_static(Vector3(front_x - float(dir) * 1.1, 3.7, zc), Vector3(2.2, 0.2, w - 1.0), CANOPY, n + "c", "wood") # Upper floors: sign band + window band per floor (render-only) for f in range(1, floors): var y := float(f) * FLOOR_H _deco_box(Vector3(front_x - float(dir) * 0.1, y + 0.9, zc), Vector3(0.16, 1.3, w - 2.6), SIGN_COLORS[(f + id.hash()) % SIGN_COLORS.size()], true) _deco_box(Vector3(front_x - float(dir) * 0.08, y + 2.6, zc), Vector3(0.12, 1.4, w - 2.0), WINDOW) # Corner sign column with glowing segments if floors >= 3: var zs := z0 + 1.3 var col_h := h - FLOOR_H _box_static(Vector3(front_x - float(dir) * 0.45, FLOOR_H + col_h * 0.5 - 1.0, zs), Vector3(0.7, col_h, 1.4), METAL, n + "sc", "metal") for s2 in floors - 1: var sy := FLOOR_H - 1.0 + 0.6 + (col_h - 1.2) * (float(s2) + 0.5) / float(floors - 1) _deco_box(Vector3(front_x - float(dir) * 0.85, sy, zs), Vector3(0.12, 2.2, 1.2), SIGN_COLORS[(s2 + 1 + id.hash()) % SIGN_COLORS.size()], true) # Roof furniture: AC units always; water tank or antenna by seed var count := maxi(1, int(w / 8.0)) for a in count: var za := z0 + w * (float(a) + 0.5) / float(count) _box_static(Vector3(cx + float(dir) * 1.5, h + 0.8, za), Vector3(1.7, 1.6, 1.7), METAL, n + "ac%d" % a, "metal") match _rng.randi() % 3: 0: # water tank on legs _box_static(Vector3(cx - float(dir) * 2.5, h + 1.9, zc), Vector3(2.4, 2.4, 2.4), METAL.lightened(0.15), n + "wt", "metal") 1: # antenna mast (grapple pole) _box_static(Vector3(cx - float(dir) * 2.5, h + 2.6, zc), Vector3(0.3, 5.2, 0.3), METAL, n + "an", "metal") _: pass # Rooftop billboard on tall buildings if floors >= 4: var bw := minf(w - 4.0, 10.0) _box_static(Vector3(front_x + float(dir) * 2.0, h + 2.1, zc), Vector3(0.5, 3.2, bw), METAL, n + "bf", "metal") _deco_box(Vector3(front_x + float(dir) * 1.7, h + 2.1, zc), Vector3(0.12, 2.6, bw - 0.6), SIGN_COLORS[id.hash() % SIGN_COLORS.size()], true) # ── Block types ────────────────────────────────────────────────────────────── ## Retail fabric: two building pairs back-to-back facing the E/W streets, ## seeded heights/colors; ~half the blocks get a mid-block alley with fire ## escapes (wallrun + climb route). func _standard_block(c: Vector3, bx: int, bz: int) -> void: var half := BLOCK * 0.5 # Detailed kit buildings fill both street-facing edges _kit_fill_side(c.x - half, 1, c.z - half, c.z + half, "%d_%dW" % [bx, bz]) _kit_fill_side(c.x + half, -1, c.z - half, c.z + half, "%d_%dE" % [bx, bz]) # A supported mid-block fire-escape scaffold on some blocks. The old # version was two unrelated slabs suspended in open air. if _rng.randi() % 2 == 0: var escape_h := FLOOR_H * 2.0 - 0.4 for ex in [-1.1, 1.1]: for ez in [2.9, 5.1]: _box_static(c + Vector3(ex, escape_h * 0.5, ez), Vector3(0.12, escape_h, 0.12), METAL.darkened(0.18), "EscPost_%d_%d_%d_%d" % [bx, bz, int(ex), int(ez)], "metal") for f in range(1, 3): var y := float(f) * FLOOR_H - 0.4 _box_static(c + Vector3(0, y, 4.0), Vector3(2.6, 0.16, 2.4), METAL, "EscLanding_%d_%d_%d" % [bx, bz, f], "metal") var stair_angle := rad_to_deg(atan2(FLOOR_H, 4.0)) _ramp_static(c + Vector3(0, FLOOR_H * 0.45, 4.0), Vector3(1.2, 0.16, sqrt(FLOOR_H * FLOOR_H + 16.0)), Vector3(stair_angle, 0, 0), METAL.darkened(0.08), "EscStairGround_%d_%d" % [bx, bz], "metal") _ramp_static(c + Vector3(0, FLOOR_H + 1.6, 4.0), Vector3(1.2, 0.16, sqrt(FLOOR_H * FLOOR_H + 16.0)), Vector3(-stair_angle, 0, 0), METAL.darkened(0.08), "EscStair_%d_%d" % [bx, bz], "metal") # Sidewalk life: phone booth on some corners, bike rows by the fronts if _rng.randi() % 4 == 0: _hero("phonebooth", c + Vector3(-half - 0.6, 0.1, -half - 0.6), _rng.randf_range(0.0, 360.0), Vector3(1.2, 2.7, 1.2), 1.35, "glass") if _rng.randi() % 2 == 0: var bikes := 2 + _rng.randi() % 3 var bside := 1.0 if _rng.randi() % 2 == 0 else -1.0 for b in bikes: _kit_prop_path("res://assets/props/hero/bicycle.glb", c + Vector3(bside * (half + 1.0), 0.1, -6.0 + float(b) * 1.5), 90.0 * bside + _rng.randf_range(-14.0, 14.0), 1.0) if _rng.randi() % 3 == 0: _spawn_points.append(c + Vector3(0, 1.5, 0)) ## Landmark: an 8-10 floor tower with a retail podium and mega-billboards. func _tower_block(c: Vector3, bx: int, bz: int) -> void: var n := "Tower_%d_%d" % [bx, bz] var col := SEGA_RED if _rng.randi() % 3 == 0 else (ELEC_BLUE if _rng.randi() % 2 == 0 else FACADES[_rng.randi() % FACADES.size()]) # Podium (1 floor, walkable roof ring) with glass front _box_static(c + Vector3(0, FLOOR_H * 0.5, 0), Vector3(BLOCK - 4.0, FLOOR_H, BLOCK - 4.0), col.darkened(0.15), n + "_Podium", "concrete") _box_static(c + Vector3(0, 2.0, -(BLOCK - 4.0) * 0.5 + 0.1), Vector3(BLOCK - 8.0, 3.0, 0.25), GLASS, n + "_Glass", "glass") # Detailed kit skyscraper on the podium var keys := KIT_SKYSCRAPERS.keys() var key: String = keys[_rng.randi() % keys.size()] var dims: Vector3 = KIT_SKYSCRAPERS[key] var s := (BLOCK - 22.0) / dims.x # shaft footprint ~26m var h := dims.y * s + FLOOR_H var yaw := float(90 * (_rng.randi() % 4)) var body := StaticBody3D.new() body.name = n body.set_meta("acoustic_material", "concrete") add_child(body) body.global_position = c + Vector3(0, FLOOR_H, 0) body.rotation_degrees.y = yaw var inst: Node3D = _kit_scene(key).instantiate() inst.scale = Vector3.ONE * s body.add_child(inst) LevelMaterials.apply_toon_recursive(inst, 0.0) _fit_mesh_colliders(body, inst) # Rooftop crown billboard both directions (skyline wayfinding) var tw := dims.x * s _box_static(c + Vector3(0, h + 2.4, 0), Vector3(0.6, 4.0, tw - 4.0), METAL, n + "_Crown", "metal") _ad_quad(c + Vector3(0.42, h + 2.4, 0), Vector2(tw - 4.6, 3.3), 90.0, _board_tex()) _ad_quad(c + Vector3(-0.42, h + 2.4, 0), Vector2(tw - 4.6, 3.3), -90.0, _board_tex()) # Giant mid-height wall ad on the tower's street faces (ZZZ-style) var wall_off := dims.z * s * 0.5 + 0.15 _ad_quad(c + Vector3(0, FLOOR_H + dims.y * s * 0.45, -wall_off).rotated(Vector3.UP, deg_to_rad(yaw)), Vector2(tw * 0.55, tw * 0.8), yaw + 180.0, _poster_tex()) _spawn_points.append(c + Vector3(0, FLOOR_H + 1.5, (BLOCK - 4.0) * 0.4)) ## Shrine courtyard: torii, hall with sloped roof, lanterns, trees — the ## quiet green break in the city fabric. func _shrine_block(c: Vector3, bx: int, bz: int) -> void: var n := "Shrine_%d_%d" % [bx, bz] # Courtyard paving + low wall ring _box_static(c + Vector3(0, 0.12, 0), Vector3(BLOCK - 6.0, 0.1, BLOCK - 6.0), PAVING.lightened(0.1), n + "_Court") for i in 4: var horiz := i < 2 var s := 1.0 if i % 2 == 0 else -1.0 var off := (BLOCK - 6.0) * 0.5 * s _box_static(c + Vector3(0.0 if horiz else off, 0.7, off if horiz else 0.0), Vector3(BLOCK - 6.0 if horiz else 0.6, 1.4, 0.6 if horiz else BLOCK - 6.0), Color(0.55, 0.55, 0.52), n + "_Wall%d" % i, "brick") # Kitbashed torii (curved kasagi) at the south entry; pillar colliders # Local south-edge offset. This used to include c.z and was then added to c # a second time, launching shrine gates/fences far outside their blocks. var tz := (BLOCK - 6.0) * 0.5 _hero("torii", c + Vector3(0, 0.1, tz), 0.0, Vector3(0.1, 0.1, 0.1), 0.05, "wood") _box_static(c + Vector3(-4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarW", "wood").visible = false _box_static(c + Vector3(4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarE", "wood").visible = false # Hall with slide roof _box_static(c + Vector3(0, 2.0, -10.0), Vector3(12.0, 4.0, 8.0), WOOD.darkened(0.2), n + "_Hall", "wood") _ramp_static(c + Vector3(0, 4.9, -8.2), Vector3(13.0, 0.3, 5.6), Vector3(-24, 0, 0), VERMILION.darkened(0.35), n + "_Roof", "wood") # Stone + hanging lanterns, wooden gate fence, real modeled trees for i in 2: var lx := -8.0 + 16.0 * float(i) _box_static(c + Vector3(lx, 1.0, 8.0), Vector3(1.0, 2.0, 1.0), Color(0.55, 0.55, 0.52), n + "_Lant%d" % i, "brick") _kit_prop_path("res://assets/props/town/lantern.glb", c + Vector3(lx, 2.0, 8.0), 0.0, 2.6) _box_static(c + Vector3(lx, 1.6, -2.0), Vector3(0.8, 3.2, 0.8), TRUNK, n + "_Trunk%d" % i, "wood") _kit_prop_path("res://assets/props/nature/" + ["tree_detailed.glb", "tree_default.glb"][i % 2], c + Vector3(lx, 0, -2.0), _rng.randf_range(0.0, 360.0), 7.0) _kit_prop_path("res://assets/props/nature/tree_oak.glb", c + Vector3(0, 0, 2.0), _rng.randf_range(0.0, 360.0), 6.0) # Fence gate flanking the torii approach _kit_prop_path("res://assets/props/town/fence.glb", c + Vector3(-8.0, 0.15, tz - 0.2), 0.0, 3.0) _kit_prop_path("res://assets/props/town/fence.glb", c + Vector3(8.0, 0.15, tz - 0.2), 0.0, 3.0) _spawn_points.append(c + Vector3(0, 1.5, 12)) ## Market street: two dense stall rows with lantern strings — low cover maze. func _market_block(c: Vector3, bx: int, bz: int) -> void: var n := "Mkt_%d_%d" % [bx, bz] # Back walls: detailed kit buildings on east/west faces _kit_fill_side(c.x - BLOCK * 0.5, 1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, "%d_%dmw" % [bx, bz]) _kit_fill_side(c.x + BLOCK * 0.5, -1, c.z - BLOCK * 0.5, c.z + BLOCK * 0.5, "%d_%dme" % [bx, bz]) # Cafe parasols scattered among the stalls for pi in 3: var pp := c + Vector3(_rng.randf_range(-9.0, 9.0), 0.1, _rng.randf_range(-12.0, 12.0)) _kit_prop(["detail-parasol-a", "detail-parasol-b"][pi % 2], pp, _rng.randf_range(0.0, 360.0), 6.0) # Modeled stall rows down the middle (kit stalls + carts, box colliders) var stall_keys := ["stall", "stall-green", "stall-red", "stall-bench"] for r in 2: var x := c.x + (-5.0 if r == 0 else 5.0) for s in 4: var z := c.z - 15.0 + float(s) * 10.0 var body := _box_static(Vector3(x, 1.3, z), Vector3(3.4, 2.6, 3.0), WOOD, "%s_S%d_%d" % [n, r, s], "wood") for mi in body.find_children("*", "MeshInstance3D", false, false): mi.visible = false # collider only; the kit stall is the visual _kit_prop_path("res://assets/props/town/%s.glb" % stall_keys[(r * 4 + s + bx) % stall_keys.size()], Vector3(x, 0.02, z), 90.0 if r == 0 else -90.0, 3.2) if s % 2 == 0: _kit_prop_path("res://assets/props/town/lantern.glb", Vector3(x + 2.2, 0.02, z + 1.8), 0.0, 3.0) # Lantern string over each row for l in 6: _deco_box(Vector3(x, 3.6, c.z - 17.5 + float(l) * 7.0), Vector3(0.32, 0.45, 0.32), Color(1.0, 0.62, 0.25), true) # Hand cart + kitbashed yatai ramen stand at the market mouths _kit_prop_path("res://assets/props/town/cart.glb", c + Vector3(0, 0.02, 19.0), _rng.randf_range(-30, 30), 3.0) _hero("yatai", c + Vector3(2.0, 0.02, -19.0), _rng.randf_range(150.0, 210.0), Vector3(3.0, 2.8, 1.8), 1.4, "wood") # Vending pair at the north entry for v in 2: _box_static(c + Vector3(-1.5 + 3.0 * float(v), 1.0, -BLOCK * 0.5 + 3.0), Vector3(1.1, 2.0, 0.9), [Color(0.85, 0.3, 0.3), Color(0.3, 0.55, 0.85)][v], "%s_V%d" % [n, v], "metal") _spawn_points.append(c + Vector3(0, 1.5, 0)) ## Plaza: open ground; the exact centre cell hosts the giant screen tower. func _plaza_block(c: Vector3, bx: int, bz: int) -> void: var n := "Plaza_%d_%d" % [bx, bz] # Planter cover ring with real trees for i in 4: var a := TAU * float(i) / 4.0 + PI / 4.0 var p := c + Vector3(cos(a), 0, sin(a)) * (BLOCK * 0.32) _box_static(Vector3(p.x, 0.55, p.z), Vector3(4.0, 1.1, 4.0), Color(0.5, 0.48, 0.52), "%s_Pl%d" % [n, i], "brick") _kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 0.16, p.z), float(i) * 90.0, 4.5) # Kitbashed clock tower on the south-west plaza cell if bx == 4 and bz == 5: _hero("clock", c + Vector3(0, 0.1, 0), 0.0, Vector3(3.2, 10.5, 3.2), 5.25, "concrete") # Koban police box watching the central crossing if bx == 5 and bz == 5: _hero("koban", c + Vector3(-BLOCK * 0.5 + 4.0, 0.02, -BLOCK * 0.5 + 4.0), 225.0, Vector3(3.7, 3.4, 3.5), 1.7, "concrete") # The Crossing screen tower on the single centre-most plaza cell if bx == 5 and bz == 4: _box_static(c + Vector3(0, 9.0, 0), Vector3(6.0, 18.0, 6.0), METAL.darkened(0.2), n + "_ScreenTower", "metal") for face in 4: var a2 := TAU * float(face) / 4.0 var off := Vector3(cos(a2), 0, sin(a2)) * 3.15 _ad_quad(c + off + Vector3(0, 11.0, 0), Vector2(4.6, 6.0), rad_to_deg(a2) + 90.0, _board_tex()) _spawn_points.append(c + Vector3(BLOCK * 0.2, 1.5, -BLOCK * 0.2)) ## Construction site: crane (grapple mast + jib), container stacks, ramps ## and scaffold platforms — the vertical playground block. func _construction_block(c: Vector3, bx: int, bz: int) -> void: var n := "Site_%d_%d" % [bx, bz] # Unfinished frame: slab stack with open floors (wallrun edges) for lvl in 3: _box_static(c + Vector3(-8, float(lvl) * 6.0 + 5.7, -8), Vector3(24.0, 0.6, 24.0), Color(0.6, 0.58, 0.56), "%s_Slab%d" % [n, lvl], "concrete") for px in 2: for pz in 2: _box_static(c + Vector3(-8.0 + (-10.0 + 20.0 * float(px)), 9.0, -8.0 + (-10.0 + 20.0 * float(pz))), Vector3(1.0, 18.0, 1.0), Color(0.6, 0.58, 0.56), "%s_Col%d%d" % [n, px, pz], "concrete") # Access ramp to first slab _ramp_static(c + Vector3(6.0, 3.0, -8.0), Vector3(4.0, 0.4, 13.5), Vector3(0, 0, -26), METAL, n + "_Ramp", "metal") # Crane: mast + jib over the street (THE grapple anchor) _box_static(c + Vector3(14, 14.0, 14), Vector3(1.6, 28.0, 1.6), Color(0.9, 0.6, 0.15), n + "_CraneMast", "metal") _box_static(c + Vector3(4, 27.5, 14), Vector3(22.0, 1.2, 1.2), Color(0.9, 0.6, 0.15), n + "_CraneJib", "metal") _deco_box(c + Vector3(-6.0, 24.0, 14), Vector3(0.12, 6.5, 0.12), INK_COLOR) # Container stacks (jump steps + cover) var cc := [Color(0.75, 0.35, 0.25), Color(0.3, 0.55, 0.85), Color(0.35, 0.65, 0.4)] for i in 3: _box_static(c + Vector3(10, 1.3 + 2.6 * float(mini(i, 1)), -12.0 + float(i) * 3.2), Vector3(6.2, 2.6, 2.6), cc[i], "%s_Cont%d" % [n, i], "metal") # Cones + barriers scattered around the site entrance for i in 4: _kit_prop_path("res://assets/props/roads/construction-cone.glb", c + Vector3(-16.0 + float(i) * 4.0, 0.1, 18.0), 0.0, 8.0) for i in 2: _kit_prop_path("res://assets/props/roads/construction-barrier.glb", c + Vector3(-14.0 + float(i) * 12.0, 0.1, 14.0), float(i * 30), 8.0) _spawn_points.append(c + Vector3(14, 1.5, -14)) const INK_COLOR := Color(0.08, 0.07, 0.1) # ── Rail viaduct across the whole city ─────────────────────────────────────── func _build_rail_line() -> void: var z := (float(RAIL_ROW) - float(GRID - 1) * 0.5) * PITCH + PITCH * 0.5 # street south of RAIL_ROW var full := CITY_HALF * 2.0 + 12.0 _box_static(Vector3(0, RAIL_TOP - 0.5, z), Vector3(full, 1.0, 7.0), BRICK, "Rail_Deck", "brick") _box_static(Vector3(0, RAIL_TOP + 0.55, z - 3.2), Vector3(full, 1.1, 0.5), BRICK.darkened(0.1), "Rail_WallN", "brick") _box_static(Vector3(0, RAIL_TOP + 0.55, z + 3.2), Vector3(full, 1.1, 0.5), BRICK.darkened(0.1), "Rail_WallS", "brick") # Piers at every block line (streets cross beneath between them) var i := 0 var x := -CITY_HALF + PITCH * 0.5 while x <= CITY_HALF: _box_static(Vector3(x, (RAIL_TOP - 1.0) * 0.5, z), Vector3(2.2, RAIL_TOP - 1.0, 5.4), BRICK, "Rail_Pier%d" % i, "brick") # vending pair + posters under alternating arches if i % 2 == 0: _box_static(Vector3(x + 4.0, 1.0, z + 1.8), Vector3(1.1, 2.0, 0.9), Color(0.85, 0.3, 0.3), "Rail_V%da" % i, "metal") _box_static(Vector3(x + 5.6, 1.0, z + 1.8), Vector3(1.1, 2.0, 0.9), Color(0.3, 0.55, 0.85), "Rail_V%db" % i, "metal") _ad_quad(Vector3(x + 1.35, 2.0, z), Vector2(1.2, 1.8), 90.0, _poster_tex()) _ad_quad(Vector3(x - 1.35, 2.0, z), Vector2(1.2, 1.8), -90.0, _poster_tex()) i += 1 x += PITCH # Kitbashed station entrance beside the central avenue crossing, with a # platform strip (canopy + benches + signs) up on the deck above it _hero("station", Vector3(ROAD_W * 0.5 + 8.0, 0.02, z + 7.5), 90.0, Vector3(6.0, 3.6, 5.0), 1.8, "concrete") _hero("platform", Vector3(10.0, RAIL_TOP, z), 0.0, Vector3(14.0, 2.8, 2.4), 1.4, "metal") _hero("platform", Vector3(-24.0, RAIL_TOP, z), 0.0, Vector3(14.0, 2.8, 2.4), 1.4, "metal") # Pedestrian footbridge over the central avenue (deck + stair colliders # are walkable: a third crossing route between the viaduct and plaza) var fb_z := z + PITCH var fb := StaticBody3D.new() fb.name = "Footbridge" fb.set_meta("acoustic_material", "metal") add_child(fb) fb.global_position = Vector3(0, 0, fb_z) # span runs along x, across the avenue var fb_inst: Node3D = _kit_scene_at("res://assets/props/hero/footbridge.glb").instantiate() fb.add_child(fb_inst) LevelMaterials.apply_toon_recursive(fb_inst, 0.005) _fit_mesh_colliders(fb, fb_inst) # Slide ramps to the deck at the two main-avenue crossings for sx in [-PITCH * 0.5 - 4.0, PITCH * 0.5 + 4.0]: var ang := rad_to_deg(atan2(RAIL_TOP, 16.0)) _ramp_static(Vector3(sx, RAIL_TOP * 0.5, z + 3.5 + 8.0), Vector3(3.0, 0.4, sqrt(256.0 + RAIL_TOP * RAIL_TOP)), Vector3(-ang, 0, 0), PAVING.darkened(0.1), "Rail_Ramp_%d" % int(sx), "concrete") _spawn_points.append(Vector3(0, RAIL_TOP + 1.5, z)) # ── Streets: real road meshes with markings ────────────────────────────────── const ROAD_W := 12.0 # visual road width between sidewalk aprons ## Street lines run between block columns/rows at multiples of PITCH. func _street_lines() -> Array[float]: var lines: Array[float] = [] for i in GRID - 1: lines.append((float(i) - float(GRID) * 0.5 + 1.0) * PITCH) return lines func _road_piece(key: String, pos: Vector3, yaw_deg: float, sc: Vector3) -> void: var inst: Node3D = _kit_scene_at("res://assets/props/roads/" + key + ".glb").instantiate() inst.scale = sc add_child(inst) inst.global_position = pos inst.rotation_degrees.y = yaw_deg LevelMaterials.apply_toon_recursive(inst, 0.0) # The kit's pale concrete clips to white under the map lighting _tint_recursive(inst, Color(0.55, 0.55, 0.6)) var _path_cache: Dictionary = {} var _power_cable_index := 0 var _power_curve_index := 0 var _utility_terminal_index := 0 func _kit_scene_at(path: String) -> PackedScene: if not _path_cache.has(path): _path_cache[path] = load(path) return _path_cache[path] func _cable_segment(from: Vector3, to: Vector3, curve_id: int, segment_index: int, segment_count: int) -> void: var direction := to - from var cable := MeshInstance3D.new() cable.name = "PowerCable_%d" % _power_cable_index _power_cable_index += 1 var mesh := BoxMesh.new() mesh.size = Vector3(0.055, 0.055, direction.length() + 0.02) mesh.material = LevelMaterials.flat(INK_COLOR) cable.mesh = mesh add_child(cable) cable.look_at_from_position((from + to) * 0.5, to, Vector3.UP) cable.set_meta("cable_from", from) cable.set_meta("cable_to", to) cable.set_meta("curve_id", curve_id) cable.set_meta("segment_index", segment_index) cable.set_meta("segment_count", segment_count) func _utility_terminal(pos: Vector3, terminal_kind: String) -> void: var marker := Node3D.new() marker.name = "UtilityTerminal_%04d" % _utility_terminal_index _utility_terminal_index += 1 marker.position = pos marker.set_meta("utility_terminal", true) marker.set_meta("terminal_kind", terminal_kind) add_child(marker) func _cable_curve(from: Vector3, to: Vector3, sag: float, segments: int = 8) -> void: var curve_id := _power_curve_index _power_curve_index += 1 for segment in segments: var t0 := float(segment) / float(segments) var t1 := float(segment + 1) / float(segments) var p0 := from.lerp(to, t0) - Vector3.UP * sin(t0 * PI) * sag var p1 := from.lerp(to, t1) - Vector3.UP * sin(t1 * PI) * sag _cable_segment(p0, p1, curve_id, segment, segments) func _utility_pole(pos: Vector3, height: float, pole_id: String) -> void: var pole := _box_static( pos + Vector3(0, height * 0.5, 0), Vector3(0.24, height, 0.24), METAL.darkened(0.35), "UtilityPole_" + pole_id, "wood") for mesh in pole.find_children("*", "MeshInstance3D", false, false): mesh.material_override = LevelMaterials.flat(METAL.darkened(0.35)) _deco_box(pos + Vector3(0, height, 0), Vector3(1.35, 0.12, 0.16), METAL.darkened(0.2)) for lane in 3: var lane_x := -0.42 + float(lane) * 0.42 var terminal := pos + Vector3(lane_x, height + 0.12, 0) _deco_box(terminal, Vector3(0.10, 0.16, 0.10), Color(0.32, 0.28, 0.34)) _utility_terminal(terminal, "pole") # A transformer and grounded service cabinet make these read as utility # poles rather than unexplained posts holding decorative strings. _deco_box(pos + Vector3(0, height - 1.25, 0), Vector3(0.56, 0.82, 0.42), METAL.darkened(0.08)) _box_static(pos + Vector3(0.38, 0.55, 0), Vector3(0.62, 1.1, 0.48), METAL.darkened(0.16), "UtilityCabinet_" + pole_id, "metal") func _storefront_service_drop(pole_pos: Vector3, height: float, front_x: float, z: float, side: float, _service_id: String) -> void: # Terminate on a facade service mast one metre outside the decorative # sign/sill layer, then bracket that mast back to the wall. Driving the wire # directly into the facade let it pass through three protruding sign columns. var wall_x := front_x - side * 1.05 var service_height := 7.25 var pole_terminal := pole_pos + Vector3(side * 0.42, height + 0.12, 0) var wall_terminal := Vector3(wall_x, service_height, z) _utility_terminal(wall_terminal, "building_service") _cable_curve(pole_terminal, wall_terminal, 0.16, 5) # Junction box and conduit sit on the facade plane and carry the line down # to street level: a visually meaningful endpoint. _deco_box(wall_terminal, Vector3(0.16, 0.40, 0.46), METAL.darkened(0.12)) _deco_box(Vector3(wall_x, service_height * 0.5, z), Vector3(0.08, service_height, 0.10), METAL.darkened(0.25)) _deco_box(Vector3((wall_x + front_x) * 0.5, service_height, z), Vector3(absf(front_x - wall_x), 0.08, 0.10), METAL.darkened(0.2)) func _build_utility_network() -> void: var lines := _street_lines() var pole_zs: Array[float] = [] for row in GRID: pole_zs.append(_cell_center(0, row).z) for line_index in range(0, lines.size(), 2): var street_x: float = lines[line_index] if absf(street_x) < 1.0: continue var side := 1.0 if line_index % 4 == 0 else -1.0 # Stay between the road edge and the furthest storefront sign/canopy. # The previous 1.15 m offset grazed sign columns that project 0.8 m # from the facade; 0.55 m is the clear sidewalk utility corridor. var pole_x := street_x + side * (ROAD_W * 0.5 + 0.55) var height := 11.35 for row in GRID: var pole_pos := Vector3(pole_x, 0, pole_zs[row]) _utility_pole(pole_pos, height, "%d_%d" % [line_index, row]) if row < GRID - 1: var next_pos := Vector3(pole_x, 0, pole_zs[row + 1]) for lane in 3: var lane_x := -0.42 + float(lane) * 0.42 _cable_curve( pole_pos + Vector3(lane_x, height + 0.12, 0), next_pos + Vector3(lane_x, height + 0.12, 0), 0.58, 9) var adjacent_bx := line_index + 1 if side > 0.0 else line_index if adjacent_bx < 0 or adjacent_bx >= GRID: continue var block_type := LAYOUT[row][adjacent_bx] if block_type != "S" and block_type != "M": continue var front_x := street_x + side * (STREET * 0.5) _storefront_service_drop( pole_pos, height, front_x, pole_zs[row], side, "%d_%d" % [line_index, row]) func _build_streets() -> void: var lines := _street_lines() var y := 0.02 # Intersections with line markings for x in lines: for z in lines: _road_piece("road-crossroad-line", Vector3(x, y, z), 0.0, Vector3(ROAD_W, 2.0, ROAD_W)) # Straight segments (stretched single meshes) + edge stubs to the wall for x in lines: for i in lines.size() + 1: var z0 := -CITY_HALF if i == 0 else lines[i - 1] + ROAD_W * 0.5 var z1 := CITY_HALF if i == lines.size() else lines[i] - ROAD_W * 0.5 if z1 - z0 < 2.0: continue _road_piece("road-straight", Vector3(x, y, (z0 + z1) * 0.5), 0.0, Vector3(ROAD_W, 2.0, z1 - z0)) for z in lines: for i in lines.size() + 1: var x0 := -CITY_HALF if i == 0 else lines[i - 1] + ROAD_W * 0.5 var x1 := CITY_HALF if i == lines.size() else lines[i] - ROAD_W * 0.5 if x1 - x0 < 2.0: continue _road_piece("road-straight", Vector3((x0 + x1) * 0.5, y, z), 90.0, Vector3(ROAD_W, 2.0, x1 - x0)) _build_utility_network() # ── Main avenues: kit streetlights ─────────────────────────────────────────── func _build_avenues() -> void: var lines := _street_lines() var lamp_scale := 10.0 # Keep pole bases a full character radius behind the 12 m road mesh. The # old +0.4 m curb offset let the imported curved base lean into traffic. var sidewalk_pole_offset := ROAD_W * 0.5 + 1.45 for z in lines: for side: float in [-1.0, 1.0]: # Double-arm lights along the central N-S avenue at every block line var x := side * sidewalk_pole_offset var lamp_z := z + side * sidewalk_pole_offset _box_static(Vector3(x, 3.4, lamp_z), Vector3(0.26, 6.8, 0.26), METAL, "LampP_%d_%d" % [int(z), int(side)], "metal") _kit_prop_path("res://assets/props/roads/light-curved.glb", Vector3(x, 0, lamp_z), 90.0 if side > 0 else -90.0, lamp_scale) # Traffic signal on the opposite diagonal corner of each crossing _kit_prop_path("res://assets/props/roads/light-square-cross.glb", Vector3(x, 0, z - side * sidewalk_pole_offset), 90.0 if side > 0 else -90.0, lamp_scale * 0.8) # Bus stop shelters on alternating blocks along the avenue if int(roundf(z / PITCH)) % 2 == 0: _hero("busstop", Vector3(ROAD_W * 0.5 + 2.0, 0.02, z + 18.0), -90.0, Vector3(3.8, 2.6, 1.6), 1.3, "metal") _hero("busstop", Vector3(-ROAD_W * 0.5 - 2.0, 0.02, z - 18.0), 90.0, Vector3(3.8, 2.6, 1.6), 1.3, "metal") for x in lines: if absf(x) < 1.0: continue # Single lights down the central E-W avenue for side: float in [-1.0, 1.0]: var z2 := side * sidewalk_pole_offset _kit_prop_path("res://assets/props/roads/light-curved.glb", Vector3(x + side * sidewalk_pole_offset, 0, z2), 0.0 if side > 0 else 180.0, lamp_scale) # ── Parked cars (Kenney Car Kit) ───────────────────────────────────────────── const CAR_KEYS := ["sedan", "sedan", "taxi", "van", "suv", "hatchback-sports", "delivery", "sedan-sports", "police"] const CAR_SCALE := 1.8 ## A parked car: metal-tagged collider, kit body, wheels attached to the ## model's wheel sockets, slight per-car tint. func _parked_car(pos: Vector3, yaw_deg: float) -> void: var key: String = CAR_KEYS[_rng.randi() % CAR_KEYS.size()] var body := StaticBody3D.new() body.name = "Car_%d" % _rng.randi() body.set_meta("acoustic_material", "metal") add_child(body) body.global_position = pos body.rotation_degrees.y = yaw_deg + _rng.randf_range(-3.0, 3.0) var inst: Node3D = _kit_scene_at("res://assets/props/cars/" + key + ".glb").instantiate() inst.scale = Vector3.ONE * CAR_SCALE body.add_child(inst) var wheel_scene := _kit_scene_at("res://assets/props/cars/wheel-default.glb") for n in inst.find_children("wheel*", "Node3D", true, false): if n.get_child_count() == 0 and not (n is MeshInstance3D): var wheel: Node3D = wheel_scene.instantiate() n.add_child(wheel) LevelMaterials.apply_toon_recursive(inst, 0.0) # Kenney cars carry their paint/window/wheel palette as vertex colors. # Toon conversion now preserves it; keep the seeded tint subtle enough that # those authored regions remain legible. _tint_recursive(inst, Color(0.82, 0.82, 0.86) \ * Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.55, 1.0), 0.22)) # Imported origins vary by car. Seat every variant by its actual lowest # rendered point, then derive collision from the grounded visual. var bounds := _visual_bounds_in(inst, body) inst.position.y -= bounds.position.y _fit_mesh_colliders(body, inst) ## Seeded parking lanes hugging the curbs of the N-S streets. func _build_parked_cars() -> void: for x in _street_lines(): for row in GRID: _rng.seed = hash(Vector2i(int(x), row)) + 913 if _rng.randf() < 0.45: continue var z_base := (float(row) - float(GRID - 1) * 0.5) * PITCH var cars := 1 + _rng.randi() % 3 for i in cars: var side := 1.0 if _rng.randi() % 2 == 0 else -1.0 var cx := x + side * (ROAD_W * 0.5 - 1.8) var cz := z_base + _rng.randf_range(-18.0, 18.0) _parked_car(Vector3(cx, 0.02, cz), 0.0 if _rng.randi() % 2 == 0 else 180.0) func _kit_prop_path(path: String, pos: Vector3, yaw_deg: float, scale_f: float) -> void: var inst: Node3D = _kit_scene_at(path).instantiate() inst.scale = Vector3.ONE * scale_f add_child(inst) inst.global_position = pos inst.rotation_degrees.y = yaw_deg inst.set_meta("source_asset_path", path) # Nature-kit foliage uses vertex-colored materials that the toon swap # mangles (cyan trees) — keep original materials; the faceted low-poly # look already reads cel under this lighting. if not path.contains("/nature/"): LevelMaterials.apply_toon_recursive(inst, 0.0) # `pos.y` is always the intended support plane (ground, pedestal, or facade # attachment). Imported origins vary, so align the actual rendered bottom # to that plane for every kit prop. The old lantern/awning exceptions were # the source of many inexplicable floating pieces. var bounds := _visual_bounds_in(inst, self) inst.global_position.y += pos.y - bounds.position.y inst.set_meta("support_fitted", true) inst.set_meta("support_plane_y", pos.y)