feat: Neon Alley — urban-Japan movement map built on the acoustic materials

New map (scenes/maps/neon_alley/, auto-appears in the level selectors):

Layout for movement flow:
- Main N-S street = the long sightline, crossed by a pedestrian bridge
  (slide ramps up both ends, underpass cover below, railed high ground)
- West machiya row: wooden two-story houses with a jump route
  (awning -> balcony -> roof deck) and 25-degree slide roofs facing the street
- East concrete towers with glass storefronts; brick wallrun alley between
  them (AC units to vault); metal fire-escape platform route to the roofs;
  angled rooftop billboard as cover + grapple anchor
- Three power pylons with crossarms over the street chain a grapple route;
  the torii gate crossbeam at the north shrine plaza is a fourth anchor
- South market: wooden stalls with neon canopies + metal vending machines
  = scattered mid cover; shipping container corner piece
- Spawns ring the block and face the map center

Acoustics as level design: every surface is tagged (machiya/stalls/torii
wood, towers concrete, storefronts glass, alley+perimeter brick, escapes/
AC/rails/container metal) so occlusion tells you which structure a shot
came through and reflections differ per street.

Style: sunset sky + fixed dusk-lavender ambient (calibrated for the toon
shader's 1/PI light response), warm low sun, neon sign columns, shop signs,
lantern strings, billboard glow, underpass strip lights.

Verified: aerial + street-level + third-person captures; 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:18:06 -04:00
co-authored by Claude Fable 5
parent a51595e3b9
commit 0813d0b40d
6 changed files with 331 additions and 5 deletions
+3 -2
View File
@@ -90,8 +90,9 @@ func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String =
return body
func _ramp_static(pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color, node_name: String = "") -> StaticBody3D:
var body := _box_static(pos, size, color, node_name)
func _ramp_static(pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color, node_name: String = "",
acoustic: String = "") -> StaticBody3D:
var body := _box_static(pos, size, color, node_name, acoustic)
body.rotation_degrees = rot_deg
return body
+17 -3
View File
@@ -2,17 +2,21 @@ extends SceneTree
## Dev tool: launch with rendering, screenshot the main menu and a level,
## then quit. Run:
## godot --path . -s res://debug/visual_capture.gd -- <output_dir>
## Screenshots land in <output_dir>/shot_menu.png and shot_level.png.
## godot --path . -s res://debug/visual_capture.gd -- <output_dir> [scene_path]
## Screenshots land in <output_dir>/shot_menu.png, shot_level.png, shot_tp_run.png
## and shot_aerial.png. scene_path defaults to the test level.
var _frames := 0
var _out_dir := "."
var _scene := "res://scenes/maps/test_level/test_level.tscn"
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
if args.size() > 0:
_out_dir = args[0]
if args.size() > 1:
_scene = args[1]
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
@@ -23,7 +27,7 @@ func _process(_delta: float) -> bool:
var nm = root.get_node_or_null("NetworkManager")
if nm and nm.has_method("start_singleplayer_match"):
nm.start_singleplayer_match("Deathmatch")
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
change_scene_to_file(_scene)
elif _frames == 240:
_shot("level")
# Third person + run forward, to eyeball model grounding and animation
@@ -34,6 +38,16 @@ func _process(_delta: float) -> bool:
elif _frames == 320:
_shot("tp_run")
Input.action_release("move_forward")
# Aerial overview of the whole map
var cam := root.get_camera_3d()
if cam:
var aerial := Camera3D.new()
current_scene.add_child(aerial)
aerial.global_position = Vector3(0, 55, 42)
aerial.look_at(Vector3(0, 0, -5), Vector3.UP)
aerial.current = true
elif _frames == 340:
_shot("aerial")
return true
return false
+5
View File
@@ -0,0 +1,5 @@
[map]
name="Neon Alley"
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)
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://bq3nalley7neon"]
[ext_resource type="Script" path="res://scenes/maps/neon_alley/neon_alley_builder.gd" id="1_na"]
[node name="NeonAlley" type="Node3D"]
script = ExtResource("1_na")
@@ -0,0 +1,299 @@
extends TestLevelBuilder
## Neon Alley — an urban-Japan block built for the movement kit.
##
## 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
##
## Every surface carries an acoustic material (wood/brick/concrete/metal/
## glass) so occlusion and reflections describe the map truthfully.
# 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.45, 0.3, 0.18)
const WOOD_DARK := Color(0.32, 0.21, 0.13)
const VERMILION := Color(0.82, 0.25, 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 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),
]
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),
]
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
env.environment.fog_density = 0.0006
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
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()
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.
player.rotation.y = atan2(player.position.x, player.position.z)
return player
# ── Ground & perimeter ───────────────────────────────────────────────────────
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_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")
# ── West: machiya row (wood) ─────────────────────────────────────────────────
func _build_machiya_row() -> void:
var xs := [-16.0, -27.0, -38.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)
# Fourth house south side, rotated feel via different depth
_machiya(Vector3(-22.0, 0, 16.0), 3)
# 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")
## 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")
# ── 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 _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.tinted(Color(0.1, 0.1, 0.12))
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:
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
return m
func _neon_box(pos: Vector3, size: Vector3, color: Color) -> void:
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = size
bm.material = _neon_mat(color)
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))
@@ -0,0 +1 @@
uid://bg7464r8f2qxd