Build distinct Joke Shop and TV Studio venues
Add the alley walls, warm lantern, shutters, dumpster, AC clutter and door step for the Joke Shop. Build the studio stone facade, bulb canopy, glossy black car, stanchions and barriers. Rebuild only the world container when the venue changes, so the guard and UI survive; add per-venue navigation checks. Capture gallery shots 12/13, update the concept mapping and docs. Headless 231/231, Python 87/87, screenshot gate 13 good/0 bad. The vision-judge retry is tracked separately because the API key is invalid.
This commit is contained in:
1 parent
4cca2af2dd
commit
8b0845bcce
25 files changed
+347
-87
No files matched your search
@@ -32,6 +32,9 @@ const SettingsPanel := preload("res://src/game/ui/settings_panel.gd")
|
||||
const EventTimeline := preload("res://src/game/ui/event_timeline.gd")
|
||||
|
||||
var world_builder: WorldBuilder = null
|
||||
## Dedicated container for the procedural world, so switching venues can rebuild
|
||||
## the geometry without touching the guard, UI, or other scene-root children.
|
||||
var world_root: Node3D = null
|
||||
var door := DoorState.new()
|
||||
var flashlight_on := false
|
||||
var body_camera_on := false
|
||||
@@ -135,8 +138,11 @@ func _ready() -> void:
|
||||
scene_root = Node3D.new()
|
||||
scene_root.name = "SceneRoot"
|
||||
add_child(scene_root)
|
||||
world_root = Node3D.new()
|
||||
world_root.name = "World"
|
||||
scene_root.add_child(world_root)
|
||||
|
||||
world_builder = WorldBuilder.new(scene_root)
|
||||
world_builder = WorldBuilder.new(world_root, active_scenario_id)
|
||||
world_builder.build()
|
||||
_ensure_lighting()
|
||||
|
||||
@@ -235,9 +241,26 @@ func _build_scenario(id: String) -> VenueScenario:
|
||||
func switch_scenario(id: String) -> void:
|
||||
active_scenario_id = id
|
||||
selected_scenario = _build_scenario(id)
|
||||
_rebuild_world_for_venue(id)
|
||||
if coordinator:
|
||||
restart_shift()
|
||||
|
||||
## Rebuild the procedural venue in place. The guard/UI stay under `SceneRoot`;
|
||||
## only the `World` container is replaced, so navigation, props, lights and the
|
||||
## venue-specific dressing match the selected scenario.
|
||||
func _rebuild_world_for_venue(id: String) -> void:
|
||||
if world_root == null:
|
||||
return
|
||||
for child in world_root.get_children():
|
||||
child.free()
|
||||
world_builder = WorldBuilder.new(world_root, id)
|
||||
world_builder.build()
|
||||
if guard != null:
|
||||
guard.global_position = world_builder.spec.get("guard_spawn", Vector3(0, 0.1, -2.0))
|
||||
guard.rotation.y = PI
|
||||
if world_builder != null:
|
||||
world_builder.set_door_locked(door.locked)
|
||||
|
||||
func restart_shift() -> void:
|
||||
if shift_review_panel:
|
||||
shift_review_panel.hide()
|
||||
@@ -301,6 +324,7 @@ func _on_venue_selected(scenario_id: String) -> void:
|
||||
active_scenario_id = scenario_id
|
||||
_venue_index = maxi(0, VENUE_ORDER.find(scenario_id))
|
||||
selected_scenario = _build_scenario(scenario_id)
|
||||
_rebuild_world_for_venue(scenario_id)
|
||||
if hud:
|
||||
hud.set_wand_icon(false)
|
||||
_reset_shift_state()
|
||||
|
||||
+170
-35
@@ -68,7 +68,8 @@ 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,
|
||||
"facade_center_z": -6.0, "facade_texture": T_BRICK,
|
||||
"facade_tint": Color(0.86, 0.85, 0.83), "facade_role": "brick",
|
||||
"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],
|
||||
@@ -80,13 +81,14 @@ const VENUE_SPECS := {
|
||||
"features": {
|
||||
"marquee": true, "skyline": true, "handrail": true, "sedan": true,
|
||||
"wall_lanterns": true, "palms": true, "street_trees": true,
|
||||
"crosswalk": true, "bulb_canopy": false,
|
||||
"street_lamps": 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,
|
||||
"facade_center_z": -6.5, "facade_texture": T_BRICK,
|
||||
"facade_tint": Color(0.78, 0.66, 0.58), "facade_role": "brick",
|
||||
"window_columns": [-3.8, 3.8],
|
||||
"pilaster_columns": [-5.2, 5.2],
|
||||
"window_rows": [8.2],
|
||||
@@ -98,14 +100,15 @@ const VENUE_SPECS := {
|
||||
"features": {
|
||||
"marquee": false, "skyline": false, "handrail": false, "sedan": false,
|
||||
"wall_lanterns": true, "palms": false, "street_trees": false,
|
||||
"crosswalk": false, "bulb_canopy": false,
|
||||
"street_lamps": 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,
|
||||
"facade_center_z": -6.0, "facade_texture": T_CONCRETE,
|
||||
"facade_tint": Color(0.58, 0.57, 0.55), "facade_role": "stone",
|
||||
"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],
|
||||
@@ -114,12 +117,19 @@ const VENUE_SPECS := {
|
||||
"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),
|
||||
"sedan_position": Vector3(5.4, 0.1, 8.8), "sedan_rotation": 90.0,
|
||||
"features": {
|
||||
"marquee": false, "skyline": true, "handrail": true, "sedan": true,
|
||||
"wall_lanterns": false, "palms": false, "street_trees": true,
|
||||
"crosswalk": true, "bulb_canopy": true,
|
||||
"street_lamps": true, "crosswalk": true, "bulb_canopy": true,
|
||||
},
|
||||
"prop_placements": [],
|
||||
"prop_placements": [
|
||||
{"zone": "entrance", "prop": "stanchion", "x": -2.8, "z": 4.4},
|
||||
{"zone": "entrance", "prop": "stanchion", "x": 2.8, "z": 4.4},
|
||||
{"zone": "entrance", "prop": "crowd_barrier", "x": -5.2, "z": 4.2},
|
||||
{"zone": "entrance", "prop": "crowd_barrier", "x": 5.2, "z": 4.2},
|
||||
{"zone": "curb", "prop": "security_podium", "x": 4.6, "z": 5.9, "rot": -15.0},
|
||||
],
|
||||
},
|
||||
}
|
||||
const DEFAULT_VENUE := "hollywood_building"
|
||||
@@ -147,6 +157,8 @@ func build() -> void:
|
||||
_build_skyline_backdrop()
|
||||
if _feature("marquee"):
|
||||
_build_marquee()
|
||||
if _feature("bulb_canopy"):
|
||||
_build_bulb_canopy()
|
||||
_build_steps()
|
||||
_build_sidewalk()
|
||||
_build_street()
|
||||
@@ -155,6 +167,8 @@ func build() -> void:
|
||||
_build_street_furniture()
|
||||
_build_street_detail()
|
||||
_build_library_dressing()
|
||||
if venue_id == "joke_shop":
|
||||
_build_alley_dressing()
|
||||
_build_guard_spawn()
|
||||
_build_navmesh()
|
||||
set_night_mode(false)
|
||||
@@ -197,8 +211,11 @@ func _build_building() -> void:
|
||||
var d := float(spec["facade_depth"])
|
||||
var cz := float(spec["facade_center_z"])
|
||||
var front := cz + d * 0.5
|
||||
# Main facade (textured 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")
|
||||
# Main facade (brick on the boulevard/alley, stone on the studio)
|
||||
var facade_texture: Texture2D = spec.get("facade_texture", T_BRICK)
|
||||
var facade_tint: Color = spec.get("facade_tint", Color(0.86, 0.85, 0.83))
|
||||
var facade_role := str(spec.get("facade_role", "brick"))
|
||||
_add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), facade_tint, false, facade_texture, Vector2(w * 0.55, h * 0.55), facade_role)
|
||||
# Stone base course so the facade has a footing instead of meeting the steps dead-on.
|
||||
_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
|
||||
@@ -209,7 +226,7 @@ func _build_building() -> void:
|
||||
_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 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(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, facade_texture, Vector2(0.3, 4.7), facade_role)
|
||||
# Entrance recess + door recess frame
|
||||
_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.
|
||||
@@ -486,26 +503,143 @@ func _build_library_dressing() -> void:
|
||||
library_nodes.append(rail)
|
||||
_apply_library_ao(rail, "handrail")
|
||||
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_pos: Vector3 = spec.get("sedan_position", Vector3(6.0, 0.1, 17.0))
|
||||
var sedan_rot := float(spec.get("sedan_rotation", 180.0))
|
||||
var sedan := _add_prop_model(root, "sedan", sedan_pos, sedan_rot)
|
||||
sedan.name = "ParkedSedan"
|
||||
if venue_id == "tv_studio":
|
||||
_apply_car_paint(sedan)
|
||||
if not library_nodes.has(sedan):
|
||||
library_nodes.append(sedan)
|
||||
for material in _emissive_materials(sedan):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
if _feature("wall_lanterns") and ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"):
|
||||
for side in [-4.9, 4.9]:
|
||||
var lantern := _load_prop_scene("wall_lantern")
|
||||
if lantern == null:
|
||||
continue
|
||||
lantern.name = "WallLantern"
|
||||
lantern.position = Vector3(side, 3.3, -3.78)
|
||||
root.add_child(lantern)
|
||||
library_nodes.append(lantern)
|
||||
_apply_library_ao(lantern, "wall_lantern")
|
||||
for material in _emissive_materials(lantern):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
if venue_id == "joke_shop":
|
||||
_add_wall_lantern(Vector3(3.2, 3.3, -3.72), true)
|
||||
else:
|
||||
for side in [-4.9, 4.9]:
|
||||
_add_wall_lantern(Vector3(side, 3.3, -3.78), false)
|
||||
|
||||
## The TV studio entrance canopy: a dark metal slab with a row of warm bulbs,
|
||||
## replacing the boulevard marquee.
|
||||
func _build_bulb_canopy() -> void:
|
||||
var front := float(spec["facade_center_z"]) + float(spec["facade_depth"]) * 0.5
|
||||
var canopy := _add_box(root, Vector3(7.2, 0.22, 2.2),
|
||||
Vector3(0, 5.7, front + 1.0), Color(0.16, 0.16, 0.18),
|
||||
true, null, Vector2.ONE, "metal")
|
||||
canopy.name = "BulbCanopy"
|
||||
for x in [-3.0, 3.0]:
|
||||
_add_box(root, Vector3(0.12, 5.5, 0.12),
|
||||
Vector3(x, 2.75, front + 1.95), Color(0.20, 0.20, 0.22),
|
||||
true, null, Vector2.ONE, "metal")
|
||||
for x in [-2.8, -1.4, 0.0, 1.4, 2.8]:
|
||||
var bulb := MeshInstance3D.new()
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 0.075
|
||||
sphere.height = 0.15
|
||||
bulb.mesh = sphere
|
||||
var mat := _emissive_material(Color(1.0, 0.78, 0.42))
|
||||
bulb.material_override = mat
|
||||
bulb.position = Vector3(x, 5.53, front + 1.0)
|
||||
root.add_child(bulb)
|
||||
if not _marquee_emissive.has(mat):
|
||||
_marquee_emissive.append(mat)
|
||||
for x in [-2.2, 2.2]:
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(x, 5.15, front + 1.4)
|
||||
omni.omni_range = 8.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1.0, 0.72, 0.36)
|
||||
root.add_child(omni)
|
||||
_lamp_omni.append(omni)
|
||||
|
||||
## Glossy black paint for the studio's parked car. The library sedan keeps its
|
||||
## tintable slots; the studio overrides the body meshes with a single paint.
|
||||
func _apply_car_paint(holder: Node3D) -> void:
|
||||
var paint := StandardMaterial3D.new()
|
||||
paint.resource_name = "StudioCarPaint"
|
||||
paint.albedo_color = Color(0.018, 0.020, 0.024)
|
||||
paint.metallic = 0.85
|
||||
paint.roughness = 0.12
|
||||
paint.metallic_specular = 0.8
|
||||
for node in holder.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := node as MeshInstance3D
|
||||
if mi != null:
|
||||
mi.material_override = paint
|
||||
|
||||
## One wall lantern plus an optional warm light. The Joke Shop uses a single
|
||||
## lantern as its key light; the boulevard keeps two unlit visual lanterns.
|
||||
func _add_wall_lantern(pos: Vector3, with_light: bool) -> void:
|
||||
var lantern := _load_prop_scene("wall_lantern")
|
||||
if lantern == null:
|
||||
return
|
||||
lantern.name = "WallLantern"
|
||||
lantern.position = pos
|
||||
root.add_child(lantern)
|
||||
library_nodes.append(lantern)
|
||||
_apply_library_ao(lantern, "wall_lantern")
|
||||
for material in _emissive_materials(lantern):
|
||||
if not _lamp_bulb_mats.has(material):
|
||||
_lamp_bulb_mats.append(material)
|
||||
if with_light:
|
||||
var omni := OmniLight3D.new()
|
||||
omni.name = "LanternGlow"
|
||||
omni.position = pos + Vector3(0.0, -0.1, 0.45)
|
||||
omni.omni_range = 9.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1.0, 0.72, 0.38)
|
||||
root.add_child(omni)
|
||||
_lamp_omni.append(omni)
|
||||
|
||||
## The Joke Shop alley: two brick side walls frame a 12 m passage, with
|
||||
## shutters, a recessed door step, a dumpster and wall-mounted AC boxes.
|
||||
func _build_alley_dressing() -> void:
|
||||
var front := float(spec["facade_center_z"]) + float(spec["facade_depth"]) * 0.5
|
||||
var height := float(spec["facade_height"])
|
||||
var wall_x := float(spec["facade_width"]) * 0.5 + 0.35
|
||||
for side in [-1.0, 1.0]:
|
||||
var wall := _add_box(root, Vector3(0.7, height, 15.0),
|
||||
Vector3(side * wall_x, height * 0.5, front + 7.5), Color(0.72, 0.70, 0.68),
|
||||
true, T_BRICK, Vector2(8, 6), "brick")
|
||||
wall.name = "AlleyWall"
|
||||
_add_box(root, Vector3(0.9, 0.35, 15.0),
|
||||
Vector3(side * wall_x, 0.18, front + 7.5), Color(0.44, 0.42, 0.40),
|
||||
true, null, Vector2.ONE, "stone")
|
||||
# Shutters over the two upper windows.
|
||||
for x in [-3.8, 3.8]:
|
||||
_add_box(root, Vector3(2.0, 2.5, 0.12), Vector3(x, 8.2, front + 0.22),
|
||||
Color(0.18, 0.24, 0.21), true, null, Vector2.ONE, "painted_metal")
|
||||
for slat in range(-4, 5):
|
||||
_add_box(root, Vector3(1.9, 0.07, 0.04),
|
||||
Vector3(x, 8.2 + slat * 0.28, front + 0.30),
|
||||
Color(0.24, 0.31, 0.27), false, null, Vector2.ONE, "painted_metal")
|
||||
# The recessed door gets a low stone step into the alley.
|
||||
_add_box(root, Vector3(3.4, 0.16, 0.55), Vector3(0, 0.08, front + 0.75),
|
||||
Color(0.48, 0.45, 0.42), true, null, Vector2.ONE, "stone")
|
||||
# Dumpster and its lid; a solid body so guests and the guard walk around it.
|
||||
var dump := StaticBody3D.new()
|
||||
dump.name = "AlleyDumpster"
|
||||
dump.position = Vector3(-4.2, 0, front + 9.2)
|
||||
_add_box(dump, Vector3(1.9, 1.15, 1.25), Vector3(0, 0.58, 0),
|
||||
Color(0.16, 0.28, 0.21), true, null, Vector2.ONE, "painted_metal")
|
||||
_add_box(dump, Vector3(2.0, 0.08, 1.35), Vector3(0, 1.18, -0.02),
|
||||
Color(0.11, 0.19, 0.15), true, null, Vector2.ONE, "painted_metal")
|
||||
var dump_col := BoxShape3D.new()
|
||||
dump_col.size = Vector3(1.9, 1.2, 1.3)
|
||||
var owner_id := dump.create_shape_owner(dump)
|
||||
dump.shape_owner_add_shape(owner_id, dump_col)
|
||||
dump.shape_owner_set_transform(owner_id, Transform3D(Basis(), Vector3(0, 0.6, 0)))
|
||||
root.add_child(dump)
|
||||
# AC boxes on the inside faces of the walls, above head height.
|
||||
for side in [-1.0, 1.0]:
|
||||
for z in [front + 3.0, front + 7.0]:
|
||||
_add_box(root, Vector3(0.55, 0.8, 1.1),
|
||||
Vector3(side * (wall_x - 0.5), 5.8, z),
|
||||
Color(0.56, 0.57, 0.55), true, null, Vector2.ONE, "metal")
|
||||
_add_box(root, Vector3(0.08, 0.55, 0.8),
|
||||
Vector3(side * (wall_x - 0.78), 5.8, z),
|
||||
Color(0.22, 0.23, 0.22), false, null, Vector2.ONE, "metal")
|
||||
|
||||
## Emissive materials under a prop, used to tie library bulbs/LEDs to night mode.
|
||||
func _emissive_materials(holder: Node3D) -> Array[StandardMaterial3D]:
|
||||
@@ -525,18 +659,19 @@ func _build_street_furniture() -> void:
|
||||
# Blender library street lamps (bulbs + omni lights, off by day). The lamp
|
||||
# arm points to local -X, so the left side is rotated 180 degrees to face
|
||||
# the sidewalk.
|
||||
for side in [-7.5, 7.5]:
|
||||
var holder := _add_prop_model(root, "street_lamp",
|
||||
Vector3(side, 0.18, 8.5), 180.0 if side < 0.0 else 0.0)
|
||||
holder.name = "Lamp_%d" % int(side)
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(-0.85, 4.65, 0)
|
||||
omni.omni_range = 12.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1, 0.93, 0.75)
|
||||
holder.add_child(omni)
|
||||
_lamp_omni.append(omni)
|
||||
_collect_emissive(holder)
|
||||
if _feature("street_lamps"):
|
||||
for side in [-7.5, 7.5]:
|
||||
var holder := _add_prop_model(root, "street_lamp",
|
||||
Vector3(side, 0.18, 8.5), 180.0 if side < 0.0 else 0.0)
|
||||
holder.name = "Lamp_%d" % int(side)
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(-0.85, 4.65, 0)
|
||||
omni.omni_range = 12.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1, 0.93, 0.75)
|
||||
holder.add_child(omni)
|
||||
_lamp_omni.append(omni)
|
||||
_collect_emissive(holder)
|
||||
# Blender library street trees replace the cone trees. Flank the walkway and
|
||||
# keep the sight line to the door clear.
|
||||
if _feature("street_trees"):
|
||||
|
||||
Reference in new issue
Block a user