feat: Akiba Crossing — parametric Akihabara rebuild of the urban map

Complete new approach after the hand-placed layout shipped misaligned:
every element now derives from a street grid and its building's footprint,
so alignment is structural, not luck.

District structure (modeled on Akihabara's Chuo-dori / Denki-gai / JR
viaduct):
- 16m avenue flanked by continuous mid-rise rows; buildings defined ONLY
  by z-interval + floor count (4m quantized floors), packed edge-to-edge
- _building() derives everything from the footprint: parapet lips on the
  roof rim, inset glass storefront, sidewalk canopy, stacked per-floor
  sign bands, a solid vertical corner sign column with glowing segments,
  evenly spaced roof AC units, rooftop billboard on 4+ floor buildings
- Brick rail viaduct crosses the whole block on piers with an under-track
  vending arcade; sidewalk-aligned slide ramps with handrails reach the
  deck on both flanks
- Denki-gai alley cut through both rows (wallrun walls + fire escape
  platforms climbing per floor); red SEGA-style landmark + blue
  electronics building anchor the north corners
- Matched streetlight pairs every 16m with arms over the road; zebra
  crossings at the alley mouth and south block; spawns on sidewalks/deck
  facing the map center

Movement: viaduct high lane + roof-hop onto the 2-floor row, canopy lane
above each sidewalk, alley wallruns + fire escapes, grapple anchors on
lamp arms / sign columns / viaduct edge / billboards, slide ramps.

Acoustics: facades concrete, storefronts glass, canopies wood, viaduct
brick, signs/AC/vending/lights/escapes metal.

