Rebuild the Joke Shop as the Sunset Strip block
Full build / Import + headless tests (push) Successful in 37s
Full build / Full desktop build + optional release (push) Skipped

- Replace the alley with the 27 m Comedy Store-inspired hall, signature canopy, circular blade sign, red curtain bays, west entrance, low-rise neighbours, dry hillside and opposite frontage.
- Add day/night/across-street concept frames and real-site mapping notes.
- Expose the Joke Shop day stage in the realism captures and refresh the gallery shots.
- Render multiline HUD/wand strings correctly and list carried items in the wand panel.
- Add the Joke Shop layout/budget tests and update docs, roadmap and changelog.
This commit is contained in:
Gig Line Dev committed 2026-11-02 14:10:07 +01:00
1 parent 74dc1fdc1d
commit 37f925c7ad
39 files changed
+1015 -96

No files matched your search

+623 -17
View File
@@ -123,23 +123,47 @@ const VENUE_SPECS := {
},
"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_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],
"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),
## A fictional stand-in for The Comedy Store at 8433 Sunset Blvd. The
## playable frontage is 27 m: the real parcel's 47 m footprint includes
## the rear rooms and the hillside lot, which are not walkable here.
"architecture": "comedy_store",
"facade_width": 27.0, "facade_height": 8.4, "facade_depth": 12.0,
"facade_center_z": -8.0, "facade_texture": T_CONCRETE,
"facade_tint": Color(0.13, 0.13, 0.14), "facade_role": "concrete",
"window_columns": [], "window_rows": [], "pilaster_columns": [],
"stair_width": 4.4, "stair_count": 1, "stair_tread": 1.2, "stair_height": 0.14,
"sidewalk_width": 38.0, "sidewalk_depth": 14.0, "curb_z": 11.9,
"street_width": 90.0, "street_depth": 24.0, "street_front_z": 12.0,
"nav_min": Vector2(-15.0, -0.6), "nav_max": Vector2(15.0, 14.0),
"guard_spawn": Vector3(-4.8, 0.1, 1.4),
"sedan_position": Vector3(9.4, 0.1, 15.0), "sedan_rotation": 90.0,
"street_lamp_positions": [
Vector3(-13.4, 0.18, 10.8), Vector3(13.4, 0.18, 10.8),
],
"features": {
"marquee": false, "skyline": false, "handrail": false, "sedan": false,
"wall_lanterns": true, "palms": false, "street_trees": false,
"street_lamps": false, "crosswalk": false, "bulb_canopy": false,
"marquee": false, "skyline": false, "handrail": false, "sedan": true,
"wall_lanterns": false, "palms": false, "street_trees": false,
"street_lamps": true, "crosswalk": true, "bulb_canopy": false,
"comedy_store": true, "sunset_hillside": true, "sunset_neighbours": true,
"sunset_across_street": true, "hollywood_lane_markings": true,
},
"prop_placements": [],
"prop_placements": [
{"zone": "entrance", "prop": "security_podium", "x": 6.8, "z": 5.8, "rot": -15.0},
{"zone": "entrance", "prop": "crowd_barrier", "x": 2.2, "z": 9.0},
{"zone": "entrance", "prop": "stanchion", "x": -3.0, "z": 9.4},
{"zone": "entrance", "prop": "stanchion", "x": 3.0, "z": 9.4},
{"zone": "mid_block", "prop": "palm_tree", "x": -13.0, "z": 8.0, "rot": 8.0},
{"zone": "mid_block", "prop": "palm_tree", "x": 13.0, "z": 8.0, "rot": -8.0},
{"zone": "mid_block", "prop": "trash_bin", "x": -11.5, "z": 7.2, "accessory": "bag"},
{"zone": "mid_block", "prop": "trash_bin", "x": 10.5, "z": 7.2},
{"zone": "mid_block", "prop": "planter_box", "x": -12.0, "z": 9.7},
{"zone": "mid_block", "prop": "round_planter", "x": 8.5, "z": 7.3},
{"zone": "curb", "prop": "a_frame_sign", "x": 5.0, "z": 7.8, "rot": 18.0},
{"zone": "curb", "prop": "newspaper_box", "x": 11.0, "z": 10.6, "rot": -12.0},
{"zone": "curb", "prop": "newspaper_box", "x": 11.7, "z": 10.6, "rot": -12.0},
{"zone": "curb", "prop": "fire_hydrant", "x": -13.2, "z": 10.9,
"accessory": "cone"},
],
},
"tv_studio": {
"display_name": "Late Night TV Studio",
@@ -189,8 +213,12 @@ func _feature(name: String) -> bool:
func build() -> void:
_build_ground()
if _feature("sunset_hillside"):
_build_sunset_hillside()
if _feature("masonic_facade"):
_build_masonic_building()
elif _feature("comedy_store"):
_build_comedy_store_building()
else:
_build_building()
if _feature("skyline"):
@@ -199,7 +227,8 @@ func build() -> void:
_build_marquee()
if _feature("bulb_canopy"):
_build_bulb_canopy()
_build_steps()
if not _feature("comedy_store"):
_build_steps()
_build_sidewalk()
if _feature("walk_of_fame"):
_build_walk_of_fame()
@@ -207,6 +236,10 @@ func build() -> void:
_build_neighbour_blocks()
if _feature("opposite_block"):
_build_opposite_block()
if _feature("sunset_neighbours"):
_build_sunset_neighbours()
if _feature("sunset_across_street"):
_build_sunset_across_street()
_build_street()
if _feature("hollywood_lane_markings"):
_build_hollywood_lane_markings()
@@ -215,7 +248,7 @@ func build() -> void:
_build_street_furniture()
_build_street_detail()
_build_library_dressing()
if venue_id == "joke_shop":
if venue_id == "joke_shop" and not _feature("comedy_store"):
_build_alley_dressing()
_build_guard_spawn()
_build_navmesh()
@@ -311,6 +344,557 @@ func _build_building() -> void:
building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, h * 0.5, cz)))
root.add_child(building)
## The Joke Shop is a fictional stand-in for The Comedy Store on the Sunset
## Strip: a low black hall with a deep signature-covered canopy, a circular
## blade sign, red velvet curtains and a dry hillside of houses behind it.
## The real venue's plan is an irregular L across several parcels; the game
## keeps the 27 m street frontage and compresses the rear rooms.
func _build_comedy_store_building() -> void:
var building := StaticBody3D.new()
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
var black := Color(0.105, 0.10, 0.105)
var black_deep := Color(0.045, 0.045, 0.05)
var signature := Color(0.58, 0.56, 0.52)
var red := Color(0.42, 0.035, 0.045)
# Main hall. The black stucco is the real building's dominant surface; the
# concrete texture keeps it matte instead of turning it into glossy paint.
_add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), black,
false, T_CONCRETE, Vector2(w * 0.42, h * 0.42), "concrete")
# Low plinth and a thin parapet cap so the roofline reads against the hill.
_add_box(building, Vector3(w, 0.30, 0.34), Vector3(0, 0.15, front + 0.05),
Color(0.07, 0.07, 0.075), true, null, Vector2.ONE, "stone")
_add_box(building, Vector3(w, 0.42, 0.48), Vector3(0, h + 0.10, front + 0.06),
Color(0.08, 0.08, 0.085), true, null, Vector2.ONE, "painted_metal")
# Roof plant behind the blade sign: the real hall has a taller rear volume.
_add_box(building, Vector3(12.0, 1.5, 4.6), Vector3(5.5, h + 0.75, cz - 2.6),
black, false, null, Vector2.ONE, "concrete")
# Deep sloping canopy over the whole frontage. The outer edge drops about a
# metre, as in the reference photographs.
var canopy := _add_box(building, Vector3(w - 0.6, 0.22, 4.9),
Vector3(0, 5.65, front + 2.35), black, true, T_CONCRETE,
Vector2(w * 0.35, 1.5), "concrete")
canopy.name = "SignatureCanopy"
canopy.rotation_degrees.x = 11.0
# Outer fascia: the signature band that faces the street.
_add_box(building, Vector3(w - 0.2, 0.92, 0.20),
Vector3(0, 4.98, front + 4.76), black_deep, true, null, Vector2.ONE, "concrete")
for side in [-1.0, 1.0]:
_add_box(building, Vector3(0.20, 0.92, 4.9),
Vector3(side * (w * 0.5 - 0.22), 4.98, front + 2.35),
black_deep, true, null, Vector2.ONE, "concrete")
# Signature scribbles on the fascia and the wall. Keeping them as a few
# texture-free labels reads as handwriting at gameplay distance and costs
# no extra draw calls compared with stamping dozens of decals.
var scribbles := "Richard Pryor Robin Williams George Carlin\n" \
+ "Dave Chappelle Bill Burr Ali Wong Marc Maron\n" \
+ "Joan Rivers Sam Kinison Andrew Dice Clay Mitzi Shore"
for side in [-1.0, 1.0]:
var names := Label3D.new()
names.name = "CanopySignatures_%s" % ("west" if side < 0.0 else "east")
names.text = scribbles
names.position = Vector3(side * 6.6, 4.98, front + 4.88)
names.font_size = 22
names.pixel_size = 0.0043
names.modulate = signature
names.outline_size = 0
names.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
building.add_child(names)
var wall_names := Label3D.new()
wall_names.name = "WallSignatures"
wall_names.text = "The Comedy Store family\nEvery name written in chalk\nand paint since 1972"
wall_names.position = Vector3(-3.8, 3.55, front + 0.18)
wall_names.font_size = 20
wall_names.pixel_size = 0.0042
wall_names.modulate = Color(0.42, 0.40, 0.38)
wall_names.outline_size = 0
wall_names.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
building.add_child(wall_names)
# Front-of-house name on the fascia, matching the concept's white lettering.
var fascia_name := Label3D.new()
fascia_name.name = "FasciaName"
fascia_name.text = "THE JOKE SHOP"
fascia_name.position = Vector3(0.0, 4.96, front + 4.90)
fascia_name.font_size = 64
fascia_name.pixel_size = 0.0042
fascia_name.modulate = Color(0.92, 0.90, 0.84)
fascia_name.outline_size = 0
building.add_child(fascia_name)
# Warm bulbs under the outer fascia. They are off in daylight and lit by
# `set_night_mode` through `_marquee_emissive`.
for index in 11:
var bulb := MeshInstance3D.new()
var bulb_mesh := SphereMesh.new()
bulb_mesh.radius = 0.075
bulb_mesh.height = 0.15
bulb_mesh.radial_segments = 8
bulb_mesh.rings = 4
bulb.mesh = bulb_mesh
bulb.position = Vector3(-11.0 + float(index) * 2.2, 5.48, front + 4.78)
var bulb_mat := Materials.emissive(Color(1.0, 0.83, 0.52), 0.2)
bulb.material_override = bulb_mat
_marquee_emissive.append(bulb_mat)
building.add_child(bulb)
# Recessed entrance at the west end, with a low step and a red velvet door
# curtain. The real door sits under the same deep canopy, not in the centre.
var door_x := -8.8
_add_box(building, Vector3(4.2, 3.8, 0.18), Vector3(door_x, 1.9, front + 0.06),
black_deep, false, null, Vector2.ONE, "concrete")
_add_box(building, Vector3(2.45, 3.25, 0.12), Vector3(door_x, 1.63, front + 0.18),
red, true, null, Vector2.ONE, "cloth")
_add_box(building, Vector3(2.62, 0.16, 0.18), Vector3(door_x, 2.35, front + 0.26),
Color(0.24, 0.02, 0.03), false, null, Vector2.ONE, "cloth")
for side in [-1.0, 1.0]:
_add_box(building, Vector3(0.16, 3.5, 0.30),
Vector3(door_x + side * 1.32, 1.75, front + 0.14),
Color(0.19, 0.18, 0.17), true, null, Vector2.ONE, "painted_metal")
_add_box(building, Vector3(3.0, 0.20, 0.32), Vector3(door_x, 3.55, front + 0.14),
Color(0.19, 0.18, 0.17), true, null, Vector2.ONE, "painted_metal")
for side in [-0.42, 0.42]:
_add_box(building, Vector3(0.06, 0.44, 0.06),
Vector3(door_x + side, 1.45, front + 0.29),
Color(0.75, 0.70, 0.55), false, null, Vector2.ONE, "metal")
door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24),
Vector3(door_x + 1.85, 1.70, front + 0.22), Color(0.1, 0.9, 0.3))
door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3))
# Step and collision in front of the curtain.
var step := _add_box(building, Vector3(4.4, 0.14, 1.25),
Vector3(door_x, 0.07, front + 0.92), Color(0.24, 0.23, 0.21),
true, T_CONCRETE, Vector2(2.0, 0.6), "concrete")
step.name = "EntranceStep"
var step_body := StaticBody3D.new()
step_body.name = "EntranceStepCollision"
var step_shape := BoxShape3D.new()
step_shape.size = Vector3(4.4, 0.14, 1.25)
var step_owner := step_body.create_shape_owner(step_body)
step_body.shape_owner_add_shape(step_owner, step_shape)
step_body.shape_owner_set_transform(step_owner,
Transform3D(Basis(), Vector3(door_x, 0.07, front + 0.92)))
root.add_child(step_body)
# Red curtains hang between the entrance and the signature wall. Three
# narrow folds and a tieback keep them from reading as flat red slabs.
for x in [-3.2, 1.9, 7.0, 11.2]:
for fold in [-0.27, 0.0, 0.27]:
var fold_color := red.lightened(0.10 if fold > 0.0 else 0.0)
_add_box(building, Vector3(0.24, 3.10, 0.18),
Vector3(x + fold, 2.70, front + 4.56 + absf(fold) * 0.08),
fold_color, true, null, Vector2.ONE, "cloth")
_add_box(building, Vector3(0.96, 0.16, 0.22),
Vector3(x, 2.13, front + 4.60), Color(0.24, 0.02, 0.03),
false, null, Vector2.ONE, "cloth")
_add_box(building, Vector3(1.06, 0.52, 0.20),
Vector3(x, 1.34, front + 4.54), Color(0.34, 0.025, 0.035),
false, null, Vector2.ONE, "cloth")
# Small warm sconces along the black wall. Their bulbs are emissive and each
# gets a low-energy practical so the facade is not a black hole after dark.
for x in [-12.4, -6.2, 3.0, 9.4]:
_add_box(building, Vector3(0.34, 0.62, 0.20), Vector3(x, 3.55, front + 0.18),
Color(0.17, 0.16, 0.14), false, null, Vector2.ONE, "painted_metal")
var sconce := _add_box(building, Vector3(0.22, 0.34, 0.14),
Vector3(x, 3.55, front + 0.30), Color(1.0, 0.82, 0.52),
false, null, Vector2.ONE, "metal")
var sconce_mat := Materials.emissive(Color(1.0, 0.78, 0.45), 0.2)
sconce.material_override = sconce_mat
_marquee_emissive.append(sconce_mat)
var sconce_light := OmniLight3D.new()
sconce_light.name = "JokeSconce_%d" % int(x)
sconce_light.position = Vector3(x, 3.35, front + 0.75)
sconce_light.omni_range = 5.0
sconce_light.omni_attenuation = 1.0
sconce_light.light_energy = 0.0
sconce_light.light_color = Color(1.0, 0.73, 0.42)
building.add_child(sconce_light)
_accent_omni.append(sconce_light)
# Small open sign above the entrance.
var open_sign := Label3D.new()
open_sign.name = "EntranceCopy"
open_sign.text = "OPEN FOR COMEDY\nFOOD AND DRINKS"
open_sign.position = Vector3(door_x, 4.15, front + 0.22)
open_sign.font_size = 28
open_sign.pixel_size = 0.0035
open_sign.modulate = Color(0.82, 0.80, 0.72)
open_sign.outline_size = 0
open_sign.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
building.add_child(open_sign)
# Circular blade sign on the roof. It is deliberately the same silhouette as
# the concept art: a white disc, black rim and black three-line wordmark.
var sign_x := -10.8
# The pole stands on the roof just behind its front edge; the disc projects
# forward but is not floating over the canopy.
var sign_z := front + 0.35
var pole := MeshInstance3D.new()
var pole_mesh := CylinderMesh.new()
pole_mesh.top_radius = 0.10
pole_mesh.bottom_radius = 0.13
pole_mesh.height = 2.7
pole_mesh.radial_segments = 12
pole.mesh = pole_mesh
pole.position = Vector3(sign_x, h + 1.2, sign_z)
pole.material_override = Materials.flat(Color(0.12, 0.12, 0.13), "metal")
building.add_child(pole)
var rim := MeshInstance3D.new()
var rim_mesh := CylinderMesh.new()
rim_mesh.top_radius = 2.12
rim_mesh.bottom_radius = 2.12
rim_mesh.height = 0.16
rim_mesh.radial_segments = 48
rim.mesh = rim_mesh
rim.position = Vector3(sign_x, h + 2.55, sign_z - 0.03)
rim.rotation_degrees.x = 90.0
rim.material_override = Materials.flat(Color(0.06, 0.06, 0.07), "painted_metal")
building.add_child(rim)
var disc := MeshInstance3D.new()
var disc_mesh := CylinderMesh.new()
disc_mesh.top_radius = 1.96
disc_mesh.bottom_radius = 1.96
disc_mesh.height = 0.10
disc_mesh.radial_segments = 48
disc.mesh = disc_mesh
disc.position = Vector3(sign_x, h + 2.55, sign_z + 0.05)
disc.rotation_degrees.x = 90.0
# Slight self-illumination so the blade reads as a lit sign after dark; the
# emission is low in daylight and switched with the other marquee materials.
var disc_mat := Materials.emissive(Color(0.92, 0.90, 0.83), 0.22)
disc.material_override = disc_mat
building.add_child(disc)
var sign_label := Label3D.new()
sign_label.name = "ComedySignLabel"
sign_label.text = "THE\nJOKE\nSHOP"
sign_label.position = Vector3(sign_x, h + 2.55, sign_z + 0.12)
sign_label.font_size = 150
sign_label.pixel_size = 0.0048
sign_label.modulate = Color(0.06, 0.06, 0.07)
sign_label.outline_size = 0
sign_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sign_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
building.add_child(sign_label)
# Night practicals: one light on the blade sign, three under the canopy and
# one at the entrance. Their energies are set in `set_night_mode`.
for data in [
["JokeSign", Vector3(sign_x, h + 2.5, sign_z + 1.2), 9.0, Color(1.0, 0.94, 0.78)],
["JokeCanopy_west", Vector3(-7.5, 4.55, front + 2.6), 8.0, Color(1.0, 0.78, 0.48)],
["JokeCanopy_mid", Vector3(0.0, 4.55, front + 2.6), 8.0, Color(1.0, 0.78, 0.48)],
["JokeCanopy_east", Vector3(7.5, 4.55, front + 2.6), 8.0, Color(1.0, 0.78, 0.48)],
["JokeEntrance", Vector3(door_x, 3.35, front + 1.1), 7.0, Color(1.0, 0.72, 0.42)],
["JokeGround_west", Vector3(-7.0, 0.7, front + 6.0), 10.0, Color(1.0, 0.68, 0.38)],
["JokeGround_mid", Vector3(0.0, 0.7, front + 6.0), 10.0, Color(1.0, 0.68, 0.38)],
["JokeGround_east", Vector3(7.0, 0.7, front + 6.0), 10.0, Color(1.0, 0.68, 0.38)],
]:
var omni := OmniLight3D.new()
omni.name = str(data[0])
omni.position = data[1]
omni.omni_range = float(data[2])
omni.omni_attenuation = 1.0
omni.light_energy = 0.0
omni.light_color = data[3]
building.add_child(omni)
_accent_omni.append(omni)
# Solid collision volume for the hall.
var col := BoxShape3D.new()
col.size = Vector3(w, h, d)
var so := building.create_shape_owner(building)
building.shape_owner_add_shape(so, col)
building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, h * 0.5, cz)))
root.add_child(building)
## A window with a dark reveal, a pane that lights at night and a sill. Used by
## the Sunset neighbours and the hillside houses, which have no bespoke facade
## geometry of their own.
func _add_sunset_window(parent: Node3D, center: Vector3, size: Vector2) -> void:
_add_box(parent, Vector3(size.x + 0.24, size.y + 0.24, 0.10),
center, Color(0.16, 0.15, 0.14), false, null, Vector2.ONE, "stone")
var pane := _add_box(parent, Vector3(size.x, size.y, 0.08),
center + Vector3(0.0, 0.0, 0.09), Color(0.1, 0.12, 0.16),
false, null, Vector2.ONE, "glass")
var pane_mat := Materials.window_pane()
pane.material_override = pane_mat
_window_mats.append(pane_mat)
_add_box(parent, Vector3(size.x + 0.36, 0.10, 0.20),
center + Vector3(0.0, -size.y * 0.5 - 0.10, 0.08),
Color(0.52, 0.49, 0.44), false, null, Vector2.ONE, "stone")
## Sunset Strip neighbours: a cream Mediterranean block west and a white
## commercial block east. Only their street-facing volumes are modelled; the
## playable map never leaves the sidewalk in front of the club.
func _build_sunset_neighbours() -> void:
var neighbours := Node3D.new()
neighbours.name = "SunsetNeighbours"
root.add_child(neighbours)
# West: 15 m cream stucco block with a terracotta parapet and small balconies.
var west := StaticBody3D.new()
west.name = "WestMediterraneanBlock"
var west_w := 15.0
var west_h := 11.0
var west_front := -3.2
_add_box(west, Vector3(west_w, west_h, 12.0),
Vector3(-21.0, west_h * 0.5, -9.2), Color(0.80, 0.73, 0.60),
false, T_CONCRETE, Vector2(6, 5), "concrete")
_add_box(west, Vector3(west_w + 0.4, 1.0, 12.4),
Vector3(-21.0, west_h + 0.35, -9.2), Color(0.42, 0.18, 0.11),
true, null, Vector2.ONE, "stone")
_add_box(west, Vector3(west_w, 0.26, 0.55),
Vector3(-21.0, 3.35, west_front + 0.15), Color(0.44, 0.40, 0.33),
true, null, Vector2.ONE, "stone")
for row in 3:
var y := 2.2 + float(row) * 2.9
for column in 4:
var x := -26.6 + float(column) * 3.6
_add_sunset_window(west, Vector3(x, y, west_front + 0.10),
Vector2(1.5, 1.9) if row > 0 else Vector2(1.8, 2.3))
# Ground-floor shopfront and a red canvas awning.
_add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08),
Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass")
_add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65),
Color(0.48, 0.10, 0.08), true, T_AWNING, Vector2(3, 0.5), "awning")
# Balcony rails on the upper floor.
for x in [-24.8, -21.2, -17.6]:
_add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55),
Color(0.31, 0.29, 0.26), true, null, Vector2.ONE, "painted_metal")
_add_box(west, Vector3(2.4, 0.62, 0.05), Vector3(x, 5.9, west_front + 0.95),
Color(0.24, 0.23, 0.21), false, null, Vector2.ONE, "painted_metal")
var west_col := BoxShape3D.new()
west_col.size = Vector3(west_w, west_h, 12.0)
var west_owner := west.create_shape_owner(west)
west.shape_owner_add_shape(west_owner, west_col)
west.shape_owner_set_transform(west_owner,
Transform3D(Basis(), Vector3(-21.0, west_h * 0.5, -9.2)))
neighbours.add_child(west)
# East: 15 m white block with a taller parapet, ribbon windows and a lit
# ground-floor lobby.
var east := StaticBody3D.new()
east.name = "EastCommercialBlock"
var east_w := 15.0
var east_h := 12.4
var east_front := -3.0
_add_box(east, Vector3(east_w, east_h, 12.0),
Vector3(21.0, east_h * 0.5, -9.0), Color(0.84, 0.83, 0.79),
false, T_CONCRETE, Vector2(6, 6), "concrete")
_add_box(east, Vector3(east_w + 0.3, 0.55, 12.2),
Vector3(21.0, east_h + 0.18, -9.0), Color(0.57, 0.55, 0.51),
true, null, Vector2.ONE, "stone")
for row in 3:
var y := 2.8 + float(row) * 3.2
for column in 5:
var x := 14.6 + float(column) * 3.2
_add_sunset_window(east, Vector3(x, y, east_front + 0.10),
Vector2(2.1, 1.35))
_add_box(east, Vector3(10.0, 3.0, 0.12), Vector3(21.0, 1.65, east_front + 0.08),
Color(0.08, 0.11, 0.14), false, null, Vector2.ONE, "glass")
_add_box(east, Vector3(10.4, 0.18, 1.1), Vector3(21.0, 3.4, east_front + 0.55),
Color(0.11, 0.11, 0.12), true, null, Vector2.ONE, "painted_metal")
var east_col := BoxShape3D.new()
east_col.size = Vector3(east_w, east_h, 12.0)
var east_owner := east.create_shape_owner(east)
east.shape_owner_add_shape(east_owner, east_col)
east.shape_owner_set_transform(east_owner,
Transform3D(Basis(), Vector3(21.0, east_h * 0.5, -9.0)))
neighbours.add_child(east)
## Dry Hollywood Hills slope immediately behind the club, with a few stucco
## houses and palms. The plane starts at the rear wall so it cannot poke through
## the playable frontage, and the houses sit on the computed slope height.
func _build_sunset_hillside() -> void:
var hillside := Node3D.new()
hillside.name = "SunsetHillside"
root.add_child(hillside)
# Hand-built 45-degree ramp instead of a rotated PlaneMesh: the front edge
# meets the building's rear wall and the face points squarely at the street,
# so the slope reads as a hill from ground level instead of disappearing
# edge-on. The rear ridge is high enough to sit above the roofline.
var plane := MeshInstance3D.new()
plane.name = "DryHillside"
var verts := PackedVector3Array([
Vector3(-55.0, 0.0, -14.0), Vector3(55.0, 0.0, -14.0),
Vector3(55.0, 28.0, -30.0), Vector3(-55.0, 28.0, -30.0),
])
var normals := PackedVector3Array()
var slope_normal := Vector3(0.0, 1.0, 1.0).normalized()
for i in 4:
normals.append(slope_normal)
var uvs := PackedVector2Array([
Vector2(0.0, 0.0), Vector2(14.0, 0.0),
Vector2(14.0, 7.0), Vector2(0.0, 7.0),
])
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = verts
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 2, 1, 0, 3, 2])
var plane_mesh := ArrayMesh.new()
plane_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
plane.mesh = plane_mesh
# The hillside is background scenery. Like the skyline panorama it is
# unshaded and fog-free: scene fog at 50 m washed a lit slope into the sky,
# which read as a hole behind the roofline.
var hill_shader := Shader.new()
hill_shader.code = """
shader_type spatial;
render_mode unshaded, cull_disabled, fog_disabled;
uniform sampler2D hill_texture : source_color, repeat_enable, filter_linear_mipmap;
uniform vec4 tint : source_color = vec4(1.0);
void fragment() {
ALBEDO = texture(hill_texture, UV * vec2(14.0, 7.0)).rgb * tint.rgb;
}
"""
var hill_mat := ShaderMaterial.new()
hill_mat.shader = hill_shader
hill_mat.set_shader_parameter("hill_texture", T_CONCRETE)
hill_mat.set_shader_parameter("tint", Color(0.48, 0.42, 0.29))
_hill_mat = hill_mat
plane.material_override = hill_mat
plane.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
hillside.add_child(plane)
# A handful of low-poly shrubs break up the flat slope.
for index in 14:
var x := -40.0 + float(index) * 6.1
var z := -17.0 - float((index * 7) % 13)
var height := _sunset_hill_height(z)
var shrub := MeshInstance3D.new()
var shrub_mesh := SphereMesh.new()
shrub_mesh.radius = 0.9 + float(index % 3) * 0.25
shrub_mesh.height = 1.2 + float(index % 3) * 0.3
shrub_mesh.radial_segments = 7
shrub_mesh.rings = 4
shrub.mesh = shrub_mesh
shrub.position = Vector3(x, height + 0.5, z)
shrub.scale = Vector3(1.4, 0.7, 1.0)
shrub.material_override = Materials.flat(Color(0.25, 0.26, 0.15), "concrete")
shrub.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
hillside.add_child(shrub)
# Houses at three heights along the slope.
var houses := [
{"x": -23.0, "z": -18.0, "w": 8.0, "h": 4.8, "d": 7.0},
{"x": -5.0, "z": -22.0, "w": 10.0, "h": 5.2, "d": 8.0},
{"x": 15.0, "z": -20.0, "w": 7.0, "h": 4.2, "d": 6.0},
{"x": 31.0, "z": -27.0, "w": 9.0, "h": 5.0, "d": 7.0},
]
for house in houses:
var hx := float(house["x"])
var hz := float(house["z"])
var hh := float(house["h"])
var base := _sunset_hill_height(hz)
_add_box(hillside, Vector3(float(house["w"]), hh, float(house["d"])),
Vector3(hx, base + hh * 0.5, hz), Color(0.78, 0.73, 0.63),
false, T_CONCRETE, Vector2(3, 2.5), "concrete")
_add_box(hillside, Vector3(float(house["w"]) + 0.4, 0.35, float(house["d"]) + 0.4),
Vector3(hx, base + hh + 0.12, hz), Color(0.36, 0.20, 0.13),
true, null, Vector2.ONE, "stone")
# Front windows face the street (positive Z side of the house).
for offset in [-1.7, 1.7]:
_add_sunset_window(hillside,
Vector3(hx + offset, base + hh * 0.58, hz + float(house["d"]) * 0.5 + 0.08),
Vector2(1.35, 1.1))
# A porch bulb and small warm practical: the reference night shows a few
# scattered house lights, not a uniformly black hill.
var porch := _add_box(hillside, Vector3(0.22, 0.28, 0.14),
Vector3(hx, base + 1.45, hz + float(house["d"]) * 0.5 + 0.12),
Color(1.0, 0.82, 0.55), false, null, Vector2.ONE, "metal")
var porch_mat := Materials.emissive(Color(1.0, 0.78, 0.46), 0.2)
porch.material_override = porch_mat
_marquee_emissive.append(porch_mat)
var porch_light := OmniLight3D.new()
porch_light.name = "HillPorch"
porch_light.position = Vector3(hx, base + 1.35, hz + float(house["d"]) * 0.5 + 0.8)
porch_light.omni_range = 5.0
porch_light.omni_attenuation = 1.0
porch_light.light_energy = 0.0
porch_light.light_color = Color(1.0, 0.72, 0.42)
hillside.add_child(porch_light)
_accent_omni.append(porch_light)
# Tall palms on the slope; the library palm gives the same silhouette as the
# street trees without another bespoke asset.
for data in [
[-32.0, -17.0, 0.78], [9.0, -20.0, 0.9], [36.0, -24.0, 0.72],
]:
var px := float(data[0])
var pz := float(data[1])
var palm := _add_prop_model(hillside, "palm_tree",
Vector3(px, _sunset_hill_height(pz) - 0.1, pz), 20.0)
palm.scale = Vector3.ONE * float(data[2])
palm.name = "HillsidePalm_%d" % int(px)
func _sunset_hill_height(z: float) -> float:
return 0.24 + maxf(0.0, -14.0 - z) * tan(deg_to_rad(60.0))
## The opposite side of Sunset: a low retail row, a rooftop billboard and palms.
## It is background context; the navigable area stops at the near kerb.
func _build_sunset_across_street() -> void:
var across := Node3D.new()
across.name = "SunsetAcrossStreet"
root.add_child(across)
_add_box(across, Vector3(90.0, 0.18, 6.0), Vector3(0.0, 0.09, 39.0),
Color(0.53, 0.52, 0.50), false, T_CONCRETE, Vector2(26, 2), "concrete")
_add_box(across, Vector3(90.0, 0.22, 0.5), Vector3(0.0, 0.11, 36.0),
Color(0.60, 0.59, 0.56), false, null, Vector2.ONE, "stone")
var buildings := [
{"x": -34.0, "w": 22.0, "h": 9.5, "d": 10.0, "color": Color(0.66, 0.58, 0.47)},
{"x": -10.0, "w": 24.0, "h": 12.5, "d": 10.0, "color": Color(0.74, 0.72, 0.66)},
{"x": 14.0, "w": 20.0, "h": 8.5, "d": 10.0, "color": Color(0.57, 0.50, 0.44)},
{"x": 34.0, "w": 16.0, "h": 10.5, "d": 10.0, "color": Color(0.70, 0.66, 0.59)},
]
for index in buildings.size():
var entry: Dictionary = buildings[index]
var bx := float(entry["x"])
var bw := float(entry["w"])
var bh := float(entry["h"])
var bz := 47.0
_add_box(across, Vector3(bw, bh, float(entry["d"])),
Vector3(bx, bh * 0.5, bz), entry["color"], false, T_CONCRETE,
Vector2(8, 4), "concrete")
_add_box(across, Vector3(bw + 0.35, 0.45, 10.3),
Vector3(bx, bh + 0.18, bz), Color(0.34, 0.29, 0.23),
true, null, Vector2.ONE, "stone")
for row in 2:
var y := 3.3 + float(row) * 3.1
for column in int(bw / 3.3):
var x := bx - bw * 0.5 + 1.9 + float(column) * 3.3
_add_sunset_window(across, Vector3(x, y, 41.94), Vector2(1.8, 1.4))
# Ground-floor shop glass and a projecting awning.
_add_box(across, Vector3(bw - 1.2, 2.5, 0.12),
Vector3(bx, 1.55, 41.92), Color(0.08, 0.10, 0.13),
false, null, Vector2.ONE, "glass")
_add_box(across, Vector3(bw - 0.8, 0.16, 1.1),
Vector3(bx, 3.05, 42.45), Color(0.26, 0.23, 0.22),
true, null, Vector2.ONE, "awning")
# Rooftop billboard on the central block.
_add_box(across, Vector3(14.0, 6.0, 0.30), Vector3(-10.0, 15.5, 42.3),
Color(0.13, 0.13, 0.14), false, null, Vector2.ONE, "painted_metal")
for side in [-5.0, 5.0]:
_add_box(across, Vector3(0.22, 7.0, 0.22),
Vector3(-10.0 + side, 12.0, 42.4), Color(0.22, 0.21, 0.20),
false, null, Vector2.ONE, "metal")
var billboard := Label3D.new()
billboard.name = "SunsetBillboard"
billboard.text = "SUNSET\nLIVE"
billboard.position = Vector3(-10.0, 15.5, 42.12)
billboard.rotation.y = PI
billboard.font_size = 120
billboard.pixel_size = 0.005
billboard.modulate = Color(0.86, 0.31, 0.45)
billboard.outline_size = 0
billboard.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
across.add_child(billboard)
var club_sign := Label3D.new()
club_sign.name = "AcrossNeon"
club_sign.text = "COMEDY"
club_sign.position = Vector3(14.0, 7.0, 41.82)
club_sign.rotation.y = PI
club_sign.font_size = 110
club_sign.pixel_size = 0.0045
club_sign.modulate = Color(0.95, 0.38, 0.58)
club_sign.outline_size = 12
club_sign.outline_modulate = Color(0.22, 0.03, 0.10)
across.add_child(club_sign)
for side in [-1.0, 1.0]:
var palm := _add_prop_model(across, "palm_tree",
Vector3(side * 26.0, 0.18, 37.8), 90.0 if side < 0.0 else -90.0)
palm.name = "AcrossPalm_%s" % ("west" if side < 0.0 else "east")
## Regular window bands above the entrance. Panes are dark glass by day and
## light up after dark, which gives the facade depth at night.
## Real-world Hollywood Boulevard landmark: the El Capitan Entertainment
@@ -1060,6 +1644,7 @@ void fragment() {
"""
var _skyline_mat: ShaderMaterial = null
var _hill_mat: ShaderMaterial = null
var _street_mat: StandardMaterial3D = null
var _phase: int = Rules.Phase.EARLY
@@ -1680,6 +2265,15 @@ func set_night_mode(night: bool) -> void:
if omni.name == "LanternGlow":
lamp_energy = 3.0 if venue_id == "joke_shop" else 1.3
omni.light_energy = (lamp_energy if night else 0.0)
if _hill_mat != null:
# The unshaded hillside has no real lighting, so its tint follows the
# phase. Night keeps it as a dark silhouette behind the lit houses.
var hill_tint := Color(0.48, 0.42, 0.29)
if _phase == Rules.Phase.EVENING:
hill_tint = Color(0.34, 0.29, 0.25)
elif _phase == Rules.Phase.LATE:
hill_tint = Color(0.22, 0.21, 0.23)
_hill_mat.set_shader_parameter("tint", hill_tint)
for omni in _accent_omni:
# Keep the facade washes soft: the reference night reads as dark stone
# with warm pools at the sconces, not a flooded yellow wall.
@@ -1690,6 +2284,18 @@ func set_night_mode(night: bool) -> void:
energy = 0.28
elif omni.name.begins_with("OvationWash"):
energy = 0.75
elif omni.name == "JokeSign":
energy = 1.25
elif omni.name.begins_with("JokeCanopy"):
energy = 2.8
elif omni.name == "JokeEntrance":
energy = 3.0
elif omni.name.begins_with("JokeSconce"):
energy = 1.8
elif omni.name.begins_with("JokeGround"):
energy = 1.35
elif omni.name == "HillPorch":
energy = 0.75
omni.light_energy = (energy if night else 0.0)
if _ovation_sign:
_ovation_sign.modulate = Color(0.90, 0.84, 0.70) if night else Color(0.16, 0.14, 0.12)