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
@@ -173,6 +173,10 @@ func _begin_stage() -> void:
|
||||
if venue != "" and venue != str(gm.get("active_scenario_id")):
|
||||
gm.call("switch_scenario", venue)
|
||||
gm.coordinator.elapsed_s = float(stage["elapsed"])
|
||||
if gm.hud != null:
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
gm.hud.set_action_hint("")
|
||||
if _flashlight:
|
||||
var want := bool(stage["night"])
|
||||
if want != bool(gm.flashlight_on):
|
||||
|
||||
@@ -105,6 +105,7 @@ func _advance() -> void:
|
||||
if state_frames == 1 and gm != null:
|
||||
gm.guard.rotation.y = deg_to_rad(-70)
|
||||
gm.guard.head.rotation.x = deg_to_rad(3.0)
|
||||
_reset_hud()
|
||||
if state_frames >= 90:
|
||||
_shoot("05-late-night-avenue.png")
|
||||
state = 6
|
||||
@@ -114,6 +115,7 @@ func _advance() -> void:
|
||||
if state_frames == 1 and gm != null:
|
||||
gm.guard.rotation.y = 0.0
|
||||
gm.guard.head.rotation.x = deg_to_rad(4.0)
|
||||
_reset_hud()
|
||||
gm.call("_toggle_flashlight")
|
||||
if state_frames >= 30:
|
||||
_shoot("06-night-flashlight.png")
|
||||
@@ -239,6 +241,15 @@ func _advance() -> void:
|
||||
_shoot("13-studio-arrival.png")
|
||||
quit(0)
|
||||
|
||||
## Clear log/target leftovers between staged shots so the HUD only shows the
|
||||
## current moment.
|
||||
func _reset_hud() -> void:
|
||||
if gm == null or gm.hud == null:
|
||||
return
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
gm.hud.set_action_hint("")
|
||||
|
||||
## Put the guard a couple of steps in front of the target so a staged UI capture
|
||||
## frames the person the panel is about.
|
||||
func _wand_alerts() -> bool:
|
||||
|
||||
@@ -29,22 +29,36 @@ func _process(_delta: float) -> bool:
|
||||
quit(1)
|
||||
return true
|
||||
print("boot: start menu visible")
|
||||
_manager._on_venue_selected("hollywood_building_night")
|
||||
if _frames == 20:
|
||||
if str(_manager.active_scenario_id) != "hollywood_building" \
|
||||
or int(_manager.coordinator.current_phase) != 3:
|
||||
printerr("night variant did not resolve: id=%s phase=%s offset=%.1f" % [
|
||||
str(_manager.active_scenario_id), str(_manager.coordinator.current_phase),
|
||||
float(_manager.coordinator.phase_offset_s)])
|
||||
quit(1)
|
||||
return true
|
||||
print("boot: night variant starts Hollywood in the late phase")
|
||||
_manager._quit_to_menu()
|
||||
if _frames == 30:
|
||||
_manager._on_venue_selected("joke_shop")
|
||||
if _frames == 200:
|
||||
if _frames == 260:
|
||||
if _manager.npcs.size() == 0:
|
||||
printerr("no NPCs spawned after shift start")
|
||||
printerr("no NPCs spawned after shift start (id=%s running=%s elapsed=%.1f phase=%d)" % [
|
||||
str(_manager.active_scenario_id), str(_manager.coordinator.running),
|
||||
float(_manager.coordinator.elapsed_s), int(_manager.coordinator.current_phase)])
|
||||
quit(1)
|
||||
return true
|
||||
print("boot: shift running, npcs=%d" % _manager.npcs.size())
|
||||
_manager._open_pause()
|
||||
if _frames == 210:
|
||||
if _frames == 270:
|
||||
if _manager.pause_overlay == null or not _manager.pause_overlay.visible:
|
||||
printerr("pause overlay did not open")
|
||||
quit(1)
|
||||
return true
|
||||
print("boot: pause overlay visible")
|
||||
_manager._open_settings()
|
||||
if _frames == 220:
|
||||
if _frames == 280:
|
||||
if _manager.settings_panel == null or not _manager.settings_panel.visible:
|
||||
printerr("settings panel did not open")
|
||||
quit(1)
|
||||
@@ -52,7 +66,7 @@ func _process(_delta: float) -> bool:
|
||||
print("boot: settings visible")
|
||||
_manager.settings_panel._on_cancel()
|
||||
_manager._quit_to_menu()
|
||||
if _frames == 230:
|
||||
if _frames == 290:
|
||||
if not _manager.start_menu.visible:
|
||||
printerr("quit-to-menu did not return to the start menu")
|
||||
quit(1)
|
||||
|
||||
@@ -208,15 +208,14 @@ func test_world_loads_the_blender_library_props() -> bool:
|
||||
var a := TestUtils.check(builder.fallback_props.is_empty(),
|
||||
"no primitive prop fallback (fallbacks: %s)" % str(builder.fallback_props))
|
||||
var b := TestUtils.check(builder.library_props.has("street_lamp")
|
||||
and builder.library_props.has("street_tree")
|
||||
and builder.library_props.has("bollard")
|
||||
and builder.library_props.has("palm_tree")
|
||||
and builder.library_props.has("sedan"),
|
||||
and builder.library_props.has("palm_tree"),
|
||||
"replacement and new props come from the library (%d)" % builder.library_props.size())
|
||||
var c := TestUtils.check(root.get_node_or_null("Building/LibraryDoubleDoor") != null
|
||||
and root.get_node_or_null("LibraryMarquee") != null
|
||||
and root.get_node_or_null("ParkedSedan") != null,
|
||||
"library door, marquee and sedan are placed")
|
||||
var c := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null
|
||||
and root.get_node_or_null("OvationHollywood/OvationSign") != null
|
||||
and root.get_node_or_null("WalkOfFame") != null
|
||||
and root.get_node_or_null("Building/FriezeInscription") != null,
|
||||
"the landmark neighbours, opposite block, Walk of Fame and frieze are placed")
|
||||
var d := TestUtils.check(not builder._lamp_bulb_mats.is_empty()
|
||||
and not builder._marquee_emissive.is_empty(),
|
||||
"library lamps/marquee expose emissive materials")
|
||||
@@ -248,7 +247,7 @@ func test_world_facade_windows_light_at_night() -> bool:
|
||||
var root := Node3D.new()
|
||||
var builder := WorldBuilder.new(root)
|
||||
builder.build()
|
||||
var a := TestUtils.check(builder._window_mats.size() >= 8, "window panes built")
|
||||
var a := TestUtils.check(builder._window_mats.size() >= 3, "door transom panes built")
|
||||
builder.set_night_mode(true)
|
||||
var lit: bool = builder._window_mats[0].emission_energy_multiplier > 0.0
|
||||
builder.set_night_mode(false)
|
||||
|
||||
@@ -9,6 +9,7 @@ const Strings := preload("res://src/core/strings.gd")
|
||||
const AccessLoadout := preload("res://src/core/access_loadout.gd")
|
||||
const AccessVerifier := preload("res://src/core/access_verifier.gd")
|
||||
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
||||
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
|
||||
|
||||
func _events(scenario_id: String) -> Array:
|
||||
var scenario: Resource
|
||||
@@ -123,6 +124,25 @@ func test_every_authored_hint_resolves() -> bool:
|
||||
missing.append(key)
|
||||
return TestUtils.check(missing.is_empty(), "all authored hints have strings (missing: %s)" % str(missing))
|
||||
|
||||
func test_hollywood_night_has_routine_traffic() -> bool:
|
||||
var scenario := Factory.hollywood_building()
|
||||
var late_routine := 0
|
||||
for event in scenario.event_pool:
|
||||
if int(event.get("phase", -1)) == Rules.Phase.LATE \
|
||||
and str(event.get("kind", "routine")) == "routine":
|
||||
late_routine += 1
|
||||
var a := TestUtils.check(late_routine >= 3,
|
||||
"the night shift has a late routine crowd (got %d)" % late_routine)
|
||||
var coordinator: ShiftCoordinator = ShiftCoordinator.new()
|
||||
coordinator.setup(scenario, 900.0, 7)
|
||||
coordinator.phase_offset_s = 700.0
|
||||
coordinator.start()
|
||||
var b := TestUtils.check_eq(coordinator.current_phase, Rules.Phase.LATE,
|
||||
"the night offset starts in LATE")
|
||||
var c := TestUtils.check(is_equal_approx(coordinator.schedule_time_s(), 700.0),
|
||||
"the scheduler sees the late time window while the clock starts at zero")
|
||||
return a and b and c
|
||||
|
||||
func test_new_events_have_unique_ids() -> bool:
|
||||
# Event ids only need to be unique within a venue.
|
||||
var problems: Array[String] = []
|
||||
|
||||
@@ -15,20 +15,21 @@ func test_start_menu_lists_all_venues() -> bool:
|
||||
var ids: Array[String] = []
|
||||
for venue in StartMenu.VENUES:
|
||||
ids.append(str(venue["id"]))
|
||||
var a := TestUtils.check_eq(ids.size(), 3, "three venues offered")
|
||||
var b := TestUtils.check(ids.has("hollywood_building"), "Hollywood Blvd present")
|
||||
var c := TestUtils.check(ids.has("joke_shop"), "Joke Shop present")
|
||||
var d := TestUtils.check(ids.has("tv_studio"), "TV Studio present")
|
||||
var a := TestUtils.check_eq(ids.size(), 4, "four venue rows offered")
|
||||
var b := TestUtils.check(ids.has("hollywood_building"), "Hollywood day present")
|
||||
var c := TestUtils.check(ids.has("hollywood_building_night"), "Hollywood night present")
|
||||
var d := TestUtils.check(ids.has("joke_shop"), "Joke Shop present")
|
||||
var e := TestUtils.check(ids.has("tv_studio"), "TV Studio present")
|
||||
menu.free()
|
||||
return a and b and c and d
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_start_menu_emits_selected_venue() -> bool:
|
||||
var menu: Node = StartMenu.new()
|
||||
menu._build()
|
||||
var got := {"id": ""}
|
||||
menu.venue_selected.connect(func(id): got["id"] = id)
|
||||
menu._on_venue_pressed(1)
|
||||
var a := TestUtils.check_eq(got["id"], "joke_shop", "clicking venue 2 selects the Joke Shop")
|
||||
menu._on_venue_pressed(2)
|
||||
var a := TestUtils.check_eq(got["id"], "joke_shop", "clicking the Joke Shop row selects it")
|
||||
var b := TestUtils.check(not menu.visible, "menu closes after a venue is picked")
|
||||
menu.free()
|
||||
return a and b
|
||||
|
||||
@@ -11,6 +11,7 @@ func test_coordinator_snapshot_round_trip() -> bool:
|
||||
var scenario := ScenarioFactory.hollywood_building()
|
||||
var first := ShiftCoordinator.new()
|
||||
first.setup(scenario, 900.0, 42)
|
||||
first.phase_offset_s = 700.0
|
||||
first.start()
|
||||
first.elapsed_s = 123.0
|
||||
first.recorder.start_incident("guest", "focus")
|
||||
@@ -24,13 +25,16 @@ func test_coordinator_snapshot_round_trip() -> bool:
|
||||
second.setup(scenario, 900.0, 42)
|
||||
second.restore(payload)
|
||||
var a := TestUtils.check(is_equal_approx(second.elapsed_s, 123.0), "elapsed time restores")
|
||||
var g := TestUtils.check(is_equal_approx(second.phase_offset_s, 700.0), "phase offset restores")
|
||||
var h := TestUtils.check_eq(second.current_phase, Rules.Phase.LATE,
|
||||
"night offset starts in the late phase")
|
||||
var b := TestUtils.check_eq(second.recorder.to_dict()["open_count"], 0, "incident state restores")
|
||||
var c := TestUtils.check_eq(second.recorder.to_dict()["incidents"].size(), 1,
|
||||
"closed incidents restore")
|
||||
var d := TestUtils.check(is_equal_approx(second.reputation.rating, 75.0), "reputation restores")
|
||||
var e := TestUtils.check(is_equal_approx(second.crowd.mood, 0.4), "crowd mood restores")
|
||||
var f := TestUtils.check(is_equal_approx(second.radio.battery, 70.0), "radio battery restores")
|
||||
return a and b and c and d and e and f
|
||||
return a and b and c and d and e and f and g and h
|
||||
|
||||
func test_open_incident_snapshot_restores_evidence() -> bool:
|
||||
var recorder := IncidentRecorder.new()
|
||||
|
||||
@@ -52,6 +52,52 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool:
|
||||
root.free()
|
||||
return a and b and c and d and e and f and g
|
||||
|
||||
|
||||
func test_hollywood_uses_the_real_landmark_layout() -> bool:
|
||||
var root := Node3D.new()
|
||||
var builder := WorldBuilder.new(root)
|
||||
builder.build()
|
||||
var columns := root.find_child("IonicColonnade", true, false)
|
||||
var steps := root.find_child("Steps", true, false)
|
||||
var a := TestUtils.check(columns != null and columns.get_child_count() >= 12,
|
||||
"six Ionic columns with bases and capitals are built")
|
||||
var b := TestUtils.check(steps != null and steps.get_child_count() >= 8,
|
||||
"eight stone steps are built")
|
||||
var frieze := root.find_child("FriezeInscription", true, false) as Label3D
|
||||
var c := TestUtils.check(frieze != null and frieze.text.contains("FREEMASONRY")
|
||||
and frieze.text.contains("HEARTS OF MEN"), "the Masonic parapet motto is present")
|
||||
var banners := root.find_child("EventBanners", true, false)
|
||||
var d := TestUtils.check(banners != null and banners.get_child_count() >= 9,
|
||||
"three red event banners with their trim are present")
|
||||
var e := TestUtils.check(root.find_child("WalkOfFame", true, false) != null,
|
||||
"the Hollywood Walk of Fame strip is built")
|
||||
var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null
|
||||
and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null,
|
||||
"the El Capitan Theatre and east retail neighbours are built")
|
||||
var g := TestUtils.check(root.find_child("OvationHollywood", true, false) != null,
|
||||
"the opposite Ovation Hollywood block is built")
|
||||
var placements: Array = builder.spec.get("prop_placements", [])
|
||||
var h := TestUtils.check(not placements.is_empty()
|
||||
and WorldBuilder.placement_problems(placements).is_empty(),
|
||||
"the landmark prop placements keep the wide steps clear")
|
||||
root.free()
|
||||
return a and b and c and d and e and f and g and h
|
||||
|
||||
|
||||
func test_hollywood_geometry_stays_inside_the_map_budget() -> bool:
|
||||
var root := Node3D.new()
|
||||
var builder := WorldBuilder.new(root)
|
||||
builder.build()
|
||||
var triangles := builder.world_triangle_count()
|
||||
var meshes := builder.world_mesh_count()
|
||||
print("hollywood map: %d triangles across %d meshes" % [triangles, meshes])
|
||||
var a := TestUtils.check(triangles < 150000,
|
||||
"landmark triangle budget stays under 150k (got %d)" % triangles)
|
||||
var b := TestUtils.check(meshes < 600,
|
||||
"landmark draw-call count stays under 600 meshes (got %d)" % meshes)
|
||||
root.free()
|
||||
return a and b
|
||||
|
||||
func test_navigation_mesh_matches_each_venue_bounds() -> bool:
|
||||
var problems: Array[String] = []
|
||||
for id in ["hollywood_building", "joke_shop", "tv_studio"]:
|
||||
|
||||
Reference in new issue
Block a user