Renamed in the level selector: "Akiba Crossing". Verified via aerial and
street captures (aligned rows, clean canyon, viaduct framing); movement
11/11, smoke 0 failures, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-18 14:34:31 -04:00
co-authored by Claude Fable 5
parent 233f88aee3
commit a7fa7f207c
2 changed files with 227 additions and 240 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[map] [map]
name="Neon Alley" name="Akiba Crossing"
scene_path="res://scenes/maps/neon_alley/neon_alley.tscn" scene_path="res://scenes/maps/neon_alley/neon_alley.tscn"
color1=Color(1.0, 0.3, 0.55, 1.0) color1=Color(1.0, 0.3, 0.55, 1.0)
color2=Color(0.2, 0.15, 0.45, 1.0) color2=Color(0.2, 0.15, 0.45, 1.0)
+226 -239
View File
@@ -1,55 +1,71 @@
extends TestLevelBuilder extends TestLevelBuilder
## Neon Alley — an urban-Japan block built for the movement kit. ## Akiba Crossing — an Akihabara-styled block, rebuilt parametrically.
## ##
## Layout (X east, -Z north): ## Real-district structure this mimics (Chuo-dori + Denki-gai + JR viaduct):
## - Main street runs N-S: the long sightline, crossed by a pedestrian ## - A wide N-S avenue flanked by continuous mid-rise retail rows
## bridge (underpass = cover, deck = high ground, ramps = slide entries) ## - Buildings quantized to 4m floors, packed edge-to-edge with designed
## - West: wooden machiya row houses — jumpable awnings, sloped slide ## alley cuts (the narrow perpendicular Denki-gai streets)
## roofs, warm acoustics (shots through them stay readable) ## - An elevated rail viaduct crossing the whole block on brick piers,
## - East: concrete towers with glass storefronts, a brick wallrun alley ## with an arcade of vending machines beneath the arches
## between them, and a metal fire-escape route to the roofs ## - Stacked floor-by-floor sign bands, vertical corner sign columns and
## - North: shrine plaza — torii gate crossbeam is a grapple anchor ## rooftop billboards — every sign is derived from its building's
## - South: market stalls + vending machines = scattered mid cover ## footprint, so nothing floats or clips
## - Power pylons with crossarms chain a grapple route down the street
## ##
## Every surface carries an acoustic material (wood/brick/concrete/metal/ ## Movement: viaduct deck = high lane (slide ramps at both avenue flanks),
## glass) so occlusion and reflections describe the map truthfully. ## canopy lane above each sidewalk, roof-hop route across quantized roofs,
## fire escapes in the alleys, wallrun alley walls, grapple anchors on
## streetlight arms / sign columns / viaduct edge / billboards.
##
## Acoustics: facades concrete, storefronts glass, canopies wood, viaduct
## brick, signs/AC/vending/lights/escapes metal.
# Palette — sunset-neon cel look. const FLOOR_H := 4.0 # one story
const ASPHALT := Color(0.24, 0.24, 0.31) const AVE_HALF := 8.0 # avenue half-width (x in [-8, 8])
const PAVING := Color(0.4, 0.38, 0.44) const WALK_W := 3.0 # sidewalk width
const CONCRETE := Color(0.52, 0.5, 0.55) const FRONT_E := AVE_HALF + WALK_W # east row front plane x = +11
const CONCRETE_DARK := Color(0.38, 0.37, 0.44) const FRONT_W := -AVE_HALF - WALK_W # west row front plane x = -11
const WOOD := Color(0.52, 0.34, 0.2) const ROW_DEPTH := 14.0 # building depth away from the avenue
const WOOD_DARK := Color(0.36, 0.24, 0.15) const EDGE_Z := 38.0 # block half-length
const VERMILION := Color(0.88, 0.26, 0.16) # torii / shrine red const VIADUCT_Z0 := -26.0 # viaduct strip (z)
const METAL := Color(0.42, 0.46, 0.52) const VIADUCT_Z1 := -18.0
const GLASS := Color(0.6, 0.78, 0.82) const VIADUCT_TOP := 8.0 # deck walking height (2 floors)
const ROOF_TILE := Color(0.25, 0.3, 0.42) const ALLEY_Z0 := 8.0 # Denki-gai alley cut (z)
const ALLEY_Z1 := 12.0
const NEON_COLORS: Array[Color] = [ # Palette — dusk cel. Facades muted so the signage carries the color.
Color(1.0, 0.25, 0.55), Color(0.2, 0.9, 0.95), Color(1.0, 0.75, 0.2), const ASPHALT := Color(0.3, 0.3, 0.37)
Color(0.55, 0.4, 1.0), Color(0.3, 1.0, 0.5), const PAVING := Color(0.46, 0.44, 0.5)
const STRIPE := Color(0.85, 0.85, 0.88)
const FACADES: Array[Color] = [
Color(0.74, 0.71, 0.66), # cream
Color(0.62, 0.63, 0.67), # cool gray
Color(0.68, 0.6, 0.5), # tan
Color(0.55, 0.6, 0.66), # blue-gray
Color(0.71, 0.66, 0.6), # warm gray
]
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 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 INK := Color(0.08, 0.07, 0.1)
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),
] ]
const SPAWNS: Array[Vector3] = [ var _spawn_points: Array[Vector3] = []
Vector3(0, 2, 30), Vector3(-20, 2, -26), Vector3(20, 2, -24),
Vector3(-20, 2, 24), Vector3(24, 2, 20), Vector3(0, 7, 1),
]
## Full cel treatment: every structure gets a FLAT toon color (no greybox ## Full cel treatment (flat toon color + prop-scale ink outlines).
## grid texture) plus an ink outline hull scaled to its size — the same
## inked-silhouette language as the characters. Ground-scale slabs skip the
## outline (a hull around an 80m floor just z-fights the sidewalks).
func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String = "", func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String = "",
acoustic: String = "") -> StaticBody3D: acoustic: String = "") -> StaticBody3D:
var body := super._box_static(pos, size, color, node_name, acoustic) var body := super._box_static(pos, size, color, node_name, acoustic)
for mi in body.find_children("*", "MeshInstance3D", false, false): for mi in body.find_children("*", "MeshInstance3D", false, false):
mi.mesh.surface_set_material(0, LevelMaterials.flat(color)) mi.mesh.surface_set_material(0, LevelMaterials.flat(color))
# Ink outlines on prop-scale pieces only: on building-scale boxes the
# inflated hull plates read as smears at close range.
if maxf(size.x, maxf(size.y, size.z)) < 10.0: if maxf(size.x, maxf(size.y, size.z)) < 10.0:
var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.008, 0.02, 0.05) var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.008, 0.02, 0.05)
mi.material_overlay = LevelMaterials.outline(w) mi.material_overlay = LevelMaterials.outline(w)
@@ -58,8 +74,6 @@ func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String =
func _build_environment() -> void: func _build_environment() -> void:
var env := LevelEnvironment.add_to(self, "sunset") var env := LevelEnvironment.add_to(self, "sunset")
# The sunset sky is too dark to light street canyons — use a fixed dusk
# lavender ambient instead so the map stays readable and neon still pops.
env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.environment.ambient_light_color = Color(0.58, 0.54, 0.66) env.environment.ambient_light_color = Color(0.58, 0.54, 0.66)
env.environment.ambient_light_energy = 3.0 env.environment.ambient_light_energy = 3.0
@@ -67,205 +81,207 @@ func _build_environment() -> void:
var sun := get_node_or_null("Sun") var sun := get_node_or_null("Sun")
if sun: if sun:
sun.light_energy = 3.0 sun.light_energy = 3.0
sun.rotation_degrees = Vector3(-38, 55, 0) # low warm sun down the street sun.rotation_degrees = Vector3(-38, 55, 0)
func _build_geometry() -> void: func _build_geometry() -> void:
_build_environment() _build_environment()
_build_lighting() _build_lighting()
_build_streets() _build_ground()
_build_perimeter() _build_row(1) # east row (front plane x=+11, faces -X)
_build_machiya_row() _build_row(-1) # west row
_build_east_block() _build_viaduct()
_build_bridge() _build_streetlights()
_build_shrine_plaza() _build_crosswalks()
_build_market() _spawn_points.append_array([
_build_pylons() Vector3(0, 1.5, 34), Vector3(-9.5, 1.5, 20), Vector3(9.5, 1.5, -6),
_build_neon_dressing() Vector3(0, VIADUCT_TOP + 1.5, -22), Vector3(-9.5, 1.5, -32), Vector3(9.5, 1.5, 30),
])
func _spawn_player(pid: int) -> CharacterBody3D: func _spawn_player(pid: int) -> CharacterBody3D:
var player := super._spawn_player(pid) var player := super._spawn_player(pid)
var s: Vector3 = SPAWNS[randi() % SPAWNS.size()] var s: Vector3 = _spawn_points[randi() % _spawn_points.size()] if not _spawn_points.is_empty() else Vector3(0, 2, 30)
player.position = s + Vector3(randf_range(-1.5, 1.5), 0, randf_range(-1.5, 1.5)) player.position = s + Vector3(randf_range(-1.0, 1.0), 0, randf_range(-1.0, 1.0))
# Face the map center so nobody spawns staring at the perimeter wall.
player.rotation.y = atan2(player.position.x, player.position.z) player.rotation.y = atan2(player.position.x, player.position.z)
return player return player
# ── Ground & perimeter ─────────────────────────────────────────────────────── # ── Ground ───────────────────────────────────────────────────────────────────
func _build_streets() -> void: func _build_ground() -> void:
# Asphalt base _box_static(Vector3(0, -0.5, 0), Vector3(2.0 * (absf(FRONT_E) + ROW_DEPTH) + 4.0, 1.0, 2.0 * EDGE_Z + 4.0),
_box_static(Vector3(0, -0.5, 0), Vector3(104, 1.0, 74), ASPHALT, "Street_Asphalt") ASPHALT, "Ground")
# Lighter paving strip down the main street (readability of the lane) # Raised sidewalks flanking the avenue, full length
_box_static(Vector3(0, 0.02, 0), Vector3(12, 0.04, 74), PAVING, "Street_Main") _box_static(Vector3(-AVE_HALF - WALK_W * 0.5, 0.08, 0), Vector3(WALK_W, 0.16, EDGE_Z * 2.0), PAVING, "Sidewalk_W")
# Sidewalks in front of both districts _box_static(Vector3(AVE_HALF + WALK_W * 0.5, 0.08, 0), Vector3(WALK_W, 0.16, EDGE_Z * 2.0), PAVING, "Sidewalk_E")
_box_static(Vector3(-8.5, 0.08, 0), Vector3(3, 0.16, 74), CONCRETE_DARK, "Sidewalk_W")
_box_static(Vector3(8.5, 0.08, 0), Vector3(3, 0.16, 74), CONCRETE_DARK, "Sidewalk_E")
func _build_perimeter() -> void: # ── Building rows (the aligned heart of the map) ─────────────────────────────
# Brick block walls boxing the arena — all wallrunnable.
_box_static(Vector3(0, 7, -37), Vector3(104, 14, 0.8), Color(0.42, 0.3, 0.28), "Perimeter_N", "brick") ## side: +1 = east row, -1 = west row. Buildings are defined ONLY by their
_box_static(Vector3(0, 7, 37), Vector3(104, 14, 0.8), Color(0.42, 0.3, 0.28), "Perimeter_S", "brick") ## z-interval and floor count; every facade element derives from those.
_box_static(Vector3(-52, 7, 0), Vector3(0.8, 14, 74), Color(0.42, 0.3, 0.28), "Perimeter_W", "brick") func _build_row(side: int) -> void:
_box_static(Vector3(52, 7, 0), Vector3(0.8, 14, 74), Color(0.42, 0.3, 0.28), "Perimeter_E", "brick") # z0, z1, floors, facade color index (-1 = landmark red / -2 = blue)
var plots: Array = [
[-EDGE_Z, VIADUCT_Z0, 5 if side == 1 else 4, -1 if side == 1 else 3], # north of viaduct
[VIADUCT_Z1, -6.0, 3 if side == 1 else 2, 0 if side == 1 else -2], # mid segment
[-6.0, ALLEY_Z0, 2 if side == 1 else 3, 1 if side == 1 else 4], # short building (roof-hop)
[ALLEY_Z1, 24.0, 4 if side == 1 else 5, 2 if side == 1 else 0], # south of alley
[24.0, EDGE_Z, 3 if side == 1 else 3, 4 if side == 1 else 2], # south corner
]
for i in plots.size():
var p: Array = plots[i]
var color: Color
match int(p[3]):
-1: color = SEGA_RED
-2: color = ELEC_BLUE
_: color = FACADES[int(p[3])]
_building(side, float(p[0]), float(p[1]), int(p[2]), color, "%s%d" % ["E" if side == 1 else "W", i])
# Fire escapes climb the alley faces (both rows, north side of the alley)
_fire_escape(side, ALLEY_Z0, int(plots[2][2]))
# ── West: machiya row (wood) ───────────────────────────────────────────────── ## One retail building: body, parapet, glass storefront, canopy, stacked
## floor sign bands, a vertical corner sign column, aligned roof AC units,
## and a rooftop billboard on 4+ floor buildings.
func _building(side: int, z0: float, z1: 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 front := float(side) * FRONT_E
var cx := front + float(side) * ROW_DEPTH * 0.5
var n := "Bldg_" + id
func _build_machiya_row() -> void: _box_static(Vector3(cx, h * 0.5, zc), Vector3(ROW_DEPTH, h, w), facade, n, "concrete")
var xs := [-16.0, -27.0, -38.0]
# Parapet lips on the roof rim (walkable cover on the roof lane)
var pt := 0.5
_box_static(Vector3(front + float(side) * 0.2, h + pt * 0.5, zc), Vector3(0.4, pt, w), facade.darkened(0.15), n + "_ParapetF")
_box_static(Vector3(front + float(side) * (ROW_DEPTH - 0.2), h + pt * 0.5, zc), Vector3(0.4, pt, w), facade.darkened(0.15), n + "_ParapetB")
for e in 2:
var ze := z0 + 0.2 if e == 0 else z1 - 0.2
_box_static(Vector3(cx, h + pt * 0.5, ze), Vector3(ROW_DEPTH, pt, 0.4), facade.darkened(0.15), n + "_ParapetS%d" % e)
# Ground floor: inset glass storefront + wooden canopy over the sidewalk
var face := front - float(side) * 0.12
_box_static(Vector3(face, 2.0, zc), Vector3(0.25, 3.0, w - 1.6), GLASS, n + "_Storefront", "glass")
_box_static(Vector3(front - float(side) * 1.1, 3.7, zc), Vector3(2.2, 0.2, w - 1.0), CANOPY, n + "_Canopy", "wood")
# Stacked sign bands at every floor line above the ground floor
for f in range(1, floors):
var y := float(f) * FLOOR_H + 0.9
_neon_box(Vector3(front - float(side) * 0.1, y, zc), Vector3(0.16, 1.3, w - 2.6),
SIGN_COLORS[(f + id.hash()) % SIGN_COLORS.size()])
# Vertical corner sign column (solid, grapple-able) with glowing segments
if floors >= 3:
var zs := z0 + 1.3
var col_h := h - FLOOR_H
_box_static(Vector3(front - float(side) * 0.45, FLOOR_H + col_h * 0.5 - 1.0, zs),
Vector3(0.7, col_h, 1.4), METAL, n + "_SignCol", "metal")
var segs := floors - 1
for s2 in segs:
var sy := FLOOR_H + 0.6 + (col_h - 1.2) * (float(s2) + 0.5) / float(segs) - 1.0
_neon_box(Vector3(front - float(side) * 0.85, sy, zs), Vector3(0.12, 2.2, 1.2),
SIGN_COLORS[(s2 + 1 + id.hash()) % SIGN_COLORS.size()])
# Roof AC units, evenly spaced along the roof midline
var count := int(w / 6.0)
for a in count:
var za := z0 + w * (float(a) + 0.5) / float(count)
_box_static(Vector3(cx, h + 0.8, za), Vector3(1.7, 1.6, 1.7), METAL, n + "_AC%d" % a, "metal")
# Rooftop billboard on tall buildings, facing the avenue
if floors >= 4:
var bb_h := 3.2
var bz := zc
_box_static(Vector3(front + float(side) * 2.0, h + bb_h * 0.5 + pt, bz),
Vector3(0.5, bb_h, minf(w - 4.0, 10.0)), METAL, n + "_BillboardFrame", "metal")
_neon_box(Vector3(front + float(side) * 1.7, h + bb_h * 0.5 + pt, bz),
Vector3(0.12, bb_h - 0.6, minf(w - 4.6, 9.4)),
SIGN_COLORS[id.hash() % SIGN_COLORS.size()])
## Metal fire-escape platforms climbing an alley-side face, one per floor.
func _fire_escape(side: int, alley_z: float, floors: int) -> void:
var front := float(side) * FRONT_E
for f in range(1, floors + 1):
var y := float(f) * FLOOR_H - 0.4
var xo := front + float(side) * (1.5 + float(f - 1) * 2.6)
_box_static(Vector3(xo, y, alley_z - 1.3), Vector3(2.4, 0.16, 2.4), METAL,
"Escape_%s_%d" % ["E" if side == 1 else "W", f], "metal")
# ── Rail viaduct (brick, crosses the whole block) ────────────────────────────
func _build_viaduct() -> void:
var depth := VIADUCT_Z1 - VIADUCT_Z0
var zc := (VIADUCT_Z0 + VIADUCT_Z1) * 0.5
var full_w := 2.0 * (absf(FRONT_E) + ROW_DEPTH)
# Deck slab
_box_static(Vector3(0, VIADUCT_TOP - 0.5, zc), Vector3(full_w, 1.0, depth), BRICK, "Viaduct_Deck", "brick")
# Low side walls along both deck edges (slide-under cover up top)
_box_static(Vector3(0, VIADUCT_TOP + 0.55, VIADUCT_Z0 + 0.3), Vector3(full_w, 1.1, 0.6), BRICK.darkened(0.1), "Viaduct_Wall_N", "brick")
_box_static(Vector3(0, VIADUCT_TOP + 0.55, VIADUCT_Z1 - 0.3), Vector3(full_w, 1.1, 0.6), BRICK.darkened(0.1), "Viaduct_Wall_S", "brick")
# Brick piers every 10m (the under-track arcade), avenue kept clear
var xs := [-20.0, -14.0, 14.0, 20.0]
for i in xs.size(): for i in xs.size():
var x: float = xs[i] _box_static(Vector3(xs[i], (VIADUCT_TOP - 1.0) * 0.5, zc), Vector3(2.0, VIADUCT_TOP - 1.0, depth - 2.0),
var z := -14.0 + float(i) * 3.0 BRICK, "Viaduct_Pier%d" % i, "brick")
_machiya(Vector3(x, 0, z), i) # Avenue-edge piers carry the deck across the road
_box_static(Vector3(-AVE_HALF - 1.0, (VIADUCT_TOP - 1.0) * 0.5, zc), Vector3(2.0, VIADUCT_TOP - 1.0, depth - 2.0), BRICK, "Viaduct_PierW", "brick")
_box_static(Vector3(AVE_HALF + 1.0, (VIADUCT_TOP - 1.0) * 0.5, zc), Vector3(2.0, VIADUCT_TOP - 1.0, depth - 2.0), BRICK, "Viaduct_PierE", "brick")
# Fourth house south side, rotated feel via different depth # Under-arch vending arcade (Akihabara's radio-parts stalls under the
_machiya(Vector3(-22.0, 0, 16.0), 3) # tracks): aligned vending machines between the piers
var vend_colors := [Color(0.85, 0.3, 0.3), Color(0.3, 0.55, 0.85), Color(0.92, 0.92, 0.95), Color(0.3, 0.75, 0.5)]
var vx := [-17.0, 17.0, -23.0, 23.0]
for i in vx.size():
_box_static(Vector3(vx[i], 1.0, VIADUCT_Z1 - 1.6), Vector3(1.1, 2.0, 0.9),
vend_colors[i % vend_colors.size()], "ArcadeVending_%d" % i, "metal")
# Connecting awning walkway between house 1 and 2 (jump route at h=2.6) # Slide ramps up to the deck along both sidewalks (aligned to the walk)
_box_static(Vector3(-21.5, 2.6, -12.5), Vector3(6, 0.25, 2.2), WOOD_DARK, "Awning_Link", "wood") var ramp_len := 18.0
var ramp_ang := rad_to_deg(atan2(VIADUCT_TOP, ramp_len))
for side: float in [-1.0, 1.0]:
var rx: float = side * (AVE_HALF + WALK_W * 0.5)
var rz := VIADUCT_Z1 + ramp_len * 0.5
var r := _ramp_static(Vector3(rx, VIADUCT_TOP * 0.5, rz),
Vector3(WALK_W, 0.4, sqrt(ramp_len * ramp_len + VIADUCT_TOP * VIADUCT_TOP)),
Vector3(-ramp_ang, 0, 0), PAVING.darkened(0.1),
"ViaductRamp_%s" % ("E" if side > 0 else "W"), "concrete")
# Handrail (thin metal ink line along the ramp edge)
var rail := _ramp_static(Vector3(rx + side * (WALK_W * 0.5 - 0.06), VIADUCT_TOP * 0.5 + 0.55, rz),
Vector3(0.12, 1.0, sqrt(ramp_len * ramp_len + VIADUCT_TOP * VIADUCT_TOP) - 0.8),
Vector3(-ramp_ang, 0, 0), METAL, "ViaductRail_%s" % ("E" if side > 0 else "W"), "metal")
## One two-story wooden row house: body, awning, balcony, flat roof section # ── Street furniture ─────────────────────────────────────────────────────────
## plus a sloped slide roof. Roofs are reachable (awning -> balcony -> roof).
func _machiya(base: Vector3, idx: int) -> void: func _build_streetlights() -> void:
var n := "Machiya%d" % idx # Matched pairs of avenue lights every 16m, arms reaching over the road.
# Body (two floors) var z := -10.0
_box_static(base + Vector3(0, 3.0, 0), Vector3(8, 6.0, 7), WOOD if idx % 2 == 0 else WOOD_DARK, n + "_Body", "wood") while z <= EDGE_Z - 4.0:
# Street awning (first jump step, h 2.5) for side: float in [-1.0, 1.0]:
_box_static(base + Vector3(4.6, 2.5, 0), Vector3(1.6, 0.22, 7), VERMILION.darkened(0.25), n + "_Awning", "wood") var x: float = side * (AVE_HALF + 0.6)
# Balcony on the second floor (second step, h 4.4) _box_static(Vector3(x, 3.25, z), Vector3(0.28, 6.5, 0.28), METAL, "Lamp_%d_%s" % [int(z), "E" if side > 0 else "W"], "metal")
_box_static(base + Vector3(4.4, 4.4, -1.5), Vector3(1.2, 0.22, 3.2), WOOD_DARK, n + "_Balcony", "wood") _box_static(Vector3(x - side * 1.5, 6.35, z), Vector3(3.0, 0.22, 0.22), METAL, "LampArm_%d_%s" % [int(z), "E" if side > 0 else "W"], "metal")
# Flat roof deck _neon_box(Vector3(x - side * 2.8, 6.2, z), Vector3(0.9, 0.18, 0.4), Color(1.0, 0.95, 0.8))
_box_static(base + Vector3(0, 6.15, 1.4), Vector3(8, 0.3, 4.2), ROOF_TILE, n + "_Roof", "wood") z += 16.0
# Sloped slide roof falling toward the street (25°) — slide entry from deck
_ramp_static(base + Vector3(2.6, 6.7, -2.2), Vector3(8.6, 0.25, 3.4),
Vector3(0, 0, -25), ROOF_TILE.lightened(0.12), n + "_SlideRoof", "wood")
# ── East: concrete towers, glass storefronts, wallrun alley ────────────────── func _build_crosswalks() -> void:
# Zebra stripes across the avenue at the alley mouth and the south block.
func _build_east_block() -> void: for cz in [10.0, 28.0]:
# Tower A (north-east) with billboard roof var stripes := 7
_box_static(Vector3(20, 5.5, -22), Vector3(16, 11, 12), CONCRETE, "TowerA", "concrete") for s in stripes:
_glass_front(Vector3(11.8, 1.6, -22), Vector3(0.25, 3.2, 9), "TowerA_Storefront") var x := -AVE_HALF + 1.0 + (2.0 * (AVE_HALF - 1.0)) * float(s) / float(stripes - 1)
# Billboard: metal frame + angled panel on the roof (grapple anchor + cover) _box_static(Vector3(x, 0.03, cz), Vector3(0.9, 0.06, 3.6), STRIPE, "Zebra_%d_%d" % [int(cz), s])
_box_static(Vector3(17, 12.2, -25), Vector3(0.5, 2.6, 7), METAL, "Billboard_Frame", "metal")
_ramp_static(Vector3(18.2, 12.6, -25), Vector3(0.4, 3.2, 7.4), Vector3(0, 0, -18),
Color(0.9, 0.9, 0.95), "Billboard_Panel", "metal")
# Tower B (south-east), shorter, fire-escape route up its face
_box_static(Vector3(20, 4.5, 8), Vector3(16, 9, 14), CONCRETE_DARK, "TowerB", "concrete")
_glass_front(Vector3(11.8, 1.6, 8), Vector3(0.25, 3.2, 11), "TowerB_Storefront")
var esc_y := [2.2, 4.4, 6.6, 8.8]
for i in esc_y.size():
_box_static(Vector3(12.6, esc_y[i], 13.5 - float(i) * 2.6),
Vector3(2.2, 0.18, 2.2), METAL, "FireEscape_%d" % i, "metal")
# Wallrun alley between the towers (brick faces, 4.5m apart)
_box_static(Vector3(20, 4, -13.5), Vector3(15, 8, 0.6), Color(0.45, 0.32, 0.3), "Alley_N", "brick")
_box_static(Vector3(20, 4, -9.0), Vector3(15, 8, 0.6), Color(0.45, 0.32, 0.3), "Alley_S", "brick")
# AC units in the alley to vault over / take cover behind
_box_static(Vector3(16, 0.8, -11.2), Vector3(1.8, 1.6, 1.6), METAL, "Alley_AC_1", "metal")
_box_static(Vector3(24, 0.8, -11.2), Vector3(1.8, 1.6, 1.6), METAL, "Alley_AC_2", "metal")
# Rooftop AC clusters (high-ground cover)
_box_static(Vector3(23, 11.75, -19), Vector3(2.4, 1.5, 2.4), METAL, "RoofAC_A1", "metal")
_box_static(Vector3(16, 9.75, 11), Vector3(2.2, 1.5, 2.2), METAL, "RoofAC_B1", "metal")
# Shipping container by the south-east corner (metal cover + jump step)
_box_static(Vector3(38, 1.3, 26), Vector3(6.2, 2.6, 2.6), Color(0.75, 0.35, 0.25), "Container", "metal")
func _glass_front(pos: Vector3, size: Vector3, node_name: String) -> void: # ── Neon (visual only) ───────────────────────────────────────────────────────
_box_static(pos, size, GLASS, node_name, "glass")
# ── Pedestrian bridge over the main street ───────────────────────────────────
func _build_bridge() -> void:
# Deck at h=5 spanning the street; underpass below is drive-through cover.
_box_static(Vector3(0, 5.0, 0), Vector3(21, 0.4, 4.5), CONCRETE, "Bridge_Deck", "concrete")
# Railings (thin metal — reflections ring off them)
_box_static(Vector3(0, 5.75, -2.1), Vector3(21, 1.1, 0.12), METAL, "Bridge_Rail_N", "metal")
_box_static(Vector3(0, 5.75, 2.1), Vector3(21, 1.1, 0.12), METAL, "Bridge_Rail_S", "metal")
# Slide ramps up both ends (20° — sprint in, slide out)
_ramp_static(Vector3(-13.2, 2.45, 0), Vector3(7.8, 0.35, 4.5), Vector3(0, 0, -20),
CONCRETE_DARK, "Bridge_Ramp_W", "concrete")
_ramp_static(Vector3(13.2, 2.45, 0), Vector3(7.8, 0.35, 4.5), Vector3(0, 0, 20),
CONCRETE_DARK, "Bridge_Ramp_E", "concrete")
# Support pillars (cover in the underpass)
_box_static(Vector3(-5, 2.4, 0), Vector3(0.8, 4.8, 0.8), CONCRETE_DARK, "Bridge_Pillar_W", "concrete")
_box_static(Vector3(5, 2.4, 0), Vector3(0.8, 4.8, 0.8), CONCRETE_DARK, "Bridge_Pillar_E", "concrete")
# ── North: shrine plaza ──────────────────────────────────────────────────────
func _build_shrine_plaza() -> void:
# Paved plaza
_box_static(Vector3(-6, 0.06, -28), Vector3(28, 0.12, 14), PAVING.lightened(0.08), "Plaza_Paving")
# Torii gate: two pillars + double crossbeam at h≈7 (the grapple anchor)
_box_static(Vector3(-12, 3.5, -22), Vector3(0.9, 7, 0.9), VERMILION, "Torii_Pillar_W", "wood")
_box_static(Vector3(0, 3.5, -22), Vector3(0.9, 7, 0.9), VERMILION, "Torii_Pillar_E", "wood")
_box_static(Vector3(-6, 7.0, -22), Vector3(15, 0.7, 1.2), VERMILION, "Torii_Beam_Top", "wood")
_box_static(Vector3(-6, 5.8, -22), Vector3(13, 0.5, 1.0), VERMILION.darkened(0.15), "Torii_Beam_Low", "wood")
# Shrine hall (wood, walk-around cover) with sloped roof to slide off
_box_static(Vector3(-6, 1.9, -31), Vector3(9, 3.8, 5), WOOD_DARK, "Shrine_Hall", "wood")
_ramp_static(Vector3(-6, 4.4, -29.4), Vector3(10, 0.3, 4.4), Vector3(-22, 0, 0),
VERMILION.darkened(0.35), "Shrine_Roof", "wood")
# Stone lanterns flanking the approach (small brick cover)
for i in 2:
var x := -14.0 + float(i) * 16.0
_box_static(Vector3(x, 1.0, -26), Vector3(1.1, 2.0, 1.1), Color(0.55, 0.55, 0.52), "Lantern_%d" % i, "brick")
# ── South: market street ─────────────────────────────────────────────────────
func _build_market() -> void:
# Wooden stalls with canopy tops (low cover; canopy is a jump step)
var stalls := [Vector3(-8, 0, 26), Vector3(-2, 0, 30), Vector3(6, 0, 25), Vector3(12, 0, 30)]
for i in stalls.size():
var p: Vector3 = stalls[i]
_box_static(p + Vector3(0, 1.0, 0), Vector3(3.4, 2.0, 2.0), WOOD, "Stall%d_Body" % i, "wood")
_box_static(p + Vector3(0, 2.35, 0), Vector3(4.2, 0.18, 2.8),
NEON_COLORS[i % NEON_COLORS.size()].darkened(0.45), "Stall%d_Canopy" % i, "wood")
# Vending machines (classic urban-japan metal cover)
var vends := [Vector3(-10.5, 0, 12), Vector3(10.4, 0, -4), Vector3(-10.5, 0, -18), Vector3(14, 0, 22)]
for i in vends.size():
_box_static(vends[i] + Vector3(0, 1.0, 0), Vector3(1.1, 2.0, 0.9),
[Color(0.85, 0.3, 0.3), Color(0.3, 0.55, 0.85), Color(0.9, 0.9, 0.9), Color(0.3, 0.75, 0.5)][i],
"Vending_%d" % i, "metal")
# ── Grapple pylons down the main street ──────────────────────────────────────
func _build_pylons() -> void:
# Power poles with crossarms over the street: chained grapple swings.
for i in 3:
var z := -18.0 + float(i) * 18.0
var side := 1.0 if i % 2 == 0 else -1.0
var x := 7.5 * side
_box_static(Vector3(x, 5.5, z), Vector3(0.5, 11, 0.5), WOOD_DARK, "Pylon_%d" % i, "wood")
# Crossarm reaching over the street — THE grapple target
_box_static(Vector3(x - 4.0 * side, 10.2, z), Vector3(8.5, 0.35, 0.35), WOOD_DARK, "Pylon_%d_Arm" % i, "wood")
# Hanging cable segment between arm tip and next pole (visual line)
var wire := MeshInstance3D.new()
var wm := BoxMesh.new()
wm.size = Vector3(0.06, 0.06, 16.0)
wm.material = LevelMaterials.flat(Color(0.08, 0.07, 0.1)) # ink line in the sky
wire.mesh = wm
wire.position = Vector3(x - 7.5 * side, 10.0, z + 9.0)
add_child(wire)
# ── Neon dressing (visual only, no collision) ────────────────────────────────
func _neon_mat(color: Color) -> StandardMaterial3D: func _neon_mat(color: Color) -> StandardMaterial3D:
var m := StandardMaterial3D.new() var m := StandardMaterial3D.new()
@@ -285,32 +301,3 @@ func _neon_box(pos: Vector3, size: Vector3, color: Color) -> void:
mi.mesh = bm mi.mesh = bm
mi.position = pos mi.position = pos
add_child(mi) add_child(mi)
func _build_neon_dressing() -> void:
# Vertical stacked sign columns on the tower faces (kanji-board vibe)
for i in 4:
var col: Color = NEON_COLORS[i % NEON_COLORS.size()]
var z := -27.0 + float(i) * 3.4
_neon_box(Vector3(11.6, 6.0 - float(i % 2), z), Vector3(0.15, 2.6, 1.1), col)
for i in 3:
var col: Color = NEON_COLORS[(i + 2) % NEON_COLORS.size()]
_neon_box(Vector3(11.6, 5.2 + float(i % 2) * 1.8, 4.0 + float(i) * 3.6), Vector3(0.15, 2.2, 1.0), col)
# Horizontal shop signs above the machiya awnings
for i in 3:
var x := -16.0 - float(i) * 11.0
_neon_box(Vector3(x, 3.3, -14.0 + float(i) * 3.0 + 3.6), Vector3(5.0, 0.7, 0.12),
NEON_COLORS[(i + 1) % NEON_COLORS.size()])
# Lantern string along the market street (small warm glows)
for i in 8:
_neon_box(Vector3(-10.0 + float(i) * 3.4, 3.4, 22.0 + sin(float(i)) * 1.2),
Vector3(0.35, 0.5, 0.35), Color(1.0, 0.62, 0.25))
# Billboard face glow
_neon_box(Vector3(18.55, 12.6, -25), Vector3(0.1, 2.8, 6.8), Color(0.95, 0.3, 0.5))
# Underpass strip lights (so the bridge shadow isn't a black hole)
_neon_box(Vector3(0, 4.7, -1.9), Vector3(20, 0.08, 0.08), Color(0.2, 0.9, 0.95))
_neon_box(Vector3(0, 4.7, 1.9), Vector3(20, 0.08, 0.08), Color(1.0, 0.25, 0.55))