Rebuild the Joke Shop as the Sunset Strip block
- 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:
1 parent
74dc1fdc1d
commit
37f925c7ad
39 files changed
+1015
-96
No files matched your search
@@ -638,6 +638,12 @@ func show_wand(controller: Object, result: Dictionary) -> void:
|
||||
lines.append(Strings.lookup("hud.wand_false"))
|
||||
else:
|
||||
lines.append(Strings.lookup("hud.wand_clear"))
|
||||
if profile != null and not profile.carried_items.is_empty():
|
||||
var items: Array[String] = []
|
||||
for carried in profile.carried_items:
|
||||
items.append(str(carried).capitalize())
|
||||
lines.append(Strings.fmt("hud.wand_items", [", ".join(items)]))
|
||||
wand_meta.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
wand_meta.text = "\n".join(lines)
|
||||
wand_panel.visible = true
|
||||
if _wand_art_layer:
|
||||
|
||||
+623
-17
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user