Extract per-venue geometry specs into WorldBuilder

Add the Hollywood, Joke Shop and TV Studio tables for facade, window,
stair, street, nav, guard-spawn and feature data. Unknown ids fall back to
the boulevard. Use the specs for the core dimensions and gates, with a headless
test for the distinct venue data. Headless 230/230.
This commit is contained in:
zwitschi committed 2026-11-01 19:03:20 +01:00
1 parent 99f390078d
commit 4cca2af2dd
4 files changed
+198 -62

No files matched your search

+1
View File
@@ -246,6 +246,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
helmet is lifted 2 cm above the brow. `test_npc_character` guards head/back gear in Godot. helmet is lifted 2 cm above the brow. `test_npc_character` guards head/back gear in Godot.
Blender's GLB export is byte-nondeterministic for unchanged assets (padding/index order), so do Blender's GLB export is byte-nondeterministic for unchanged assets (padding/index order), so do
not commit a full no-op rebuild; rebuild only the assets whose geometry changed. not commit a full no-op rebuild; rebuild only the assets whose geometry changed.
- **Venue geometry (v0.6.1):** `WorldBuilder.VENUE_SPECS` is the per-venue table; `WorldBuilder.new(root, id)` / `set_venue()` select it, unknown ids fall back to `hollywood_building`. Dimensions, window/pilaster rows, stairs, street, nav bounds, guard spawn and feature flags are read from the spec. Joke Shop and TV Studio currently use empty `prop_placements`; bespoke alley/studio dressing and the `GameManager` rebuild-on-venue-switch are follow-ups.
- **Portrait capture:** `capture_realism.gd` faces the **Rig** toward the camera (using the - **Portrait capture:** `capture_realism.gd` faces the **Rig** toward the camera (using the
inverse look direction) and hides the blue `FocusCircle`; the old code used the wrong yaw and inverse look direction) and hides the blue `FocusCircle`; the old code used the wrong yaw and
left the focus disc over the face. An explicit `--portrait-type=N` now overrides the hooded left the focus disc over the face. An explicit `--portrait-type=N` now overrides the hooded
+5 -4
View File
@@ -9,10 +9,11 @@ Today all three venues share the Hollywood `WorldBuilder` geometry, so the Joke
concepts are only compared on lighting (noted in `tools/scene_concepts.py` `SHOTS`). They also concepts are only compared on lighting (noted in `tools/scene_concepts.py` `SHOTS`). They also
have no stage in the committed `docs/screenshots/` gallery, only in `capture_realism.gd`. have no stage in the committed `docs/screenshots/` gallery, only in `capture_realism.gd`.
- [ ] Extract a geometry spec per venue instead of branching inline: a `scenario.` (or - [x] Extract a geometry spec per venue instead of branching inline: `WorldBuilder.VENUE_SPECS`
`WorldBuilder`) table of facade width/height, door style, window rows, awning/marquee now holds facade/stair/street/nav dimensions, window/pilaster rows, guard spawn, feature
flags, street furniture set, palette and props, defaulting to the Hollywood values so flags and prop placements for Hollywood, Joke Shop and TV Studio. Unknown ids fall back to
nothing regresses. Hollywood; `test_venue_geometry_specs_are_distinct_and_defaulted` checks the narrow Joke Shop
facade, disabled skyline/marquee, studio bulb-canopy flag and fallback.
- [ ] Joke Shop: narrow brick alley (10–12 m wide), one warm wall lantern as the key light, - [ ] Joke Shop: narrow brick alley (10–12 m wide), one warm wall lantern as the key light,
recessed door with a step, shuttered window, dumpster/AC clutter, no skyline ring — the recessed door with a step, shuttered window, dumpster/AC clutter, no skyline ring — the
alley concept has depth, not a horizon. alley concept has depth, not a horizon.
+159 -58
View File
@@ -59,18 +59,99 @@ var library_nodes: Array[Node3D] = []
## Total triangle budget for the Blender library pieces placed in the world. ## Total triangle budget for the Blender library pieces placed in the world.
const LIBRARY_TRIANGLE_BUDGET := 60000 const LIBRARY_TRIANGLE_BUDGET := 60000
func _init(root_node: Node3D) -> void: ## Per-venue geometry data. The Hollywood values are the fallback: every field
## here defaults to the long-standing boulevard layout, so adding a venue cannot
## silently regress the original scene. Joke Shop and TV Studio override the
## facade/street measurements and feature flags; their bespoke dressing lands in
## the following tasks.
const VENUE_SPECS := {
"hollywood_building": {
"display_name": "Hollywood Boulevard Building",
"facade_width": 40.0, "facade_height": 18.0, "facade_depth": 4.0,
"facade_center_z": -6.0,
"window_columns": [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0],
"pilaster_columns": [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3],
"window_rows": [9.2, 12.4],
"stair_width": 8.0, "stair_count": 4, "stair_tread": 0.6, "stair_height": 0.16,
"sidewalk_width": 30.0, "sidewalk_depth": 8.0,
"street_width": 120.0, "street_depth": 26.0,
"nav_min": Vector2(-18.0, -0.6), "nav_max": Vector2(18.0, 20.0),
"guard_spawn": Vector3(0.0, 0.1, -2.0),
"features": {
"marquee": true, "skyline": true, "handrail": true, "sedan": true,
"wall_lanterns": true, "palms": true, "street_trees": true,
"crosswalk": true, "bulb_canopy": false,
},
},
"joke_shop": {
"display_name": "The Joke Shop",
"facade_width": 12.0, "facade_height": 12.0, "facade_depth": 5.0,
"facade_center_z": -6.5,
"window_columns": [-3.8, 3.8],
"pilaster_columns": [-5.2, 5.2],
"window_rows": [8.2],
"stair_width": 5.0, "stair_count": 2, "stair_tread": 0.65, "stair_height": 0.16,
"sidewalk_width": 22.0, "sidewalk_depth": 6.0,
"street_width": 75.0, "street_depth": 18.0,
"nav_min": Vector2(-9.0, -0.6), "nav_max": Vector2(9.0, 14.0),
"guard_spawn": Vector3(0.0, 0.1, -1.6),
"features": {
"marquee": false, "skyline": false, "handrail": false, "sedan": false,
"wall_lanterns": true, "palms": false, "street_trees": false,
"crosswalk": false, "bulb_canopy": false,
},
"prop_placements": [],
},
"tv_studio": {
"display_name": "Late Night TV Studio",
"facade_width": 30.0, "facade_height": 15.0, "facade_depth": 4.0,
"facade_center_z": -6.0,
"window_columns": [-11.0, -7.2, 7.2, 11.0],
"pilaster_columns": [-13.2, -9.0, -5.0, 5.0, 9.0, 13.2],
"window_rows": [10.5],
"stair_width": 7.0, "stair_count": 3, "stair_tread": 0.6, "stair_height": 0.16,
"sidewalk_width": 28.0, "sidewalk_depth": 7.0,
"street_width": 100.0, "street_depth": 24.0,
"nav_min": Vector2(-14.0, -0.6), "nav_max": Vector2(14.0, 18.0),
"guard_spawn": Vector3(0.0, 0.1, -1.8),
"features": {
"marquee": false, "skyline": true, "handrail": true, "sedan": true,
"wall_lanterns": false, "palms": false, "street_trees": true,
"crosswalk": true, "bulb_canopy": true,
},
"prop_placements": [],
},
}
const DEFAULT_VENUE := "hollywood_building"
var venue_id := DEFAULT_VENUE
var spec: Dictionary = VENUE_SPECS[DEFAULT_VENUE]
func _init(root_node: Node3D, id: String = DEFAULT_VENUE) -> void:
root = root_node root = root_node
set_venue(id)
## Select the geometry spec before `build()`. Unknown ids fall back to the
## boulevard so old callers and tests keep working.
func set_venue(id: String) -> void:
venue_id = id if VENUE_SPECS.has(id) else DEFAULT_VENUE
spec = VENUE_SPECS[venue_id]
func _feature(name: String) -> bool:
return bool((spec.get("features", {}) as Dictionary).get(name, false))
func build() -> void: func build() -> void:
_build_ground() _build_ground()
_build_building() _build_building()
_build_skyline_backdrop() if _feature("skyline"):
_build_marquee() _build_skyline_backdrop()
if _feature("marquee"):
_build_marquee()
_build_steps() _build_steps()
_build_sidewalk() _build_sidewalk()
_build_street() _build_street()
_build_crosswalk() if _feature("crosswalk"):
_build_crosswalk()
_build_street_furniture() _build_street_furniture()
_build_street_detail() _build_street_detail()
_build_library_dressing() _build_library_dressing()
@@ -111,41 +192,46 @@ func _build_ground() -> void:
func _build_building() -> void: func _build_building() -> void:
var building := StaticBody3D.new() var building := StaticBody3D.new()
building.name = "Building" building.name = "Building"
var w := float(spec["facade_width"])
var h := float(spec["facade_height"])
var d := float(spec["facade_depth"])
var cz := float(spec["facade_center_z"])
var front := cz + d * 0.5
# Main facade (textured brick) # Main facade (textured brick)
_add_box(building, Vector3(40, 18, 4), Vector3(0, 9, -6), Color(0.86, 0.85, 0.83), false, T_BRICK, Vector2(22, 10), "brick") _add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), Color(0.86, 0.85, 0.83), false, T_BRICK, Vector2(w * 0.55, h * 0.55), "brick")
# Stone base course so the facade has a footing instead of meeting the steps dead-on. # Stone base course so the facade has a footing instead of meeting the steps dead-on.
_add_box(building, Vector3(40, 0.8, 0.5), Vector3(0, 0.4, -3.78), Color(0.52, 0.5, 0.47), false, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.8, 0.5), Vector3(0, 0.4, front + 0.22), Color(0.52, 0.5, 0.47), false, null, Vector2.ONE, "stone")
# Facade depth, not a flat wall: a projecting plinth, a string course between # Facade depth, not a flat wall: a projecting plinth, a string course between
# the window bands, a cornice under the roofline and pilasters at the bays. # the window bands, a cornice under the roofline and pilasters at the bays.
_add_box(building, Vector3(40, 0.34, 0.22), Vector3(0, 0.93, -3.9), Color(0.6, 0.58, 0.55), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.34, 0.22), Vector3(0, 0.93, front + 0.1), Color(0.6, 0.58, 0.55), true, null, Vector2.ONE, "stone")
_add_box(building, Vector3(40, 0.28, 0.28), Vector3(0, 7.6, -3.86), Color(0.6, 0.58, 0.55), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.28, 0.28), Vector3(0, 7.6, front + 0.14), Color(0.6, 0.58, 0.55), true, null, Vector2.ONE, "stone")
_add_box(building, Vector3(40, 0.5, 0.5), Vector3(0, 17.7, -3.75), Color(0.56, 0.54, 0.51), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.5, 0.5), Vector3(0, h - 0.3, front + 0.25), Color(0.56, 0.54, 0.51), true, null, Vector2.ONE, "stone")
_add_box(building, Vector3(40, 0.22, 0.2), Vector3(0, 10.9, -3.9), Color(0.58, 0.56, 0.53), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.22, 0.2), Vector3(0, 10.9, front + 0.1), Color(0.58, 0.56, 0.53), true, null, Vector2.ONE, "stone")
for bay in [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3]: for bay in spec.get("pilaster_columns", [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3]):
_add_box(building, Vector3(0.5, 8.4, 0.24), Vector3(bay, 12.6, -3.9), _add_box(building, Vector3(0.5, 8.4, 0.24), Vector3(float(bay), 12.6, front + 0.1),
Color(0.8, 0.78, 0.76), true, T_BRICK, Vector2(0.3, 4.7), "brick") Color(0.8, 0.78, 0.76), true, T_BRICK, Vector2(0.3, 4.7), "brick")
# Entrance recess + door recess frame # Entrance recess + door recess frame
_add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, -3.9), Color(0.08, 0.08, 0.09), false, null, Vector2.ONE, "stone") _add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, front + 0.1), Color(0.08, 0.08, 0.09), false, null, Vector2.ONE, "stone")
# Door frame columns and lintel, so the entrance is framed rather than painted on. # Door frame columns and lintel, so the entrance is framed rather than painted on.
for side in [-1.55, 1.55]: for side in [-1.55, 1.55]:
_add_box(building, Vector3(0.28, 5.2, 0.42), Vector3(side, 2.9, -3.86), Color(0.30, 0.28, 0.26), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(0.28, 5.2, 0.42), Vector3(side, 2.9, front + 0.14), Color(0.30, 0.28, 0.26), true, null, Vector2.ONE, "stone")
_add_box(building, Vector3(3.4, 0.3, 0.42), Vector3(0, 5.55, -3.86), Color(0.30, 0.28, 0.26), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(3.4, 0.3, 0.42), Vector3(0, 5.55, front + 0.14), Color(0.30, 0.28, 0.26), true, null, Vector2.ONE, "stone")
# Door frame and dark door # Door frame and dark door
_add_box(building, Vector3(3.0, 5.2, 0.3), Vector3(0, 2.9, -3.98), Color(0.13, 0.12, 0.11), true, null, Vector2.ONE, "painted_metal") _add_box(building, Vector3(3.0, 5.2, 0.3), Vector3(0, 2.9, front + 0.02), Color(0.13, 0.12, 0.11), true, null, Vector2.ONE, "painted_metal")
# Door glass (semi-transparent) # Door glass (semi-transparent)
var glass := _add_box(building, Vector3(1.8, 1.4, 0.34), Vector3(0, 3.8, -3.9), Color(0.6, 0.75, 0.9, 0.55)) var glass := _add_box(building, Vector3(1.8, 1.4, 0.34), Vector3(0, 3.8, front + 0.1), Color(0.6, 0.75, 0.9, 0.55))
glass.material_override = Materials.glass(Color(0.55, 0.7, 0.88, 0.55)) glass.material_override = Materials.glass(Color(0.55, 0.7, 0.88, 0.55))
# Door handles: two slim metal bars where the guard reaches for the lock. # Door handles: two slim metal bars where the guard reaches for the lock.
for side in [-0.42, 0.42]: for side in [-0.42, 0.42]:
_add_box(building, Vector3(0.06, 0.5, 0.06), Vector3(side, 2.6, -3.79), Color(0.75, 0.74, 0.70), false, null, Vector2.ONE, "metal") _add_box(building, Vector3(0.06, 0.5, 0.06), Vector3(side, 2.6, front + 0.21), Color(0.75, 0.74, 0.70), false, null, Vector2.ONE, "metal")
# Door lock indicator (green = open, red = locked), placed beside the handle # Door lock indicator (green = open, red = locked), placed beside the handle
door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), Vector3(-1.8, 1.7, -3.82), Color(0.1, 0.9, 0.3)) door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), Vector3(-1.8, 1.7, front + 0.18), Color(0.1, 0.9, 0.3))
door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3)) door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3))
# Blender glazed double door in front of the recessed frame. # Blender glazed double door in front of the recessed frame.
var door_model := _load_prop_scene("double_door") var door_model := _load_prop_scene("double_door")
if door_model != null: if door_model != null:
door_model.name = "LibraryDoubleDoor" door_model.name = "LibraryDoubleDoor"
door_model.position = Vector3(0, 0.0, -3.84) door_model.position = Vector3(0, 0.0, front + 0.16)
building.add_child(door_model) building.add_child(door_model)
library_nodes.append(door_model) library_nodes.append(door_model)
_apply_library_ao(door_model, "double_door") _apply_library_ao(door_model, "double_door")
@@ -154,18 +240,21 @@ func _build_building() -> void:
_lamp_bulb_mats.append(material) _lamp_bulb_mats.append(material)
_build_facade_windows(building) _build_facade_windows(building)
var col := BoxShape3D.new() var col := BoxShape3D.new()
col.size = Vector3(40, 18, 4) col.size = Vector3(w, h, d)
var so := building.create_shape_owner(building) var so := building.create_shape_owner(building)
building.shape_owner_add_shape(so, col) building.shape_owner_add_shape(so, col)
building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 9, -6))) building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, h * 0.5, cz)))
root.add_child(building) root.add_child(building)
## Regular window bands above the entrance. Panes are dark glass by day and ## Regular window bands above the entrance. Panes are dark glass by day and
## light up after dark, which gives the facade depth at night. ## light up after dark, which gives the facade depth at night.
func _build_facade_windows(parent: Node3D) -> void: func _build_facade_windows(parent: Node3D) -> void:
var columns := [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0] var columns: Array = spec.get("window_columns", [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0])
for y in [9.2, 12.4]: var rows: Array = spec.get("window_rows", [9.2, 12.4])
for x in columns: for value in rows:
var y := float(value)
for x_value in columns:
var x := float(x_value)
# Recessed reveal, then the pane: gives the window a dark frame edge. # Recessed reveal, then the pane: gives the window a dark frame edge.
_add_box(parent, Vector3(1.96, 2.46, 0.1), Vector3(x, y, -3.99), _add_box(parent, Vector3(1.96, 2.46, 0.1), Vector3(x, y, -3.99),
Color(0.22, 0.21, 0.20), false, null, Vector2.ONE, "stone") Color(0.22, 0.21, 0.20), false, null, Vector2.ONE, "stone")
@@ -188,12 +277,13 @@ func _build_steps() -> void:
# The ground and building provide the actual collision boundaries. # The ground and building provide the actual collision boundaries.
var steps := StaticBody3D.new() var steps := StaticBody3D.new()
steps.name = "Steps" steps.name = "Steps"
var step_w := 8.0 var step_w := float(spec.get("stair_width", 8.0))
var tread := 0.6 var tread := float(spec.get("stair_tread", 0.6))
var height := 0.16 var height := float(spec.get("stair_height", 0.16))
for i in 4: var count := int(spec.get("stair_count", 4))
for i in count:
var y: float = i * height var y: float = i * height
var z: float = 4.0 * tread - i * tread var z: float = float(count) * tread - i * tread
var w: float = step_w - i * 0.35 var w: float = step_w - i * 0.35
_add_box(steps, Vector3(w, height, tread), Vector3(0, y + height / 2.0, z), Color(0.55, 0.52, 0.5)) _add_box(steps, Vector3(w, height, tread), Vector3(0, y + height / 2.0, z), Color(0.55, 0.52, 0.5))
# Add collision shape to each tread # Add collision shape to each tread
@@ -203,10 +293,11 @@ func _build_steps() -> void:
steps.shape_owner_add_shape(owner_id, tread_col) steps.shape_owner_add_shape(owner_id, tread_col)
steps.shape_owner_set_transform(owner_id, Transform3D(Basis(), Vector3(0, y + height / 2.0, z))) steps.shape_owner_set_transform(owner_id, Transform3D(Basis(), Vector3(0, y + height / 2.0, z)))
# Landing at the top with collision # Landing at the top with collision
var landing_y: float = 4 * height + 0.1 var landing_y: float = count * height + 0.1
_add_box(steps, Vector3(9, 0.2, 3), Vector3(0, landing_y, -1.5), Color(0.5, 0.47, 0.45)) var landing_w := step_w + 1.0
_add_box(steps, Vector3(landing_w, 0.2, 3), Vector3(0, landing_y, -1.5), Color(0.5, 0.47, 0.45))
var landing_col := BoxShape3D.new() var landing_col := BoxShape3D.new()
landing_col.size = Vector3(9, 0.2, 3) landing_col.size = Vector3(landing_w, 0.2, 3)
var landing_owner := steps.create_shape_owner(steps) var landing_owner := steps.create_shape_owner(steps)
steps.shape_owner_add_shape(landing_owner, landing_col) steps.shape_owner_add_shape(landing_owner, landing_col)
steps.shape_owner_set_transform(landing_owner, Transform3D(Basis(), Vector3(0, landing_y, -1.5))) steps.shape_owner_set_transform(landing_owner, Transform3D(Basis(), Vector3(0, landing_y, -1.5)))
@@ -215,12 +306,14 @@ func _build_steps() -> void:
func _build_sidewalk() -> void: func _build_sidewalk() -> void:
var sidewalk := StaticBody3D.new() var sidewalk := StaticBody3D.new()
sidewalk.name = "Sidewalk" sidewalk.name = "Sidewalk"
_add_box(sidewalk, Vector3(30, 0.18, 8), Vector3(0, 0.09, 8), Color(0.55, 0.55, 0.54), false, T_CONCRETE, Vector2(9, 2.4), "concrete") var w := float(spec.get("sidewalk_width", 30.0))
var d := float(spec.get("sidewalk_depth", 8.0))
_add_box(sidewalk, Vector3(w, 0.18, d), Vector3(0, 0.09, d * 0.5), Color(0.55, 0.55, 0.54), false, T_CONCRETE, Vector2(w * 0.3, d * 0.3), "concrete")
var col := BoxShape3D.new() var col := BoxShape3D.new()
col.size = Vector3(30, 0.36, 8) col.size = Vector3(w, 0.36, d)
var so := sidewalk.create_shape_owner(sidewalk) var so := sidewalk.create_shape_owner(sidewalk)
sidewalk.shape_owner_add_shape(so, col) sidewalk.shape_owner_add_shape(so, col)
sidewalk.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, 8))) sidewalk.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, d * 0.5)))
root.add_child(sidewalk) root.add_child(sidewalk)
func _build_street() -> void: func _build_street() -> void:
@@ -230,20 +323,24 @@ func _build_street() -> void:
# white looked like more pavement. # white looked like more pavement.
# The street keeps its own material so night mode can switch to the wet # The street keeps its own material so night mode can switch to the wet
# asphalt map (glossier, with lamp reflections) and back. # asphalt map (glossier, with lamp reflections) and back.
_street_mat = Materials.asphalt(Color(0.62, 0.63, 0.66), Vector2(30, 7)) var street_w := float(spec.get("street_width", 120.0))
var street_mesh := _add_box(street, Vector3(120, 0.1, 26), Vector3(0, 0.05, 22), var street_d := float(spec.get("street_depth", 26.0))
Color(0.62, 0.63, 0.66), false, T_ASPHALT, Vector2(30, 7), "asphalt") var curb_z := float(spec.get("sidewalk_depth", 8.0)) + 4.1
var street_z := curb_z + street_d * 0.5 - 3.1
_street_mat = Materials.asphalt(Color(0.62, 0.63, 0.66), Vector2(street_w * 0.25, street_d * 0.27))
var street_mesh := _add_box(street, Vector3(street_w, 0.1, street_d), Vector3(0, 0.05, street_z),
Color(0.62, 0.63, 0.66), false, T_ASPHALT, Vector2(street_w * 0.25, street_d * 0.27), "asphalt")
street_mesh.material_override = _street_mat street_mesh.material_override = _street_mat
# Center dashes along the street (x varied so they form a lane line, not a stack). # Center dashes along the street (x varied so they form a lane line, not a stack).
for i in range(-40, 41, 4): for i in range(-int(street_w / 3.0), int(street_w / 3.0) + 1, 4):
_add_box(street, Vector3(0.2, 0.02, 1.2), Vector3(float(i), 0.11, 22), Color(0.85, 0.85, 0.30), false, null, Vector2.ONE, "concrete") _add_box(street, Vector3(0.2, 0.02, 1.2), Vector3(float(i), 0.11, street_z), Color(0.85, 0.85, 0.30), false, null, Vector2.ONE, "concrete")
# Curb between the sidewalk and the roadway. # Curb between the sidewalk and the roadway.
_add_box(street, Vector3(120, 0.22, 0.5), Vector3(0, 0.11, 12.1), Color(0.62, 0.61, 0.58), false, null, Vector2.ONE, "concrete") _add_box(street, Vector3(street_w, 0.22, 0.5), Vector3(0, 0.11, curb_z), Color(0.62, 0.61, 0.58), false, null, Vector2.ONE, "concrete")
var col := BoxShape3D.new() var col := BoxShape3D.new()
col.size = Vector3(120, 0.2, 26) col.size = Vector3(street_w, 0.2, street_d)
var so := street.create_shape_owner(street) var so := street.create_shape_owner(street)
street.shape_owner_add_shape(so, col) street.shape_owner_add_shape(so, col)
street.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, 22))) street.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, street_z)))
root.add_child(street) root.add_child(street)
func _build_navmesh() -> void: func _build_navmesh() -> void:
@@ -256,9 +353,11 @@ func _build_navmesh() -> void:
mesh.agent_max_climb = 0.8 mesh.agent_max_climb = 0.8
mesh.agent_max_slope = 45.0 mesh.agent_max_slope = 45.0
mesh.cell_size = 0.25 mesh.cell_size = 0.25
var nav_min: Vector2 = spec.get("nav_min", Vector2(-18.0, -0.6))
var nav_max: Vector2 = spec.get("nav_max", Vector2(18.0, 20.0))
var verts := PackedVector3Array([ var verts := PackedVector3Array([
Vector3(-18, 0.0, -0.6), Vector3(18, 0.0, -0.6), Vector3(nav_min.x, 0.0, nav_min.y), Vector3(nav_max.x, 0.0, nav_min.y),
Vector3(18, 0.0, 20.0), Vector3(-18, 0.0, 20.0), Vector3(nav_max.x, 0.0, nav_max.y), Vector3(nav_min.x, 0.0, nav_max.y),
]) ])
mesh.set_vertices(verts) mesh.set_vertices(verts)
mesh.add_polygon(PackedInt32Array([0, 1, 2, 3])) mesh.add_polygon(PackedInt32Array([0, 1, 2, 3]))
@@ -377,7 +476,7 @@ func _build_marquee() -> void:
## a parked sedan, and wall lanterns. Kept separate so the placement validator ## a parked sedan, and wall lanterns. Kept separate so the placement validator
## only sees grounded sidewalk furniture. ## only sees grounded sidewalk furniture.
func _build_library_dressing() -> void: func _build_library_dressing() -> void:
if ResourceLoader.exists(BLENDER_PROP_DIR + "handrail.glb"): if _feature("handrail") and ResourceLoader.exists(BLENDER_PROP_DIR + "handrail.glb"):
var rail := _load_prop_scene("handrail") var rail := _load_prop_scene("handrail")
if rail != null: if rail != null:
rail.name = "LibraryHandrail" rail.name = "LibraryHandrail"
@@ -386,7 +485,7 @@ func _build_library_dressing() -> void:
root.add_child(rail) root.add_child(rail)
library_nodes.append(rail) library_nodes.append(rail)
_apply_library_ao(rail, "handrail") _apply_library_ao(rail, "handrail")
if ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"): if _feature("sedan") and ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"):
var sedan := _add_prop_model(root, "sedan", Vector3(6.0, 0.1, 17.0), 180.0) var sedan := _add_prop_model(root, "sedan", Vector3(6.0, 0.1, 17.0), 180.0)
sedan.name = "ParkedSedan" sedan.name = "ParkedSedan"
if not library_nodes.has(sedan): if not library_nodes.has(sedan):
@@ -394,7 +493,7 @@ func _build_library_dressing() -> void:
for material in _emissive_materials(sedan): for material in _emissive_materials(sedan):
if not _lamp_bulb_mats.has(material): if not _lamp_bulb_mats.has(material):
_lamp_bulb_mats.append(material) _lamp_bulb_mats.append(material)
if ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"): if _feature("wall_lanterns") and ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"):
for side in [-4.9, 4.9]: for side in [-4.9, 4.9]:
var lantern := _load_prop_scene("wall_lantern") var lantern := _load_prop_scene("wall_lantern")
if lantern == null: if lantern == null:
@@ -440,12 +539,13 @@ func _build_street_furniture() -> void:
_collect_emissive(holder) _collect_emissive(holder)
# Blender library street trees replace the cone trees. Flank the walkway and # Blender library street trees replace the cone trees. Flank the walkway and
# keep the sight line to the door clear. # keep the sight line to the door clear.
for i in 6: if _feature("street_trees"):
var side := -1.0 if i % 2 == 0 else 1.0 for i in 6:
var x := side * (6.5 + float(int(i / 2)) * 3.0) var side := -1.0 if i % 2 == 0 else 1.0
var z := 4.0 + float((i * 7) % 5) var x := side * (6.5 + float(int(i / 2)) * 3.0)
_add_prop_model(root, "street_tree", Vector3(x, 0.18, z), var z := 4.0 + float((i * 7) % 5)
float((i * 37) % 360)) _add_prop_model(root, "street_tree", Vector3(x, 0.18, z),
float((i * 37) % 360))
## Total triangles across the placed Blender library pieces, for the draw budget ## Total triangles across the placed Blender library pieces, for the draw budget
## test. Counts indices when present, otherwise the vertex stream. ## test. Counts indices when present, otherwise the vertex stream.
@@ -585,8 +685,9 @@ func _build_street_detail() -> void:
var detail := Node3D.new() var detail := Node3D.new()
detail.name = "StreetDetail" detail.name = "StreetDetail"
root.add_child(detail) root.add_child(detail)
for index in PROP_PLACEMENTS.size(): var placements: Array = spec.get("prop_placements", PROP_PLACEMENTS)
var entry: Dictionary = PROP_PLACEMENTS[index] for index in placements.size():
var entry: Dictionary = placements[index]
var base_rot := float(entry.get("rot", 0.0)) var base_rot := float(entry.get("rot", 0.0))
var variant := variant_for(index, base_rot) var variant := variant_for(index, base_rot)
var holder := _add_prop_model(detail, str(entry["prop"]), var holder := _add_prop_model(detail, str(entry["prop"]),
@@ -821,7 +922,7 @@ func _build_guard_spawn() -> void:
# The guard's head offset is 1.6m, so spawn at y=0.76-1.6=-0.84 to place feet on ground # The guard's head offset is 1.6m, so spawn at y=0.76-1.6=-0.84 to place feet on ground
# But we want the guard at eye-level with the sidewalk, so adjust to y=0.1 for feet # But we want the guard at eye-level with the sidewalk, so adjust to y=0.1 for feet
# Then the head will be at y=1.7 (1.6 offset + 0.1 base) # Then the head will be at y=1.7 (1.6 offset + 0.1 base)
marker.position = Vector3(0, 0.1, -2.0) marker.position = spec.get("guard_spawn", Vector3(0, 0.1, -2.0))
root.add_child(marker) root.add_child(marker)
func _lock_indicator_material(color: Color) -> StandardMaterial3D: func _lock_indicator_material(color: Color) -> StandardMaterial3D:
+33
View File
@@ -19,6 +19,39 @@ func test_navmesh_built() -> bool:
root.free() root.free()
return a and b return a and b
func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool:
var hollywood := WorldBuilder.new(Node3D.new())
var joke := WorldBuilder.new(Node3D.new(), "joke_shop")
var studio := WorldBuilder.new(Node3D.new(), "tv_studio")
var unknown := WorldBuilder.new(Node3D.new(), "not_a_venue")
var a := TestUtils.check(hollywood.venue_id == "hollywood_building",
"the default venue is Hollywood")
var b := TestUtils.check(float(hollywood.spec["facade_width"]) > float(joke.spec["facade_width"]),
"the Joke Shop alley is narrower than the boulevard")
var c := TestUtils.check(not bool(joke.spec["features"]["skyline"])
and not bool(joke.spec["features"]["marquee"]),
"the Joke Shop disables the skyline and marquee")
var d := TestUtils.check(bool(studio.spec["features"]["bulb_canopy"])
and not bool(studio.spec["features"]["marquee"]),
"the studio uses a bulb canopy instead of the marquee")
var e := TestUtils.check(unknown.venue_id == "hollywood_building",
"an unknown venue falls back to Hollywood")
var root := Node3D.new()
var builder := WorldBuilder.new(root, "joke_shop")
builder.build()
var body := root.find_child("Building", true, false)
var facade := body.get_child(0) as MeshInstance3D if body != null else null
var f := TestUtils.check(facade != null and (facade.mesh as BoxMesh).size.x < 20.0,
"the Joke Shop builds the narrow facade")
var g := TestUtils.check(root.find_child("SkylineBackdrop", true, false) == null,
"the Joke Shop builds no skyline ring")
hollywood.root.free()
joke.root.free()
studio.root.free()
unknown.root.free()
root.free()
return a and b and c and d and e and f and g
func test_phase_lighting_config_covered() -> bool: func test_phase_lighting_config_covered() -> bool:
var ok := true var ok := true
for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]: for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]: