Map 1: rebuild Hollywood as the El Capitan landmark
Replace the generic boulevard facade with a stylized 6840 Hollywood Blvd block: six Ionic columns, eight stone steps, Masonic frieze/medallions, event banners, side doors, El Capitan Theatre and retail neighbours, Ovation Hollywood arcade, Walk of Fame and four-lane street markings. Add the Day and Night Hollywood venue-menu rows. Night uses a phase-time offset so a full-length shift stays in LATE while the clock starts at zero; late routine traffic keeps the street populated and the offset round-trips through checkpoint restore. Retune local-light falloff to linear attenuation, add a cool north sky fill, and use the smooth limestone facade texture. Refresh the staged gallery and map references; add geometry-budget, menu, night-offset and late-traffic tests.
This commit is contained in:
1 parent
3f335481d6
commit
74dc1fdc1d
50 files changed
+1176
-100
No files matched your search
@@ -45,9 +45,14 @@ var body_camera_on := false
|
||||
var coordinator: ShiftCoordinator = null
|
||||
var guard: GuardController = null
|
||||
var _sun: DirectionalLight3D = null
|
||||
var _fill: DirectionalLight3D = null
|
||||
var _env_node: WorldEnvironment = null
|
||||
var _sky: ProceduralSkyMaterial = null
|
||||
var _last_phase: int = -1
|
||||
## Menu-only alias for the night scenery variant; it resolves to the Hollywood
|
||||
## scenario with a LATE phase offset.
|
||||
const HOLLYWOOD_NIGHT_ID := "hollywood_building_night"
|
||||
var _start_phase_override: int = -1
|
||||
|
||||
## Per-phase lighting looks for the sun, sky, and ambient (PLAN.md: shift
|
||||
## structure — phase-based lighting). Night phases switch street lamps on.
|
||||
@@ -58,7 +63,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.45, 0.5, 0.55), "ambient": Color(0.75, 0.78, 0.85), "night": false,
|
||||
"exposure": 0.6, "glow": 0.45, "fog_color": Color(0.87, 0.83, 0.76),
|
||||
"fog_density": 0.0034, "fog_energy": 0.5, "contrast": 1.05, "saturation": 1.04,
|
||||
"ambient_energy": 0.1, "volumetric_density": 0.004, "volumetric_energy": 0.0,
|
||||
"ambient_energy": 0.1, "fill_energy": 0.22, "volumetric_density": 0.004, "volumetric_energy": 0.0,
|
||||
},
|
||||
Rules.Phase.MIDDAY: {
|
||||
"sun_energy": 1.0, "sun_angle": -62.0, "sun_yaw": -30.0, "sun_color": Color(1.0, 0.99, 0.96),
|
||||
@@ -66,7 +71,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.5, 0.54, 0.58), "ambient": Color(0.85, 0.88, 0.95), "night": false,
|
||||
"exposure": 0.6, "glow": 0.4, "fog_color": Color(0.88, 0.87, 0.83),
|
||||
"fog_density": 0.0028, "fog_energy": 0.5, "contrast": 1.06, "saturation": 1.04,
|
||||
"ambient_energy": 0.28, "volumetric_density": 0.003, "volumetric_energy": 0.0,
|
||||
"ambient_energy": 0.28, "fill_energy": 0.18, "volumetric_density": 0.003, "volumetric_energy": 0.0,
|
||||
},
|
||||
# Evening is dusk, not a bright sunset: the sun is almost down and the street
|
||||
# is carried by the marquee, the lamps and the lit windows (realism pass).
|
||||
@@ -76,7 +81,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.1, 0.09, 0.12), "ambient": Color(0.2, 0.19, 0.26), "night": true,
|
||||
"exposure": 0.72, "glow": 0.8, "fog_color": Color(0.34, 0.27, 0.3),
|
||||
"fog_density": 0.0026, "fog_energy": 0.45, "contrast": 1.1, "saturation": 1.05,
|
||||
"ambient_energy": 0.12, "volumetric_density": 0.014, "volumetric_energy": 0.10,
|
||||
"ambient_energy": 0.12, "fill_energy": 0.10, "volumetric_density": 0.014, "volumetric_energy": 0.10,
|
||||
},
|
||||
Rules.Phase.LATE: {
|
||||
"sun_energy": 0.02, "sun_angle": -8.0, "sun_color": Color(0.3, 0.35, 0.5),
|
||||
@@ -84,7 +89,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.03, 0.03, 0.05), "ambient": Color(0.06, 0.07, 0.12), "night": true,
|
||||
"exposure": 0.95, "glow": 1.15, "fog_color": Color(0.08, 0.1, 0.2),
|
||||
"fog_density": 0.0028, "fog_energy": 0.45, "contrast": 1.12, "saturation": 1.0,
|
||||
"ambient_energy": 0.22, "volumetric_density": 0.020, "volumetric_energy": 0.14,
|
||||
"ambient_energy": 0.30, "fill_energy": 0.06, "volumetric_density": 0.020, "volumetric_energy": 0.14,
|
||||
},
|
||||
}
|
||||
var hud: Hud = null
|
||||
@@ -222,7 +227,7 @@ func _build_guard() -> GuardController:
|
||||
flashlight.spot_range = 30.0 if Materials.modern_light_falloff() else 20.0
|
||||
flashlight.spot_angle = 14.0
|
||||
flashlight.light_energy = 4.2
|
||||
flashlight.spot_attenuation = 0.0 if Materials.modern_light_falloff() else 0.9
|
||||
flashlight.spot_attenuation = 1.0
|
||||
flashlight.spot_angle_attenuation = 0.9
|
||||
# The volumetric shaft is a Forward+ (desktop HQ) effect. Compatibility and
|
||||
# headless builds leave the light non-volumetric, so their look is unchanged.
|
||||
@@ -265,6 +270,7 @@ func _build_scenario(id: String) -> VenueScenario:
|
||||
return ScenarioFactory.hollywood_building()
|
||||
|
||||
func switch_scenario(id: String) -> void:
|
||||
_start_phase_override = -1
|
||||
active_scenario_id = id
|
||||
selected_scenario = _build_scenario(id)
|
||||
_rebuild_world_for_venue(id)
|
||||
@@ -315,6 +321,9 @@ func _reset_shift_state() -> void:
|
||||
hud.set_body_camera(false)
|
||||
scenario_duration_s = selected_scenario.shift_duration_s
|
||||
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
||||
if _start_phase_override >= 0:
|
||||
coordinator.phase_offset_s = _phase_start_s(_start_phase_override)
|
||||
coordinator.current_phase = coordinator._phase_for_time(0.0)
|
||||
coordinator.scheduler.spawn_rate_multiplier = float(_difficulty()["spawn"])
|
||||
_connect_radio()
|
||||
_connect_crowd()
|
||||
@@ -336,6 +345,11 @@ func _start_shift() -> void:
|
||||
# opens and closes.
|
||||
_refresh_freeze()
|
||||
|
||||
func _phase_start_s(phase: int) -> float:
|
||||
if selected_scenario == null:
|
||||
return 0.0
|
||||
return float((selected_scenario.shift_phases.get(phase, {}) as Dictionary).get("start_s", 0.0))
|
||||
|
||||
func _show_start_menu() -> void:
|
||||
_shift_running = false
|
||||
if pause_overlay:
|
||||
@@ -353,6 +367,10 @@ func _on_venue_selected(scenario_id: String) -> void:
|
||||
start_menu.close()
|
||||
if not _resuming and Save:
|
||||
Save.clear_shift_state()
|
||||
_start_phase_override = -1
|
||||
if scenario_id == HOLLYWOOD_NIGHT_ID:
|
||||
_start_phase_override = Rules.Phase.LATE
|
||||
scenario_id = "hollywood_building"
|
||||
active_scenario_id = scenario_id
|
||||
_venue_index = maxi(0, VENUE_ORDER.find(scenario_id))
|
||||
selected_scenario = _build_scenario(scenario_id)
|
||||
@@ -715,6 +733,15 @@ func _ensure_lighting() -> void:
|
||||
_sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
|
||||
_sun.directional_shadow_max_distance = 60.0
|
||||
scene_root.add_child(_sun)
|
||||
# A soft sky-bounce fill from the north keeps the shaded Ovation frontage
|
||||
# readable without adding a second set of hard shadows.
|
||||
_fill = DirectionalLight3D.new()
|
||||
_fill.name = "SkyFill"
|
||||
_fill.rotation_degrees = Vector3(-24, 180, 0)
|
||||
_fill.light_energy = 0.2
|
||||
_fill.light_color = Color(0.62, 0.72, 0.9)
|
||||
_fill.shadow_enabled = false
|
||||
scene_root.add_child(_fill)
|
||||
_env_node = WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_SKY
|
||||
@@ -741,6 +768,8 @@ func _apply_phase_lighting() -> void:
|
||||
# entrance view reads as shade when the sun is off the facade.
|
||||
_sun.rotation_degrees = Vector3(float(cfg.get("sun_angle", -50.0)),
|
||||
float(cfg.get("sun_yaw", -30.0)), 0)
|
||||
if _fill:
|
||||
_fill.light_energy = float(cfg.get("fill_energy", 0.16))
|
||||
if _sky:
|
||||
_sky.sky_top_color = cfg.get("sky_top", Color.BLUE)
|
||||
_sky.sky_horizon_color = cfg.get("sky_horizon", Color.WHITE)
|
||||
@@ -1981,7 +2010,8 @@ func _physics_process(delta: float) -> void:
|
||||
hud.log("Objective: %s — keep the entrance clear and verify access." % str(
|
||||
Rules.PHASE_NAMES.get(coordinator.current_phase, "Early")))
|
||||
hud.set_shift_state(coordinator, selected_scenario)
|
||||
var preview := coordinator.scheduler.preview(coordinator.elapsed_s, coordinator.current_phase)
|
||||
var preview := coordinator.scheduler.preview(coordinator.schedule_time_s(),
|
||||
coordinator.current_phase)
|
||||
hud.refresh_debug(coordinator, npcs, preview)
|
||||
_perception_pass()
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
|
||||
const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
|
||||
const T_ASPHALT_WET := preload(PICK_DIR + "asphalt_wet.png")
|
||||
const T_AWNING := preload(PICK_DIR + "awning_canvas.png")
|
||||
const T_LIMESTONE := preload(PICK_DIR + "stone_steps.png")
|
||||
const T_BRICK_NORMAL := preload(PICK_DIR + "brick_facade_normal.png")
|
||||
const T_CONCRETE_NORMAL := preload(PICK_DIR + "sidewalk_pavers_normal.png")
|
||||
const T_ASPHALT_NORMAL := preload(PICK_DIR + "asphalt_normal.png")
|
||||
const T_ASPHALT_WET_NORMAL := preload(PICK_DIR + "asphalt_wet_normal.png")
|
||||
const T_LIMESTONE_NORMAL := preload(PICK_DIR + "stone_steps_normal.png")
|
||||
|
||||
## Roughness / metallic pairs per surface role. Exposed so tests and tooling can
|
||||
## assert the profiles instead of hard-coding numbers twice.
|
||||
@@ -96,6 +98,9 @@ static func brick(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(10,
|
||||
static func concrete(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(9, 2.4)) -> StandardMaterial3D:
|
||||
return textured(T_CONCRETE, tint, uv_scale, "concrete", T_CONCRETE_NORMAL)
|
||||
|
||||
static func limestone(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(6, 3)) -> StandardMaterial3D:
|
||||
return textured(T_LIMESTONE, tint, uv_scale, "stone", T_LIMESTONE_NORMAL)
|
||||
|
||||
static func asphalt(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(30, 7),
|
||||
wet: bool = false) -> StandardMaterial3D:
|
||||
var texture: Texture2D = T_ASPHALT_WET if wet else T_ASPHALT
|
||||
|
||||
@@ -14,7 +14,10 @@ const UiArt := preload("res://src/game/ui/art.gd")
|
||||
const Strings := preload("res://src/core/strings.gd")
|
||||
|
||||
const VENUES := [
|
||||
{"id": "hollywood_building", "label_key": "venue.hollywood.label", "blurb_key": "venue.hollywood.blurb"},
|
||||
{"id": "hollywood_building", "label_key": "venue.hollywood_day.label",
|
||||
"blurb_key": "venue.hollywood_day.blurb"},
|
||||
{"id": "hollywood_building_night", "label_key": "venue.hollywood_night.label",
|
||||
"blurb_key": "venue.hollywood_night.blurb"},
|
||||
{"id": "joke_shop", "label_key": "venue.joke.label", "blurb_key": "venue.joke.blurb"},
|
||||
{"id": "tv_studio", "label_key": "venue.studio.label", "blurb_key": "venue.studio.blurb"},
|
||||
]
|
||||
|
||||
+691
-34
@@ -8,6 +8,7 @@ const T_BRICK := preload(PICK_DIR + "brick_facade.png")
|
||||
const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
|
||||
const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
|
||||
const T_AWNING := preload(PICK_DIR + "awning_canvas.png")
|
||||
const T_LIMESTONE := preload(PICK_DIR + "stone_steps.png")
|
||||
## One skyline panorama per shift phase (realism pass): hazy daylight, warm dusk
|
||||
## with lit windows, and a night city. The old single strip was a black silhouette.
|
||||
const T_SKYLINE_DAY := preload(PICK_DIR + "skyline_day.png")
|
||||
@@ -44,6 +45,10 @@ var _door_indicator_mat: StandardMaterial3D = null
|
||||
## Street lamps and signage so night mode can switch them on.
|
||||
var _street_lamps: Array[MeshInstance3D] = []
|
||||
var _lamp_omni: Array[OmniLight3D] = []
|
||||
## Low-energy practical washes on the hero facade; kept separate from the street
|
||||
## lamps so a close omni does not blow out the limestone at night.
|
||||
var _accent_omni: Array[OmniLight3D] = []
|
||||
var _ovation_sign: Label3D = null
|
||||
var _lamp_bulb_mats: Array[StandardMaterial3D] = []
|
||||
var marquee_label: Label3D = null
|
||||
var _marquee_emissive: Array[StandardMaterial3D] = []
|
||||
@@ -66,23 +71,55 @@ const LIBRARY_TRIANGLE_BUDGET := 60000
|
||||
## 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, "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],
|
||||
"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,
|
||||
"display_name": "Hollywood Boulevard — El Capitan Entertainment Centre",
|
||||
## The real 6840 Hollywood Blvd building: a 29.6 m wide, 14.5 m high
|
||||
## Neoclassical temple facade with six Ionic columns, eight stone steps,
|
||||
## a recessed central entrance and two street-level side doors.
|
||||
"architecture": "masonic",
|
||||
"facade_width": 29.6, "facade_height": 14.5, "facade_depth": 8.0,
|
||||
"facade_center_z": -8.0, "facade_texture": T_LIMESTONE,
|
||||
"facade_tint": Color(0.88, 0.86, 0.82), "facade_role": "stone",
|
||||
"window_columns": [], "window_rows": [],
|
||||
"stair_width": 12.0, "stair_count": 8, "stair_tread": 0.38, "stair_height": 0.10,
|
||||
"landing_depth": 4.0, "landing_center_z": -1.8,
|
||||
"sidewalk_width": 34.0, "sidewalk_depth": 12.0,
|
||||
"street_width": 120.0, "street_depth": 26.0, "street_front_z": 12.0,
|
||||
"nav_min": Vector2(-18.0, -0.6), "nav_max": Vector2(18.0, 20.0),
|
||||
"guard_spawn": Vector3(0.0, 0.1, -2.0),
|
||||
"guard_spawn": Vector3(0.0, 1.0, -2.0),
|
||||
"features": {
|
||||
"marquee": true, "skyline": true, "handrail": true, "sedan": true,
|
||||
"wall_lanterns": true, "palms": true, "street_trees": true,
|
||||
"marquee": false, "skyline": true, "handrail": false, "sedan": false,
|
||||
"wall_lanterns": true, "palms": true, "street_trees": false,
|
||||
"street_lamps": true, "crosswalk": true, "bulb_canopy": false,
|
||||
"masonic_facade": true, "walk_of_fame": true, "neighbours": true,
|
||||
"opposite_block": true, "hollywood_lane_markings": true,
|
||||
},
|
||||
"street_lamp_positions": [
|
||||
Vector3(-13.2, 0.18, 10.9), Vector3(13.2, 0.18, 10.9),
|
||||
],
|
||||
"prop_placements": [
|
||||
{"zone": "entrance", "prop": "bollard", "x": -7.2, "z": 5.5},
|
||||
{"zone": "entrance", "prop": "bollard", "x": -8.6, "z": 5.5},
|
||||
{"zone": "entrance", "prop": "bollard", "x": 7.2, "z": 5.5},
|
||||
{"zone": "entrance", "prop": "bollard", "x": 8.6, "z": 5.5},
|
||||
{"zone": "entrance", "prop": "trash_bin", "x": -9.8, "z": 5.8, "accessory": "bag"},
|
||||
{"zone": "entrance", "prop": "trash_bin", "x": 9.8, "z": 5.8},
|
||||
{"zone": "mid_block", "prop": "bench", "x": -10.9, "z": 8.1, "rot": 180.0},
|
||||
{"zone": "mid_block", "prop": "bench", "x": 10.9, "z": 8.1, "rot": 180.0},
|
||||
{"zone": "mid_block", "prop": "planter_box", "x": -12.1, "z": 9.5},
|
||||
{"zone": "mid_block", "prop": "planter_box", "x": 12.1, "z": 9.5},
|
||||
{"zone": "curb", "prop": "newspaper_box", "x": -11.4, "z": 11.2, "rot": -12.0},
|
||||
{"zone": "curb", "prop": "newspaper_box", "x": -10.7, "z": 11.2, "rot": -12.0, "accessory": "crate"},
|
||||
{"zone": "curb", "prop": "fire_hydrant", "x": 10.2, "z": 11.3, "accessory": "cone"},
|
||||
{"zone": "mid_block", "prop": "palm_tree", "x": -13.2, "z": 9.7, "rot": 10.0},
|
||||
{"zone": "mid_block", "prop": "palm_tree", "x": 13.2, "z": 9.7, "rot": -10.0},
|
||||
{"zone": "mid_block", "prop": "round_planter", "x": -12.8, "z": 7.0},
|
||||
{"zone": "mid_block", "prop": "round_planter", "x": 12.8, "z": 7.0},
|
||||
{"zone": "curb", "prop": "a_frame_sign", "x": -9.8, "z": 7.0, "rot": 15.0},
|
||||
{"zone": "curb", "prop": "traffic_cone", "x": 9.6, "z": 10.0},
|
||||
{"zone": "curb", "prop": "crowd_barrier", "x": 3.0, "z": 10.9},
|
||||
{"zone": "curb", "prop": "stanchion", "x": -2.6, "z": 10.9},
|
||||
{"zone": "entrance", "prop": "security_podium", "x": 8.0, "z": 6.1, "rot": -15.0},
|
||||
],
|
||||
},
|
||||
"joke_shop": {
|
||||
"display_name": "The Joke Shop",
|
||||
@@ -152,7 +189,10 @@ func _feature(name: String) -> bool:
|
||||
|
||||
func build() -> void:
|
||||
_build_ground()
|
||||
_build_building()
|
||||
if _feature("masonic_facade"):
|
||||
_build_masonic_building()
|
||||
else:
|
||||
_build_building()
|
||||
if _feature("skyline"):
|
||||
_build_skyline_backdrop()
|
||||
if _feature("marquee"):
|
||||
@@ -161,7 +201,15 @@ func build() -> void:
|
||||
_build_bulb_canopy()
|
||||
_build_steps()
|
||||
_build_sidewalk()
|
||||
if _feature("walk_of_fame"):
|
||||
_build_walk_of_fame()
|
||||
if _feature("neighbours"):
|
||||
_build_neighbour_blocks()
|
||||
if _feature("opposite_block"):
|
||||
_build_opposite_block()
|
||||
_build_street()
|
||||
if _feature("hollywood_lane_markings"):
|
||||
_build_hollywood_lane_markings()
|
||||
if _feature("crosswalk"):
|
||||
_build_crosswalk()
|
||||
_build_street_furniture()
|
||||
@@ -265,6 +313,552 @@ func _build_building() -> void:
|
||||
|
||||
## 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
|
||||
## Centre (the 1921 Hollywood Masonic Temple). The facade is a six-column
|
||||
## Neoclassical portico with a recessed entrance, eight stone steps, a
|
||||
## crosscut-grille transom, red event banners and the Masonic parapet motto.
|
||||
## The two neighbours (El Capitan Theatre and the retail block) and the
|
||||
## opposite Ovation Hollywood frontage give the block its real context.
|
||||
func _build_masonic_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 stone := Color(0.86, 0.84, 0.80)
|
||||
var stone_dark := Color(0.61, 0.59, 0.55)
|
||||
var stone_light := Color(0.93, 0.91, 0.87)
|
||||
# Main stone mass. The real building is brick/concrete clad in a light
|
||||
# limestone-like render; granite/limestone keeps the block consistent with
|
||||
# the existing PBR surface library.
|
||||
var facade := _add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz),
|
||||
stone, false, T_LIMESTONE, Vector2(w * 0.22, h * 0.18), "stone")
|
||||
facade.name = "MasonicFacade"
|
||||
# Rusticated base and string courses.
|
||||
_add_box(building, Vector3(w + 0.3, 1.0, d + 0.3), Vector3(0, 0.5, cz + 0.12),
|
||||
stone_dark, true, T_LIMESTONE, Vector2(w * 0.22, 0.8), "stone")
|
||||
for y in [1.12, 1.48, 1.84, 2.20]:
|
||||
_add_box(building, Vector3(w, 0.055, 0.16), Vector3(0, y, front + 0.03),
|
||||
stone_light, false, null, Vector2.ONE, "stone")
|
||||
# Six engaged Ionic columns across the centre of the facade.
|
||||
var columns := Node3D.new()
|
||||
columns.name = "IonicColonnade"
|
||||
building.add_child(columns)
|
||||
for index in 6:
|
||||
var x := -12.4 + float(index) * 4.96
|
||||
_add_masonic_column(columns, x, front)
|
||||
# Entablature, frieze and decorated parapet.
|
||||
_add_box(building, Vector3(w + 0.35, 0.5, 0.7), Vector3(0, 11.55, front + 0.22),
|
||||
stone_light, true, T_LIMESTONE, Vector2(w * 0.18, 0.55), "stone")
|
||||
_add_box(building, Vector3(w, 1.15, 0.45), Vector3(0, 12.35, front + 0.16),
|
||||
stone, true, T_LIMESTONE, Vector2(w * 0.18, 0.8), "stone")
|
||||
_add_box(building, Vector3(w + 0.55, 0.42, 0.9), Vector3(0, 13.16, front + 0.25),
|
||||
stone_light, true, null, Vector2.ONE, "stone")
|
||||
_add_box(building, Vector3(w + 0.2, 0.65, 0.45), Vector3(0, 13.70, front + 0.12),
|
||||
stone, true, T_LIMESTONE, Vector2(w * 0.18, 0.45), "stone")
|
||||
_add_box(building, Vector3(w + 0.4, 0.16, 0.58), Vector3(0, 14.10, front + 0.16),
|
||||
stone_light, true, null, Vector2.ONE, "stone")
|
||||
# A low balustrade above the cornice, matching the reference roofline.
|
||||
var balustrade := Node3D.new()
|
||||
balustrade.name = "ParapetBalustrade"
|
||||
building.add_child(balustrade)
|
||||
for index in 15:
|
||||
var x := -14.0 + float(index) * 2.0
|
||||
_add_box(balustrade, Vector3(0.16, 0.62, 0.18), Vector3(x, 13.96, front + 0.34),
|
||||
stone_light, true, null, Vector2.ONE, "stone")
|
||||
_add_box(balustrade, Vector3(w, 0.12, 0.24), Vector3(0, 14.34, front + 0.34),
|
||||
stone_light, true, null, Vector2.ONE, "stone")
|
||||
# Two carved medallions flank the Masonic motto.
|
||||
for x in [-10.5, 10.5]:
|
||||
var disc := CylinderMesh.new()
|
||||
disc.top_radius = 0.44
|
||||
disc.bottom_radius = 0.44
|
||||
disc.height = 0.12
|
||||
disc.radial_segments = 20
|
||||
var medallion := MeshInstance3D.new()
|
||||
medallion.mesh = disc
|
||||
medallion.position = Vector3(x, 12.40, front + 0.61)
|
||||
medallion.rotation_degrees.x = 90.0
|
||||
medallion.material_override = Materials.limestone(stone_dark, Vector2(0.8, 0.8))
|
||||
building.add_child(medallion)
|
||||
var frieze := Label3D.new()
|
||||
frieze.name = "FriezeInscription"
|
||||
frieze.text = "FREEMASONRY BUILDS ITS TEMPLES\nAMONG THE NATIONS AND IN THE HEARTS OF MEN"
|
||||
frieze.position = Vector3(0, 12.45, front + 0.65)
|
||||
frieze.font_size = 68
|
||||
frieze.pixel_size = 0.0080
|
||||
frieze.modulate = Color(0.25, 0.23, 0.21)
|
||||
frieze.outline_size = 0
|
||||
frieze.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
frieze.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
building.add_child(frieze)
|
||||
marquee_label = frieze
|
||||
# The continuous transom behind the banners: dark crosscut iron grilles.
|
||||
var transom := Node3D.new()
|
||||
transom.name = "TransomGrilles"
|
||||
building.add_child(transom)
|
||||
for index in 5:
|
||||
var x := -9.92 + float(index) * 4.96
|
||||
_add_masonic_lattice(transom, x, 8.55, front + 0.20, 3.85, 2.1)
|
||||
# Three red event banners in the central bays, like the reference facade.
|
||||
var banners := Node3D.new()
|
||||
banners.name = "EventBanners"
|
||||
building.add_child(banners)
|
||||
var banner_texts := ["EL", "CAPITAN", "LIVE"]
|
||||
for index in 3:
|
||||
var x := -4.96 + float(index) * 4.96
|
||||
_add_masonic_banner(banners, x, front, banner_texts[index])
|
||||
# Central recessed double entrance and two street-level side doors.
|
||||
_add_masonic_door(building, 0.0, 4.2, 5.0, front)
|
||||
for side in [-1.0, 1.0]:
|
||||
_add_masonic_door(building, side * 9.92, 2.5, 4.4, front)
|
||||
# Display cases in the two remaining bays.
|
||||
for x in [-4.96, 4.96]:
|
||||
_add_box(building, Vector3(2.2, 3.2, 0.18), Vector3(x, 3.1, front + 0.16),
|
||||
Color(0.10, 0.10, 0.10), true, null, Vector2.ONE, "painted_metal")
|
||||
_add_box(building, Vector3(1.7, 2.2, 0.08), Vector3(x, 3.35, front + 0.27),
|
||||
Color(0.07, 0.08, 0.11), false, null, Vector2.ONE, "glass")
|
||||
_add_box(building, Vector3(2.4, 0.18, 0.28), Vector3(x, 4.75, front + 0.14),
|
||||
stone_light, true, null, Vector2.ONE, "stone")
|
||||
# Door lock indicator beside the central handle.
|
||||
door_indicator = _add_box(building, Vector3(0.16, 0.16, 0.22),
|
||||
Vector3(2.55, 3.32, front + 0.30), Color(0.1, 0.9, 0.3))
|
||||
door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3))
|
||||
# Warm practical wash for the columns and entrance after dark. The lights
|
||||
# are off in daylight and set by `set_night_mode`, like the street lamps.
|
||||
for x in [-10.5, -5.5, 5.5, 10.5]:
|
||||
var wash := OmniLight3D.new()
|
||||
wash.name = "FacadeWash_%d" % int(x)
|
||||
wash.position = Vector3(x, 7.2, front + 2.2)
|
||||
wash.omni_range = 16.0 if Materials.modern_light_falloff() else 10.0
|
||||
wash.omni_attenuation = 1.0
|
||||
wash.light_energy = 0.0
|
||||
wash.light_color = Color(1.0, 0.82, 0.56)
|
||||
building.add_child(wash)
|
||||
_accent_omni.append(wash)
|
||||
# A second, dimmer row higher up keeps the upper column shafts and capitals
|
||||
# from disappearing after dark.
|
||||
for x in [-10.5, -5.5, 5.5, 10.5]:
|
||||
var high := OmniLight3D.new()
|
||||
high.name = "UpperWash_%d" % int(x)
|
||||
high.position = Vector3(x, 10.4, front + 1.8)
|
||||
high.omni_range = 15.0
|
||||
high.omni_attenuation = 1.0
|
||||
high.light_energy = 0.0
|
||||
high.light_color = Color(1.0, 0.78, 0.50)
|
||||
building.add_child(high)
|
||||
_accent_omni.append(high)
|
||||
# Stone collision volume behind the recessed doors.
|
||||
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)
|
||||
|
||||
func _add_masonic_column(parent: Node3D, x: float, front: float) -> void:
|
||||
var z := front + 0.50
|
||||
var stone := Color(0.9, 0.88, 0.84)
|
||||
_add_box(parent, Vector3(1.12, 0.48, 1.08), Vector3(x, 1.24, z),
|
||||
Color(0.78, 0.76, 0.72), true, T_LIMESTONE, Vector2(0.8, 0.5), "stone")
|
||||
_add_box(parent, Vector3(0.94, 0.34, 0.94), Vector3(x, 1.65, z),
|
||||
stone, true, null, Vector2.ONE, "stone")
|
||||
var shaft := CylinderMesh.new()
|
||||
shaft.top_radius = 0.34
|
||||
shaft.bottom_radius = 0.43
|
||||
shaft.height = 8.9
|
||||
shaft.radial_segments = 14
|
||||
shaft.rings = 4
|
||||
var mesh := MeshInstance3D.new()
|
||||
mesh.name = "Column_%d" % int(round(x))
|
||||
mesh.mesh = shaft
|
||||
mesh.position = Vector3(x, 6.27, z)
|
||||
mesh.material_override = Materials.limestone(stone, Vector2(1.4, 4.2))
|
||||
mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
parent.add_child(mesh)
|
||||
var capital := CylinderMesh.new()
|
||||
capital.top_radius = 0.54
|
||||
capital.bottom_radius = 0.40
|
||||
capital.height = 0.42
|
||||
capital.radial_segments = 14
|
||||
var cap := MeshInstance3D.new()
|
||||
cap.mesh = capital
|
||||
cap.position = Vector3(x, 10.93, z)
|
||||
cap.material_override = Materials.limestone(stone, Vector2(1.0, 0.6))
|
||||
cap.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
parent.add_child(cap)
|
||||
_add_box(parent, Vector3(1.12, 0.20, 1.10), Vector3(x, 11.22, z),
|
||||
stone, true, null, Vector2.ONE, "stone")
|
||||
|
||||
## Dark crosscut iron grille panel. The bars are deliberately simple boxes: at
|
||||
## gameplay distance they read as the reference transom without a texture cost.
|
||||
func _add_masonic_lattice(parent: Node3D, x: float, y: float, z: float,
|
||||
width: float, height: float) -> void:
|
||||
_add_box(parent, Vector3(width, height, 0.08), Vector3(x, y, z),
|
||||
Color(0.08, 0.08, 0.09), false, null, Vector2.ONE, "painted_metal")
|
||||
var columns := 6
|
||||
for index in columns:
|
||||
var bar_x := x - width * 0.5 + (float(index) + 0.5) * width / float(columns)
|
||||
_add_box(parent, Vector3(0.055, height, 0.05), Vector3(bar_x, y, z + 0.07),
|
||||
Color(0.23, 0.22, 0.20), false, null, Vector2.ONE, "metal")
|
||||
for index in 4:
|
||||
var bar_y := y - height * 0.5 + (float(index) + 0.5) * height / 4.0
|
||||
_add_box(parent, Vector3(width, 0.055, 0.05), Vector3(x, bar_y, z + 0.07),
|
||||
Color(0.23, 0.22, 0.20), false, null, Vector2.ONE, "metal")
|
||||
|
||||
func _add_masonic_banner(parent: Node3D, x: float, front: float, text: String) -> void:
|
||||
var red := Color(0.58, 0.055, 0.065)
|
||||
_add_box(parent, Vector3(2.62, 5.55, 0.06), Vector3(x, 8.2, front + 0.73),
|
||||
red, true, null, Vector2.ONE, "cloth")
|
||||
for y in [5.55, 10.85]:
|
||||
_add_box(parent, Vector3(2.82, 0.10, 0.12), Vector3(x, y, front + 0.75),
|
||||
Color(0.78, 0.62, 0.28), true, null, Vector2.ONE, "metal")
|
||||
_add_box(parent, Vector3(0.10, 5.55, 0.10), Vector3(x - 1.28, 8.2, front + 0.78),
|
||||
Color(0.78, 0.62, 0.28), false, null, Vector2.ONE, "metal")
|
||||
_add_box(parent, Vector3(0.10, 5.55, 0.10), Vector3(x + 1.28, 8.2, front + 0.78),
|
||||
Color(0.78, 0.62, 0.28), false, null, Vector2.ONE, "metal")
|
||||
var label := Label3D.new()
|
||||
label.name = "Banner_%s" % text
|
||||
label.text = "\n".join(text.split(""))
|
||||
label.position = Vector3(x, 8.2, front + 0.80)
|
||||
label.font_size = 112
|
||||
label.pixel_size = 0.0044
|
||||
label.modulate = Color(1.0, 0.83, 0.47)
|
||||
label.outline_size = 0
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
parent.add_child(label)
|
||||
|
||||
func _add_masonic_door(parent: Node3D, x: float, width: float, height: float,
|
||||
front: float) -> void:
|
||||
var stone_light := Color(0.93, 0.91, 0.87)
|
||||
var bottom := 1.0
|
||||
# Recessed stone surround and dark timber/iron door.
|
||||
_add_box(parent, Vector3(width + 0.55, height + 0.35, 0.18),
|
||||
Vector3(x, bottom + height * 0.5, front + 0.06),
|
||||
Color(0.33, 0.31, 0.28), true, null, Vector2.ONE, "stone")
|
||||
_add_box(parent, Vector3(width, height, 0.14),
|
||||
Vector3(x, bottom + height * 0.5, front + 0.14),
|
||||
Color(0.075, 0.065, 0.060), true, null, Vector2.ONE, "wood")
|
||||
for side in [-1.0, 1.0]:
|
||||
_add_box(parent, Vector3(0.10, height - 0.25, 0.08),
|
||||
Vector3(x + side * width * 0.28, bottom + height * 0.5, front + 0.22),
|
||||
Color(0.33, 0.25, 0.16), false, null, Vector2.ONE, "wood")
|
||||
_add_box(parent, Vector3(0.055, 0.55, 0.055),
|
||||
Vector3(x + side * 0.22, bottom + 2.55, front + 0.24),
|
||||
Color(0.72, 0.65, 0.45), false, null, Vector2.ONE, "metal")
|
||||
# Lit transom above the door (the reference has a continuous grille band).
|
||||
var pane := _add_box(parent, Vector3(width * 0.88, 0.72, 0.08),
|
||||
Vector3(x, bottom + height + 0.38, front + 0.15), Color(1, 1, 1))
|
||||
var pane_mat := Materials.window_pane()
|
||||
pane.material_override = pane_mat
|
||||
_window_mats.append(pane_mat)
|
||||
for index in 3:
|
||||
_add_box(parent, Vector3(width * 0.88, 0.045, 0.04),
|
||||
Vector3(x, bottom + height + 0.10 + float(index) * 0.27, front + 0.20),
|
||||
Color(0.20, 0.19, 0.18), false, null, Vector2.ONE, "metal")
|
||||
|
||||
## Neighbouring El Capitan Theatre (6838) and the retail block east of the
|
||||
## temple. They are deliberately simpler than the hero facade but preserve the
|
||||
## street wall, marquee scale and the vertical theatre sign.
|
||||
func _build_neighbour_blocks() -> void:
|
||||
var group := Node3D.new()
|
||||
group.name = "NeighbourBlocks"
|
||||
root.add_child(group)
|
||||
# West neighbour: El Capitan Theatre.
|
||||
var theatre := StaticBody3D.new()
|
||||
theatre.name = "ElCapitanTheatre"
|
||||
_add_box(theatre, Vector3(17.6, 18.5, 7.0), Vector3(-24.1, 9.25, -7.5),
|
||||
Color(0.82, 0.74, 0.61), false, T_LIMESTONE, Vector2(6.0, 8.0), "stone")
|
||||
_add_box(theatre, Vector3(18.0, 0.34, 7.4), Vector3(-24.1, 6.4, -7.5),
|
||||
Color(0.63, 0.54, 0.43), true, null, Vector2.ONE, "stone")
|
||||
_add_box(theatre, Vector3(18.0, 0.48, 7.4), Vector3(-24.1, 18.2, -7.5),
|
||||
Color(0.56, 0.47, 0.38), true, null, Vector2.ONE, "stone")
|
||||
for x in [-30.0, -26.8, -23.6, -20.4, -17.2]:
|
||||
for y in [8.8, 12.6]:
|
||||
_add_box(theatre, Vector3(2.4, 2.4, 0.10), Vector3(x, y, -3.95),
|
||||
Color(0.09, 0.10, 0.12), false, null, Vector2.ONE, "glass")
|
||||
_add_box(theatre, Vector3(2.7, 0.18, 0.18), Vector3(x, y - 1.32, -3.90),
|
||||
Color(0.72, 0.64, 0.52), true, null, Vector2.ONE, "stone")
|
||||
# The theatre marquee projects over the sidewalk.
|
||||
_add_box(theatre, Vector3(10.5, 0.42, 2.3), Vector3(-23.4, 5.65, -3.30),
|
||||
Color(0.45, 0.035, 0.045), true, null, Vector2.ONE, "painted_metal")
|
||||
var marquee_trim := Materials.emissive(Color(1.0, 0.70, 0.36), 1.2)
|
||||
for index in 8:
|
||||
var bulb := MeshInstance3D.new()
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 0.06
|
||||
sphere.height = 0.12
|
||||
bulb.mesh = sphere
|
||||
bulb.position = Vector3(-28.0 + float(index) * 1.35, 5.40, -2.20)
|
||||
bulb.material_override = marquee_trim
|
||||
theatre.add_child(bulb)
|
||||
if not _marquee_emissive.has(marquee_trim):
|
||||
_marquee_emissive.append(marquee_trim)
|
||||
var marquee_glow := OmniLight3D.new()
|
||||
marquee_glow.name = "MarqueeGlow"
|
||||
marquee_glow.position = Vector3(-23.4, 5.3, -2.4)
|
||||
marquee_glow.omni_range = 18.0 if Materials.modern_light_falloff() else 12.0
|
||||
marquee_glow.omni_attenuation = 1.0
|
||||
marquee_glow.light_energy = 0.0
|
||||
marquee_glow.light_color = Color(1.0, 0.70, 0.38)
|
||||
theatre.add_child(marquee_glow)
|
||||
_accent_omni.append(marquee_glow)
|
||||
_add_box(theatre, Vector3(10.5, 0.08, 2.3), Vector3(-23.4, 5.87, -3.30),
|
||||
Color(0.95, 0.84, 0.58), false, null, Vector2.ONE, "stone")
|
||||
var theatre_label := Label3D.new()
|
||||
theatre_label.name = "ElCapitanMarqueeLabel"
|
||||
theatre_label.text = "EL CAPITAN"
|
||||
theatre_label.position = Vector3(-23.4, 5.66, -2.12)
|
||||
theatre_label.font_size = 110
|
||||
theatre_label.pixel_size = 0.0040
|
||||
theatre_label.modulate = Color(1.0, 0.78, 0.46)
|
||||
theatre_label.billboard = BaseMaterial3D.BILLBOARD_DISABLED
|
||||
theatre.add_child(theatre_label)
|
||||
# Vertical blade sign at the temple-side corner.
|
||||
_add_box(theatre, Vector3(0.92, 7.2, 0.26), Vector3(-16.1, 8.9, -3.18),
|
||||
Color(0.46, 0.035, 0.045), true, null, Vector2.ONE, "painted_metal")
|
||||
var blade := Label3D.new()
|
||||
blade.name = "ElCapitanBladeSign"
|
||||
blade.text = "EL\nCAP\nITAN"
|
||||
blade.position = Vector3(-16.1, 8.9, -3.02)
|
||||
blade.font_size = 82
|
||||
blade.pixel_size = 0.0042
|
||||
blade.modulate = Color(1.0, 0.88, 0.62)
|
||||
blade.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
blade.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
theatre.add_child(blade)
|
||||
var theatre_col := BoxShape3D.new()
|
||||
theatre_col.size = Vector3(17.6, 18.5, 7.0)
|
||||
var theatre_owner := theatre.create_shape_owner(theatre)
|
||||
theatre.shape_owner_add_shape(theatre_owner, theatre_col)
|
||||
theatre.shape_owner_set_transform(theatre_owner,
|
||||
Transform3D(Basis(), Vector3(-24.1, 9.25, -7.5)))
|
||||
group.add_child(theatre)
|
||||
# East neighbour: the modern retail block beside the temple.
|
||||
var retail := StaticBody3D.new()
|
||||
retail.name = "EastRetailBlock"
|
||||
_add_box(retail, Vector3(16.0, 13.2, 6.2), Vector3(24.6, 6.6, -7.0),
|
||||
Color(0.76, 0.74, 0.69), false, T_LIMESTONE, Vector2(6.0, 6.0), "stone")
|
||||
_add_box(retail, Vector3(16.2, 0.36, 6.6), Vector3(24.6, 13.0, -7.0),
|
||||
Color(0.61, 0.58, 0.52), true, null, Vector2.ONE, "stone")
|
||||
_add_box(retail, Vector3(15.2, 4.4, 0.12), Vector3(24.6, 2.4, -3.88),
|
||||
Color(0.08, 0.09, 0.11), false, null, Vector2.ONE, "glass")
|
||||
for x in [18.2, 21.8, 25.4, 29.0, 31.0]:
|
||||
_add_box(retail, Vector3(0.18, 4.4, 0.22), Vector3(x, 2.4, -3.82),
|
||||
Color(0.16, 0.16, 0.17), true, null, Vector2.ONE, "metal")
|
||||
for x in [18.2, 22.0, 25.8, 29.6]:
|
||||
for y in [7.4, 10.2]:
|
||||
_add_box(retail, Vector3(2.0, 1.8, 0.10), Vector3(x, y, -3.92),
|
||||
Color(0.10, 0.11, 0.13), false, null, Vector2.ONE, "glass")
|
||||
_add_box(retail, Vector3(2.3, 0.15, 0.17), Vector3(x, y - 1.0, -3.88),
|
||||
Color(0.66, 0.63, 0.56), true, null, Vector2.ONE, "stone")
|
||||
var retail_label := Label3D.new()
|
||||
retail_label.name = "EastRetailSign"
|
||||
retail_label.text = "ZARA"
|
||||
retail_label.position = Vector3(24.6, 5.4, -3.74)
|
||||
retail_label.font_size = 94
|
||||
retail_label.pixel_size = 0.0044
|
||||
retail_label.modulate = Color(0.10, 0.10, 0.10)
|
||||
retail.add_child(retail_label)
|
||||
var retail_col := BoxShape3D.new()
|
||||
retail_col.size = Vector3(16.0, 13.2, 6.2)
|
||||
var retail_owner := retail.create_shape_owner(retail)
|
||||
retail.shape_owner_add_shape(retail_owner, retail_col)
|
||||
retail.shape_owner_set_transform(retail_owner,
|
||||
Transform3D(Basis(), Vector3(24.6, 6.6, -7.0)))
|
||||
group.add_child(retail)
|
||||
|
||||
## A flat, front-facing arch panel used for the Ovation arcade. Both the stone
|
||||
## surround and the dark opening use the same mesh at different sizes, which
|
||||
## keeps the arch silhouette readable without a CSG or custom shader.
|
||||
func _arch_panel_mesh(width: float, height: float, segments: int = 18) -> ArrayMesh:
|
||||
var radius := width * 0.5
|
||||
var spring := maxf(0.0, height - radius)
|
||||
var polygon := PackedVector2Array()
|
||||
polygon.append(Vector2(-radius, 0.0))
|
||||
polygon.append(Vector2(radius, 0.0))
|
||||
polygon.append(Vector2(radius, spring))
|
||||
for index in range(1, segments):
|
||||
var angle := PI * float(index) / float(segments)
|
||||
polygon.append(Vector2(cos(angle) * radius, spring + sin(angle) * radius))
|
||||
polygon.append(Vector2(-radius, spring))
|
||||
var triangles := Geometry2D.triangulate_polygon(polygon)
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
for point in polygon:
|
||||
vertices.append(Vector3(point.x, point.y, 0.0))
|
||||
normals.append(Vector3(0.0, 0.0, -1.0))
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array(triangles)
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
## The south side of Hollywood Boulevard: the Ovation Hollywood frontage
|
||||
## across the four-lane street, with its deep retail podium and sign band.
|
||||
func _build_opposite_block() -> void:
|
||||
var opposite := Node3D.new()
|
||||
opposite.name = "OvationHollywood"
|
||||
root.add_child(opposite)
|
||||
# South sidewalk, separate from the north walk.
|
||||
_add_box(opposite, Vector3(96, 0.18, 6.0), Vector3(0, 0.09, 41.0),
|
||||
Color(0.55, 0.55, 0.54), false, T_CONCRETE, Vector2(28, 2.0), "concrete")
|
||||
_add_box(opposite, Vector3(98, 0.22, 0.5), Vector3(0, 0.11, 38.1),
|
||||
Color(0.62, 0.61, 0.58), false, null, Vector2.ONE, "stone")
|
||||
# Deep retail mass with a shaded arcade at street level.
|
||||
_add_box(opposite, Vector3(82, 21.0, 12.0), Vector3(0, 10.5, 50.5),
|
||||
Color(0.79, 0.75, 0.68), false, T_LIMESTONE, Vector2(12, 6), "stone")
|
||||
# Ground-floor arcade: seven tall arched openings with a stone surround. The
|
||||
# concept reference shows the Ovation frontage as a deep shaded arcade rather
|
||||
# than a blank glass strip.
|
||||
var arch_xs := [-24.0, -16.0, -8.0, 0.0, 8.0, 16.0, 24.0]
|
||||
for x in arch_xs:
|
||||
var surround := MeshInstance3D.new()
|
||||
surround.name = "OvationArchSurround"
|
||||
surround.mesh = _arch_panel_mesh(6.6, 6.4)
|
||||
surround.position = Vector3(x, 0.18, 44.28)
|
||||
surround.material_override = Materials.limestone(Color(0.90, 0.86, 0.78), Vector2(1.2, 1.0))
|
||||
opposite.add_child(surround)
|
||||
var opening := MeshInstance3D.new()
|
||||
opening.name = "OvationArchOpening"
|
||||
opening.mesh = _arch_panel_mesh(5.5, 5.6)
|
||||
opening.position = Vector3(x, 0.18, 44.24)
|
||||
if int((x + 24.0) / 8.0) % 2 == 0:
|
||||
var shop_pane := Materials.window_pane()
|
||||
opening.material_override = shop_pane
|
||||
_window_mats.append(shop_pane)
|
||||
else:
|
||||
opening.material_override = Materials.glass(Color(0.08, 0.09, 0.11, 0.94))
|
||||
opposite.add_child(opening)
|
||||
# Pilasters between the arcade bays and a string course above them break up
|
||||
# the long frontage instead of leaving one flat grey wall.
|
||||
for x in [-28.0, -20.0, -12.0, -4.0, 4.0, 12.0, 20.0, 28.0]:
|
||||
_add_box(opposite, Vector3(0.9, 6.4, 0.45), Vector3(x, 3.4, 44.22),
|
||||
Color(0.86, 0.82, 0.74), true, null, Vector2.ONE, "stone")
|
||||
_add_box(opposite, Vector3(82, 0.35, 0.40), Vector3(0, 6.9, 44.20),
|
||||
Color(0.84, 0.80, 0.72), true, null, Vector2.ONE, "stone")
|
||||
_add_box(opposite, Vector3(82, 0.38, 0.90), Vector3(0, 20.82, 44.10),
|
||||
Color(0.72, 0.68, 0.61), true, null, Vector2.ONE, "stone")
|
||||
# Upper ribbon windows and the sign band.
|
||||
for row_index in 3:
|
||||
var row: float = [9.0, 12.2, 15.4][row_index]
|
||||
for x in range(-34, 38, 8):
|
||||
var window := _add_box(opposite, Vector3(5.0, 1.25, 0.12),
|
||||
Vector3(float(x), row, 44.30), Color(0.09, 0.10, 0.12),
|
||||
false, null, Vector2.ONE, "glass")
|
||||
if (row_index + int((x + 34) / 8)) % 3 != 1:
|
||||
var pane_mat := Materials.window_pane()
|
||||
window.material_override = pane_mat
|
||||
_window_mats.append(pane_mat)
|
||||
_add_box(opposite, Vector3(82, 0.45, 0.70), Vector3(0, 18.0, 44.15),
|
||||
Color(0.60, 0.56, 0.49), true, null, Vector2.ONE, "stone")
|
||||
var ovation := Label3D.new()
|
||||
ovation.name = "OvationSign"
|
||||
ovation.text = "OVATION HOLLYWOOD"
|
||||
ovation.position = Vector3(0, 16.4, 44.05)
|
||||
ovation.font_size = 150
|
||||
ovation.pixel_size = 0.0052
|
||||
ovation.modulate = Color(0.16, 0.14, 0.12)
|
||||
ovation.outline_size = 0
|
||||
ovation.outline_modulate = Color(0.90, 0.87, 0.80, 0.8)
|
||||
# The sign faces north across the boulevard, so its front points -Z.
|
||||
ovation.rotation.y = PI
|
||||
opposite.add_child(ovation)
|
||||
_ovation_sign = ovation
|
||||
# Practical wash across the boulevard so the opposite frontage reads after
|
||||
# dark instead of becoming a black slab.
|
||||
for x in [-24.0, 0.0, 24.0]:
|
||||
var wash := OmniLight3D.new()
|
||||
wash.name = "OvationWash_%d" % int(x)
|
||||
wash.position = Vector3(x, 6.0, 40.2)
|
||||
wash.omni_range = 18.0 if Materials.modern_light_falloff() else 12.0
|
||||
wash.omni_attenuation = 1.0
|
||||
wash.light_energy = 0.0
|
||||
wash.light_color = Color(1.0, 0.84, 0.62)
|
||||
opposite.add_child(wash)
|
||||
_accent_omni.append(wash)
|
||||
# South-side palms frame the far sidewalk.
|
||||
for side in [-1.0, 1.0]:
|
||||
var palm := _add_prop_model(opposite, "palm_tree",
|
||||
Vector3(side * 18.0, 0.18, 41.2), 90.0 if side < 0.0 else -90.0)
|
||||
palm.name = "SouthPalm_%s" % ("west" if side < 0.0 else "east")
|
||||
|
||||
## Hollywood Walk of Fame: dark terrazzo band, brass star discs and coral
|
||||
## five-point stars along the entrance sidewalk.
|
||||
func _build_walk_of_fame() -> void:
|
||||
var walk := Node3D.new()
|
||||
walk.name = "WalkOfFame"
|
||||
root.add_child(walk)
|
||||
_add_box(walk, Vector3(30.0, 0.035, 1.75), Vector3(0, 0.198, 9.15),
|
||||
Color(0.20, 0.18, 0.17), false, null, Vector2.ONE, "stone")
|
||||
var star_mesh := _star_mesh(0.40, 0.17)
|
||||
var brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal")
|
||||
var coral := Materials.flat(Color(0.72, 0.30, 0.28), "stone")
|
||||
for index in 10:
|
||||
var x := -13.5 + float(index) * 3.0
|
||||
_add_cylinder_mesh(walk, 0.55, 0.02, Vector3(x, 0.214, 9.15),
|
||||
Color(0.58, 0.49, 0.30), "metal")
|
||||
var star := MeshInstance3D.new()
|
||||
star.name = "WalkStar_%d" % index
|
||||
star.mesh = star_mesh
|
||||
star.position = Vector3(x, 0.226, 9.15)
|
||||
star.material_override = coral if index % 2 == 0 else brass
|
||||
walk.add_child(star)
|
||||
|
||||
func _star_mesh(outer: float, inner: float) -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var indices := PackedInt32Array()
|
||||
for index in 10:
|
||||
var angle := -PI * 0.5 + float(index) * PI / 5.0
|
||||
var radius := outer if index % 2 == 0 else inner
|
||||
vertices.append(Vector3(cos(angle) * radius, 0.0, sin(angle) * radius))
|
||||
normals.append(Vector3.UP)
|
||||
vertices.append(Vector3.ZERO)
|
||||
normals.append(Vector3.UP)
|
||||
var center := 10
|
||||
for index in 10:
|
||||
indices.append(center)
|
||||
indices.append(index)
|
||||
indices.append((index + 1) % 10)
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
## Four-lane boulevard markings: double-yellow centre, broken white lane lines,
|
||||
## and the red-painted kerb from the reference photographs.
|
||||
func _build_hollywood_lane_markings() -> void:
|
||||
var street_w := float(spec.get("street_width", 120.0))
|
||||
var street_d := float(spec.get("street_depth", 26.0))
|
||||
var front := float(spec.get("street_front_z", 12.0))
|
||||
var center_z := front + street_d * 0.5
|
||||
var yellow := Color(0.86, 0.70, 0.16)
|
||||
for offset in [-0.14, 0.14]:
|
||||
_add_box(root, Vector3(street_w * 0.92, 0.02, 0.09),
|
||||
Vector3(0, 0.115, center_z + offset), yellow, false, null, Vector2.ONE, "concrete")
|
||||
var white := Color(0.88, 0.87, 0.80)
|
||||
for offset in [-6.5, 6.5]:
|
||||
for x in range(-55, 56, 6):
|
||||
_add_box(root, Vector3(2.6, 0.02, 0.08),
|
||||
Vector3(float(x), 0.116, center_z + offset),
|
||||
white, false, null, Vector2.ONE, "concrete")
|
||||
# Red kerb paint: the painted face of the concrete kerb.
|
||||
_add_box(root, Vector3(street_w, 0.13, 0.055),
|
||||
Vector3(0, 0.19, front - 0.26), Color(0.58, 0.075, 0.055),
|
||||
false, null, Vector2.ONE, "painted_metal")
|
||||
|
||||
|
||||
func _build_facade_windows(parent: Node3D) -> void:
|
||||
var columns: Array = spec.get("window_columns", [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0])
|
||||
var rows: Array = spec.get("window_rows", [9.2, 12.4])
|
||||
@@ -302,22 +896,34 @@ func _build_steps() -> void:
|
||||
var y: float = i * height
|
||||
var z: float = float(count) * tread - i * tread
|
||||
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))
|
||||
var tread_box := _add_box(steps, Vector3(w, height, tread),
|
||||
Vector3(0, y + height / 2.0, z), Color(0.55, 0.52, 0.5))
|
||||
if _feature("masonic_facade"):
|
||||
tread_box.material_override = Materials.limestone(
|
||||
Color(0.70, 0.67, 0.61), Vector2(w * 0.55, 0.55))
|
||||
# Add collision shape to each tread
|
||||
var tread_col := BoxShape3D.new()
|
||||
tread_col.size = Vector3(w, height, tread)
|
||||
var owner_id := steps.create_shape_owner(steps)
|
||||
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)))
|
||||
# Landing at the top with collision
|
||||
# Landing at the top with collision. The depth/centre are spec-driven so the
|
||||
# eight-step Masonic entrance reaches the recessed doors.
|
||||
var landing_y: float = count * height + 0.1
|
||||
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_depth := float(spec.get("landing_depth", 3.0))
|
||||
var landing_center_z := float(spec.get("landing_center_z", -1.5))
|
||||
var landing_box := _add_box(steps, Vector3(landing_w, 0.2, landing_depth),
|
||||
Vector3(0, landing_y, landing_center_z), Color(0.5, 0.47, 0.45))
|
||||
if _feature("masonic_facade"):
|
||||
landing_box.material_override = Materials.limestone(
|
||||
Color(0.70, 0.67, 0.61), Vector2(landing_w * 0.55, 1.0))
|
||||
var landing_col := BoxShape3D.new()
|
||||
landing_col.size = Vector3(landing_w, 0.2, 3)
|
||||
landing_col.size = Vector3(landing_w, 0.2, landing_depth)
|
||||
var landing_owner := steps.create_shape_owner(steps)
|
||||
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, landing_center_z)))
|
||||
root.add_child(steps)
|
||||
|
||||
func _build_sidewalk() -> void:
|
||||
@@ -342,15 +948,20 @@ func _build_street() -> void:
|
||||
# asphalt map (glossier, with lamp reflections) and back.
|
||||
var street_w := float(spec.get("street_width", 120.0))
|
||||
var street_d := float(spec.get("street_depth", 26.0))
|
||||
var curb_z := float(spec.get("sidewalk_depth", 8.0)) + 4.1
|
||||
var street_z := curb_z + street_d * 0.5 - 3.1
|
||||
var curb_z := float(spec.get("curb_z", float(spec.get("sidewalk_depth", 8.0)) + 4.1))
|
||||
# The default venues use the legacy 3.1 m overlap; the Hollywood block has an
|
||||
# explicit street frontage so the kerb, sidewalk and red paint line up.
|
||||
var street_front := float(spec.get("street_front_z", curb_z - 3.1))
|
||||
var street_z := street_front + street_d * 0.5
|
||||
_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
|
||||
# Center dashes along the street (x varied so they form a lane line, not a stack).
|
||||
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, street_z), Color(0.85, 0.85, 0.30), false, null, Vector2.ONE, "concrete")
|
||||
# Legacy centre dashes. The Hollywood block replaces these with the proper
|
||||
# four-lane markings after the street is built.
|
||||
if not _feature("hollywood_lane_markings"):
|
||||
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, street_z), Color(0.85, 0.85, 0.30), false, null, Vector2.ONE, "concrete")
|
||||
# Curb between the sidewalk and the roadway.
|
||||
_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()
|
||||
@@ -517,6 +1128,9 @@ func _build_library_dressing() -> void:
|
||||
if _feature("wall_lanterns") and ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"):
|
||||
if venue_id == "joke_shop":
|
||||
_add_wall_lantern(Vector3(3.2, 3.3, -3.72), true)
|
||||
elif _feature("masonic_facade"):
|
||||
for side in [-13.6, 13.6]:
|
||||
_add_wall_lantern(Vector3(side, 3.4, -3.62), true)
|
||||
else:
|
||||
for side in [-4.9, 4.9]:
|
||||
_add_wall_lantern(Vector3(side, 3.3, -3.78), false)
|
||||
@@ -549,7 +1163,7 @@ func _build_bulb_canopy() -> void:
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(x, 5.15, front + 1.4)
|
||||
omni.omni_range = 12.0 if Materials.modern_light_falloff() else 8.0
|
||||
omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0
|
||||
omni.omni_attenuation = 1.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1.0, 0.72, 0.36)
|
||||
root.add_child(omni)
|
||||
@@ -588,7 +1202,7 @@ func _add_wall_lantern(pos: Vector3, with_light: bool) -> void:
|
||||
omni.name = "LanternGlow"
|
||||
omni.position = pos + Vector3(0.0, -0.1, 0.45)
|
||||
omni.omni_range = 14.0 if Materials.modern_light_falloff() else 9.0
|
||||
omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0
|
||||
omni.omni_attenuation = 1.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1.0, 0.72, 0.38)
|
||||
root.add_child(omni)
|
||||
@@ -662,14 +1276,19 @@ func _build_street_furniture() -> void:
|
||||
# arm points to local -X, so the left side is rotated 180 degrees to face
|
||||
# the sidewalk.
|
||||
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 lamp_positions: Array = spec.get("street_lamp_positions", [])
|
||||
if lamp_positions.is_empty():
|
||||
for side in [-7.5, 7.5]:
|
||||
lamp_positions.append(Vector3(side, 0.18, 8.5))
|
||||
for index in lamp_positions.size():
|
||||
var lamp_pos: Vector3 = lamp_positions[index]
|
||||
var holder := _add_prop_model(root, "street_lamp", lamp_pos,
|
||||
180.0 if lamp_pos.x < 0.0 else 0.0)
|
||||
holder.name = "Lamp_%d" % index
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(-0.85, 4.65, 0)
|
||||
omni.omni_range = 16.0 if Materials.modern_light_falloff() else 12.0
|
||||
omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0
|
||||
omni.omni_attenuation = 1.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1, 0.93, 0.75)
|
||||
holder.add_child(omni)
|
||||
@@ -685,6 +1304,27 @@ func _build_street_furniture() -> void:
|
||||
_add_prop_model(root, "street_tree", Vector3(x, 0.18, z),
|
||||
float((i * 37) % 360))
|
||||
|
||||
## Total triangles in the whole venue, including procedural architecture. Used by
|
||||
## the map audit to catch duplicate/hidden geometry after a rebuild.
|
||||
func world_triangle_count() -> int:
|
||||
var total := 0
|
||||
for node in root.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := node as MeshInstance3D
|
||||
if mi == null or mi.mesh == null:
|
||||
continue
|
||||
for index in mi.mesh.get_surface_count():
|
||||
var arrays := mi.mesh.surface_get_arrays(index)
|
||||
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
||||
if indices.size() > 0:
|
||||
total += indices.size() / 3
|
||||
else:
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
total += vertices.size() / 3
|
||||
return total
|
||||
|
||||
func world_mesh_count() -> int:
|
||||
return root.find_children("*", "MeshInstance3D", true, false).size()
|
||||
|
||||
## Total triangles across the placed Blender library pieces, for the draw budget
|
||||
## test. Counts indices when present, otherwise the vertex stream.
|
||||
func library_triangle_count() -> int:
|
||||
@@ -1034,15 +1674,32 @@ func set_night_mode(night: bool) -> void:
|
||||
mat.emission_energy_multiplier = (2.0 if night else 0.0)
|
||||
for omni in _lamp_omni:
|
||||
# Two lamps at full energy washed the whole sidewalk grey; keep visible
|
||||
# pools of light instead of flat night-time brightness.
|
||||
omni.light_energy = (1.8 if night else 0.0)
|
||||
# pools of light instead of flat night-time brightness. The wall sconces
|
||||
# sit close to the stone, so they need far less energy than street lamps.
|
||||
var lamp_energy := 2.2
|
||||
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)
|
||||
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.
|
||||
var energy := 1.2
|
||||
if omni.name.begins_with("FacadeWash"):
|
||||
energy = 0.50
|
||||
elif omni.name.begins_with("UpperWash"):
|
||||
energy = 0.28
|
||||
elif omni.name.begins_with("OvationWash"):
|
||||
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)
|
||||
for mat in _marquee_emissive:
|
||||
mat.emission_energy_multiplier = (2.0 if night else 0.2)
|
||||
# Light most, not all, of the windows: a fully lit grid reads as a light box
|
||||
# instead of an occupied building.
|
||||
for i in _window_mats.size():
|
||||
var lit := night and i % 3 != 2
|
||||
_window_mats[i].emission_energy_multiplier = (1.1 if lit else 0.0)
|
||||
_window_mats[i].emission_energy_multiplier = (0.65 if lit else 0.0)
|
||||
if marquee_label:
|
||||
marquee_label.modulate.a = (1.0 if night else 0.9)
|
||||
|
||||
|
||||
Reference in new issue
Block a user