Feat/visual audio overhaul #21

Merged
Dotts merged 31 commits from feat/visual-audio-overhaul into main 2026-07-21 14:09:18 -07:00
2 changed files with 227 additions and 240 deletions
Showing only changes of commit a7fa7f207c - Show all commits
+1 -1
View File
@@ -1,5 +1,5 @@
[map]
name="Neon Alley"
name="Akiba Crossing"
scene_path="res://scenes/maps/neon_alley/neon_alley.tscn"
color1=Color(1.0, 0.3, 0.55, 1.0)
color2=Color(0.2, 0.15, 0.45, 1.0)
+226 -239
View File
@@ -1,55 +1,71 @@
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):
## - Main street runs N-S: the long sightline, crossed by a pedestrian
## bridge (underpass = cover, deck = high ground, ramps = slide entries)
## - West: wooden machiya row houses — jumpable awnings, sloped slide
## roofs, warm acoustics (shots through them stay readable)
## - East: concrete towers with glass storefronts, a brick wallrun alley
## between them, and a metal fire-escape route to the roofs
## - North: shrine plaza — torii gate crossbeam is a grapple anchor
## - South: market stalls + vending machines = scattered mid cover
## - Power pylons with crossarms chain a grapple route down the street
## Real-district structure this mimics (Chuo-dori + Denki-gai + JR viaduct):
## - A wide N-S avenue flanked by continuous mid-rise retail rows
## - Buildings quantized to 4m floors, packed edge-to-edge with designed
## alley cuts (the narrow perpendicular Denki-gai streets)
## - An elevated rail viaduct crossing the whole block on brick piers,
## with an arcade of vending machines beneath the arches
## - Stacked floor-by-floor sign bands, vertical corner sign columns and
## rooftop billboards — every sign is derived from its building's
## footprint, so nothing floats or clips
##
## Every surface carries an acoustic material (wood/brick/concrete/metal/
## glass) so occlusion and reflections describe the map truthfully.
## Movement: viaduct deck = high lane (slide ramps at both avenue flanks),
## 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 ASPHALT := Color(0.24, 0.24, 0.31)
const PAVING := Color(0.4, 0.38, 0.44)
const CONCRETE := Color(0.52, 0.5, 0.55)
const CONCRETE_DARK := Color(0.38, 0.37, 0.44)
const WOOD := Color(0.52, 0.34, 0.2)
const WOOD_DARK := Color(0.36, 0.24, 0.15)
const VERMILION := Color(0.88, 0.26, 0.16) # torii / shrine red
const METAL := Color(0.42, 0.46, 0.52)
const GLASS := Color(0.6, 0.78, 0.82)
const ROOF_TILE := Color(0.25, 0.3, 0.42)
const FLOOR_H := 4.0 # one story
const AVE_HALF := 8.0 # avenue half-width (x in [-8, 8])
const WALK_W := 3.0 # sidewalk width
const FRONT_E := AVE_HALF + WALK_W # east row front plane x = +11
const FRONT_W := -AVE_HALF - WALK_W # west row front plane x = -11
const ROW_DEPTH := 14.0 # building depth away from the avenue
const EDGE_Z := 38.0 # block half-length
const VIADUCT_Z0 := -26.0 # viaduct strip (z)
const VIADUCT_Z1 := -18.0
const VIADUCT_TOP := 8.0 # deck walking height (2 floors)
const ALLEY_Z0 := 8.0 # Denki-gai alley cut (z)
const ALLEY_Z1 := 12.0
const NEON_COLORS: Array[Color] = [
Color(1.0, 0.25, 0.55), Color(0.2, 0.9, 0.95), Color(1.0, 0.75, 0.2),
Color(0.55, 0.4, 1.0), Color(0.3, 1.0, 0.5),
# Palette — dusk cel. Facades muted so the signage carries the color.
const ASPHALT := Color(0.3, 0.3, 0.37)
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] = [
Vector3(0, 2, 30), Vector3(-20, 2, -26), Vector3(20, 2, -24),
Vector3(-20, 2, 24), Vector3(24, 2, 20), Vector3(0, 7, 1),
]
var _spawn_points: Array[Vector3] = []
## Full cel treatment: every structure gets a FLAT toon color (no greybox
## 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).
## Full cel treatment (flat toon color + prop-scale ink outlines).
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)
for mi in body.find_children("*", "MeshInstance3D", false, false):
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:
var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.008, 0.02, 0.05)
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:
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_color = Color(0.58, 0.54, 0.66)
env.environment.ambient_light_energy = 3.0
@@ -67,205 +81,207 @@ func _build_environment() -> void:
var sun := get_node_or_null("Sun")
if sun:
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:
_build_environment()
_build_lighting()
_build_streets()
_build_perimeter()
_build_machiya_row()
_build_east_block()
_build_bridge()
_build_shrine_plaza()
_build_market()
_build_pylons()
_build_neon_dressing()
_build_ground()
_build_row(1) # east row (front plane x=+11, faces -X)
_build_row(-1) # west row
_build_viaduct()
_build_streetlights()
_build_crosswalks()
_spawn_points.append_array([
Vector3(0, 1.5, 34), Vector3(-9.5, 1.5, 20), Vector3(9.5, 1.5, -6),
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:
var player := super._spawn_player(pid)
var s: Vector3 = SPAWNS[randi() % SPAWNS.size()]
player.position = s + Vector3(randf_range(-1.5, 1.5), 0, randf_range(-1.5, 1.5))
# Face the map center so nobody spawns staring at the perimeter wall.
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.0, 1.0), 0, randf_range(-1.0, 1.0))
player.rotation.y = atan2(player.position.x, player.position.z)
return player
# ── Ground & perimeter ───────────────────────────────────────────────────────
# ── Ground ───────────────────────────────────────────────────────────────────
func _build_streets() -> void:
# Asphalt base
_box_static(Vector3(0, -0.5, 0), Vector3(104, 1.0, 74), ASPHALT, "Street_Asphalt")
# Lighter paving strip down the main street (readability of the lane)
_box_static(Vector3(0, 0.02, 0), Vector3(12, 0.04, 74), PAVING, "Street_Main")
# Sidewalks in front of both districts
_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_ground() -> void:
_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),
ASPHALT, "Ground")
# Raised sidewalks flanking the avenue, full length
_box_static(Vector3(-AVE_HALF - WALK_W * 0.5, 0.08, 0), Vector3(WALK_W, 0.16, EDGE_Z * 2.0), PAVING, "Sidewalk_W")
_box_static(Vector3(AVE_HALF + WALK_W * 0.5, 0.08, 0), Vector3(WALK_W, 0.16, EDGE_Z * 2.0), PAVING, "Sidewalk_E")
func _build_perimeter() -> void:
# 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")
_box_static(Vector3(0, 7, 37), Vector3(104, 14, 0.8), Color(0.42, 0.3, 0.28), "Perimeter_S", "brick")
_box_static(Vector3(-52, 7, 0), Vector3(0.8, 14, 74), Color(0.42, 0.3, 0.28), "Perimeter_W", "brick")
_box_static(Vector3(52, 7, 0), Vector3(0.8, 14, 74), Color(0.42, 0.3, 0.28), "Perimeter_E", "brick")
# ── Building rows (the aligned heart of the map) ─────────────────────────────
## side: +1 = east row, -1 = west row. Buildings are defined ONLY by their
## z-interval and floor count; every facade element derives from those.
func _build_row(side: int) -> void:
# 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:
var xs := [-16.0, -27.0, -38.0]
_box_static(Vector3(cx, h * 0.5, zc), Vector3(ROW_DEPTH, h, w), facade, n, "concrete")
# 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():
var x: float = xs[i]
var z := -14.0 + float(i) * 3.0
_machiya(Vector3(x, 0, z), i)
_box_static(Vector3(xs[i], (VIADUCT_TOP - 1.0) * 0.5, zc), Vector3(2.0, VIADUCT_TOP - 1.0, depth - 2.0),
BRICK, "Viaduct_Pier%d" % i, "brick")
# 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
_machiya(Vector3(-22.0, 0, 16.0), 3)
# Under-arch vending arcade (Akihabara's radio-parts stalls under the
# 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)
_box_static(Vector3(-21.5, 2.6, -12.5), Vector3(6, 0.25, 2.2), WOOD_DARK, "Awning_Link", "wood")
# Slide ramps up to the deck along both sidewalks (aligned to the walk)
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
## plus a sloped slide roof. Roofs are reachable (awning -> balcony -> roof).
func _machiya(base: Vector3, idx: int) -> void:
var n := "Machiya%d" % idx
# Body (two floors)
_box_static(base + Vector3(0, 3.0, 0), Vector3(8, 6.0, 7), WOOD if idx % 2 == 0 else WOOD_DARK, n + "_Body", "wood")
# Street awning (first jump step, h 2.5)
_box_static(base + Vector3(4.6, 2.5, 0), Vector3(1.6, 0.22, 7), VERMILION.darkened(0.25), n + "_Awning", "wood")
# Balcony on the second floor (second step, h 4.4)
_box_static(base + Vector3(4.4, 4.4, -1.5), Vector3(1.2, 0.22, 3.2), WOOD_DARK, n + "_Balcony", "wood")
# Flat roof deck
_box_static(base + Vector3(0, 6.15, 1.4), Vector3(8, 0.3, 4.2), ROOF_TILE, n + "_Roof", "wood")
# 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")
# ── Street furniture ─────────────────────────────────────────────────────────
func _build_streetlights() -> void:
# Matched pairs of avenue lights every 16m, arms reaching over the road.
var z := -10.0
while z <= EDGE_Z - 4.0:
for side: float in [-1.0, 1.0]:
var x: float = side * (AVE_HALF + 0.6)
_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(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")
_neon_box(Vector3(x - side * 2.8, 6.2, z), Vector3(0.9, 0.18, 0.4), Color(1.0, 0.95, 0.8))
z += 16.0
# ── East: concrete towers, glass storefronts, wallrun alley ──────────────────
func _build_east_block() -> void:
# Tower A (north-east) with billboard roof
_box_static(Vector3(20, 5.5, -22), Vector3(16, 11, 12), CONCRETE, "TowerA", "concrete")
_glass_front(Vector3(11.8, 1.6, -22), Vector3(0.25, 3.2, 9), "TowerA_Storefront")
# Billboard: metal frame + angled panel on the roof (grapple anchor + cover)
_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 _build_crosswalks() -> void:
# Zebra stripes across the avenue at the alley mouth and the south block.
for cz in [10.0, 28.0]:
var stripes := 7
for s in stripes:
var x := -AVE_HALF + 1.0 + (2.0 * (AVE_HALF - 1.0)) * float(s) / float(stripes - 1)
_box_static(Vector3(x, 0.03, cz), Vector3(0.9, 0.06, 3.6), STRIPE, "Zebra_%d_%d" % [int(cz), s])
func _glass_front(pos: Vector3, size: Vector3, node_name: String) -> void:
_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) ────────────────────────────────
# ── Neon (visual only) ───────────────────────────────────────────────────────
func _neon_mat(color: Color) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
@@ -285,32 +301,3 @@ func _neon_box(pos: Vector3, size: Vector3, color: Color) -> void:
mi.mesh = bm
mi.position = pos
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))