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.
No files matched your search
@@ -4,6 +4,38 @@ All notable user-facing changes to Gig Line. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
|
||||
Semantic Versioning.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- A stylized reconstruction of the real El Capitan Entertainment Centre block at
|
||||
6840 Hollywood Boulevard: six Ionic columns, eight stone steps, the Masonic
|
||||
parapet motto and medallions, red event banners, central and side entrances,
|
||||
the El Capitan Theatre and retail neighbours, the Ovation Hollywood arcade,
|
||||
the Walk of Fame and four-lane street markings.
|
||||
- Real-site reference notes and AI art-direction frames for the Hollywood block.
|
||||
- Completed the UI string migration: start menu, settings, pause, Help, HUD,
|
||||
radio and shift-review labels now come from the English string table.
|
||||
- Separate **Hollywood Boulevard — Day** and **Hollywood Boulevard — Night**
|
||||
rows in the venue menu. Night starts directly in the late phase and keeps the
|
||||
full shift after dark.
|
||||
|
||||
### Changed
|
||||
|
||||
- The default Hollywood venue is no longer a generic brick entrance; the venue
|
||||
name and policy copy now identify the El Capitan Entertainment Centre.
|
||||
- Night lighting on the landmark uses low-energy practical washes rather than
|
||||
the street-lamp brightness, so the facade reads without blowing out.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The Ovation Hollywood sign faces the boulevard instead of rendering mirrored.
|
||||
- Local night lights use linear attenuation instead of constant falloff, so
|
||||
sconces and street lamps pool on the ground and stone rather than washing the
|
||||
whole facade flat and yellow.
|
||||
- The captured HUD clears stale objectives and target prompts between staged
|
||||
screenshots.
|
||||
|
||||
## [0.9.1] — 2026-11-02
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -48,6 +48,17 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
- **Campaign/v0.8.0:** `Campaign` owns Hollywood → Joke Shop → TV Studio progression. `campaign.json`, `shift_results.json`, `settings.json` and versioned `shift_state.json` persist separately; pause saves a checkpoint, review Continue advances, Settings resets with confirmation. Difficulty keys are `easy/normal/hard` (`ai_difficulty`); scripted QA bypasses tutorial unless `--tutorial` is passed. Help is searchable and generated from `InputBindings.REMAPPABLE`.
|
||||
- **Tutorial (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is the pure state machine, `TutorialOverlay` is the non-blocking card, and `GameManager` wires it with a safe visitor, suppressed spawns, persisted completion/skip and Settings replay. Scripted test/capture runs bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers completion/replay/skip.
|
||||
- **Priority change (2026-11-02):** the map optimization previously planned for 0.9.5 is now the active milestone; 0.9.2 UI, 0.9.3 NPC detail, 0.9.4 textures and the 0.9.6 integration/0.10.0 release work are postponed. A pre-build NPC revisit gate (final map/lighting portraits, gear fit, P0/P1 fixes) must pass before any build/tag.
|
||||
- **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan
|
||||
Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its
|
||||
reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test
|
||||
(`test_hollywood_geometry_stays_inside_the_map_budget`) currently measures 58,884 triangles
|
||||
across 409 meshes. The facade uses the shared smooth limestone (`stone_steps`) texture; the
|
||||
noisier granite pick was removed.
|
||||
- **Light falloff gotcha (2026-11-02):** in Godot 4.7 `omni_attenuation = 0.0` is constant
|
||||
falloff, not physical falloff. It made the night facade a flat yellow wall and the street
|
||||
lamps wash the set. All local lights now use `1.0`; the facade was retuned with 0.50/0.28
|
||||
washes, 1.3 sconces (3.0 for the Joke Shop key lantern) and 2.2 street lamps. Flashlight
|
||||
spot attenuation is also `1.0`. Do not reintroduce `0.0` as a "modern" branch.
|
||||
- **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README
|
||||
only in directories containing tracked files, verifies local links and requires
|
||||
every tracked image in a folder to be linked from its README. Gitignored
|
||||
@@ -650,7 +661,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
- Joke Shop `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
||||
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
|
||||
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
|
||||
- Headless suite is **229/229**; the windowed smokes, screenshot compare and judge pass. Remaining visual follow-ups are in `TODO.md`.
|
||||
- Headless suite is **253/253**; the windowed smokes, screenshot compare and judge pass. Remaining visual follow-ups are in `TODO.md`.
|
||||
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
|
||||
- GDScript inference trap that broke the main scene: a `var` typed as a base class
|
||||
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Premise
|
||||
|
||||
You are a private security guard working the entrance of a fictional Hollywood Boulevard building. You keep the steps and doors clear, check who is allowed in, and deal with tourists, performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people in mental-health crisis, and rare determined threats.
|
||||
You are a private security guard working the entrance of a stylized reconstruction of the real El Capitan Entertainment Centre on Hollywood Boulevard. You keep the steps and doors clear, check who is allowed in, and deal with tourists, performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people in mental-health crisis, and rare determined threats.
|
||||
|
||||
The game is about **judgment, communication, access verification, and proportionate escalation**, not about attacking everyone who approaches.
|
||||
|
||||
@@ -96,9 +96,9 @@ A poor shift involves excessive force, confronting innocent people, ignoring a r
|
||||
|
||||
## Venues
|
||||
|
||||
### Hollywood Boulevard Building
|
||||
### El Capitan Entertainment Centre (6840 Hollywood Blvd)
|
||||
|
||||
The original scenario: a building entrance with steps, sidewalk, street lamps, and a restricted door. Tourists, performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people in crisis, and rare violent threats approach from the boulevard.
|
||||
The original scenario: the historic Masonic Temple / El Capitan Entertainment Centre, with six Ionic columns, eight stone steps, the Hollywood Walk of Fame sidewalk, neighbouring theatre and retail frontage, and the four-lane boulevard. Tourists, performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people in crisis, and rare violent threats approach from the street.
|
||||
|
||||
Authorized people are recognized or present a valid wristband. Wristbands vary by color, pattern, date, and event marking, and may be valid, expired, counterfeit, or missing.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ The central design goal is simple:
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
@@ -52,14 +52,15 @@ precision walk, the Joke Shop alley, the TV Studio arrival, and the prop lineup.
|
||||
If you are here to develop or test locally instead of downloading, jump to
|
||||
[Setup and Run](#setup-and-run) below.
|
||||
|
||||
The project is under active development. The design and architecture are documented, and a playable prototype vertical slice exists (Godot 4 + GDScript): a procedural Hollywood Boulevard entrance, a first-person guard, simulated NPCs with intents and reactions, the six-level escalation model, access inspection and admission decisions, radio calls with credibility checks, reputation, crowd mood, and an end-of-shift review.
|
||||
The project is under active development. The design and architecture are documented, and a playable prototype vertical slice exists (Godot 4 + GDScript): a stylized El Capitan Entertainment Centre block, a first-person guard, simulated NPCs with intents and reactions, the six-level escalation model, access inspection and admission decisions, radio calls with credibility checks, reputation, crowd mood, and an end-of-shift review.
|
||||
|
||||
## Game Overview
|
||||
|
||||
A typical shift lasts 10–15 minutes: watch the entrance and sidewalk, judge why
|
||||
each person is approaching, verify access with recognition, wristbands, IDs,
|
||||
tickets or credentials, and escalate only when necessary. The game rewards calm
|
||||
resolution over force. The full mechanics — the three venues, the six-level
|
||||
resolution over force. The venue menu splits Hollywood Boulevard into Day and
|
||||
Night rows; the full mechanics — the three venues, the six-level
|
||||
escalation model, and the core systems — are in
|
||||
[`docs/gameplay.md`](docs/gameplay.md); the design rationale lives in
|
||||
[`PLAN.md`](PLAN.md).
|
||||
@@ -108,6 +109,7 @@ Full setup, every run/test/smoke command, and the controls table are in
|
||||
- [`docs/README.md`](docs/README.md) — Documentation index and visual previews.
|
||||
- [`docs/architecture.md`](docs/architecture.md) — arc42-style architecture documentation.
|
||||
- [`docs/gameplay.md`](docs/gameplay.md) — Gameplay systems, venues, and the escalation model.
|
||||
- [`docs/hollywood-boulevard-map.md`](docs/hollywood-boulevard-map.md) — Real-site references, game mapping and limitations for the Hollywood venue.
|
||||
- [`docs/tutorial.md`](docs/tutorial.md) — First-shift tutorial beats and rules.
|
||||
- [`docs/asset-tooling.md`](docs/asset-tooling.md) — Asset pipelines, model tools, and screenshot gates.
|
||||
- [`docs/getting-started.md`](docs/getting-started.md) — Setup, run/test commands, and controls.
|
||||
|
||||
@@ -54,13 +54,15 @@ Goal: streamline the maps, refactor geometry to match the images in `docs/screen
|
||||
|
||||
### Map 1 — Hollywood Boulevard
|
||||
|
||||
- [ ] Audit and optimize geometry for the Hollywood Boulevard map.
|
||||
- [ ] Reseach geometry for the real Hollywood Boulevard location "6840 Hollywood Blvd, Hollywood, CA 90028, United States" and create an accurate representation of the building, neighboring structures and streets, building facades, buildings across the street and other relevant elements on the map.
|
||||
- [ ] Research current imagery and references for Hollywood Boulevard to ensure the map reflects the real-world location accurately.
|
||||
- [ ] Generate more accurate images using the python AI tools for reference and alignment with the real-world location, save to `docs/screens/concepts/hollywood_boulevard/`.
|
||||
- [ ] Verify alignment with other `docs/screens/concepts` images of the Hollywood Boulevard map.
|
||||
- [ ] Remove unnecessary or hidden geometry to improve performance.
|
||||
- [ ] Ensure texture and material consistency across the map.
|
||||
- [x] Audit and optimize geometry for the Hollywood Boulevard map.
|
||||
- [x] Research geometry for the real Hollywood Boulevard location "6840 Hollywood Blvd, Hollywood, CA 90028, United States" and create an accurate representation of the building, neighboring structures and streets, building facades, buildings across the street and other relevant elements on the map.
|
||||
- [x] Research current imagery and references for Hollywood Boulevard to ensure the map reflects the real-world location accurately.
|
||||
- [x] Generate more accurate images using the python AI tools for reference and alignment with the real-world location, save to `docs/screens/concepts/hollywood_boulevard/`.
|
||||
- [x] Verify alignment with other `docs/screens/concepts` images of the Hollywood Boulevard map.
|
||||
- [x] Remove unnecessary or hidden geometry to improve performance.
|
||||
- [x] Ensure texture and material consistency across the map.
|
||||
|
||||
Completed: the Hollywood venue is rebuilt from the real 6840 Hollywood Blvd references (six-column Masonic/El Capitan facade, eight steps, neighbours, Ovation arcade, Walk of Fame, four-lane street). The map-budget test measures 58,884 triangles across 409 meshes; Day/Night menu variants start in `EARLY`/`LATE`; the realism judge gaps are 0.148 (day) and 0.278 (late). See `docs/hollywood-boulevard-map.md`.
|
||||
|
||||
### Map 2 — Joke Shop
|
||||
|
||||
@@ -74,12 +76,14 @@ Goal: streamline the maps, refactor geometry to match the images in `docs/screen
|
||||
|
||||
### Scenery Change (Day/Night) for Hollywood Boulevard
|
||||
|
||||
- [ ] Capture reference images for Hollywood Boulevard during the day and night.
|
||||
- [ ] Adjust lighting and environmental settings to match the day/night cycle.
|
||||
- [ ] Verify that all textures and materials respond correctly to the lighting changes.
|
||||
- [ ] Ensure that any dynamic elements (e.g., streetlights, signs) behave appropriately for the time of day.
|
||||
- [ ] Test performance and visual consistency for both day and night scenarios.
|
||||
- [ ] Offer the day map and the night map as distinct scenarios in the game menu when selecting a location.
|
||||
- [x] Capture reference images for Hollywood Boulevard during the day and night.
|
||||
- [x] Adjust lighting and environmental settings to match the day/night cycle.
|
||||
- [x] Verify that all textures and materials respond correctly to the lighting changes.
|
||||
- [x] Ensure that any dynamic elements (e.g., streetlights, signs) behave appropriately for the time of day.
|
||||
- [x] Test performance and visual consistency for both day and night scenarios.
|
||||
- [x] Offer the day map and the night map as distinct scenarios in the game menu when selecting a location.
|
||||
|
||||
Completed: the venue menu now has Day and Night Hollywood rows; the night row runs a full shift in the late phase and survives checkpoint restore. Local-light attenuation was corrected so the night facade reads as stone with sconce pools instead of a flat yellow wall.
|
||||
|
||||
**Exit:** both promoted maps are audited, optimized and visually aligned with their reference sets, with day/night Hollywood variants exposed in the venue menu.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -187,6 +187,10 @@
|
||||
"review.campaign_next": "Campaign shift %d complete · next: %s",
|
||||
"venue.hollywood.label": "Hollywood Boulevard",
|
||||
"venue.hollywood.blurb": "Building entrance, tourists and late-night trouble.",
|
||||
"venue.hollywood_day.label": "Hollywood Boulevard — Day",
|
||||
"venue.hollywood_day.blurb": "El Capitan entrance in daylight: tourists, performers and routine traffic.",
|
||||
"venue.hollywood_night.label": "Hollywood Boulevard — Night",
|
||||
"venue.hollywood_night.blurb": "The same entrance after dark: banners lit, late crowd, harder to read.",
|
||||
"venue.joke.label": "The Joke Shop",
|
||||
"venue.joke.blurb": "Guest list, IDs and wand screening.",
|
||||
"venue.studio.label": "Late Night TV Studio",
|
||||
|
||||
@@ -18,6 +18,11 @@ display_name = "Tourist"
|
||||
temperament = 0.1
|
||||
base_compliance = 0.9
|
||||
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
||||
context_lines = {
|
||||
"deescalation": ["Okay, I'll move to the sidewalk.", "Sorry, I'll put the camera away."],
|
||||
"recognition": ["You remember me from the tour, right?"],
|
||||
"refusal": ["I just want a photo, I'm not staying.", "I didn't know this entrance was restricted."]
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_qe635"]
|
||||
script = ExtResource("2_7qk8j")
|
||||
@@ -27,6 +32,10 @@ npc_type = 3
|
||||
temperament = 0.2
|
||||
base_compliance = 0.85
|
||||
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
||||
context_lines = {
|
||||
"deescalation": ["Oh, I understand. I'll check with the box office."],
|
||||
"refusal": ["I'm sure the event is here. Let me check my phone."]
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_u5nrp"]
|
||||
script = ExtResource("2_7qk8j")
|
||||
@@ -285,8 +294,8 @@ default_reactions = {
|
||||
[resource]
|
||||
script = ExtResource("3_lvgx2")
|
||||
id = "hollywood_building"
|
||||
display_name = "Hollywood Boulevard Building"
|
||||
description = "Steps and entrance of a private building off Hollywood Boulevard."
|
||||
display_name = "El Capitan Entertainment Centre"
|
||||
description = "The historic 1921 Masonic Temple at 6840 Hollywood Boulevard, now the El Capitan Entertainment Centre."
|
||||
access_policy = SubResource("Resource_xf4wh")
|
||||
equipment = Array[String](["wristband_policy", "flashlight", "radio"])
|
||||
locations = {
|
||||
@@ -371,6 +380,33 @@ event_pool = Array[Dictionary]([{
|
||||
"time_range_s": Vector2(300, 800)
|
||||
}, {
|
||||
"base_weight": 1.0,
|
||||
"id": "late_tourist",
|
||||
"intent": "photo",
|
||||
"kind": "routine",
|
||||
"npc_type": 0,
|
||||
"phase": 3,
|
||||
"probability": 0.22,
|
||||
"time_range_s": Vector2(700, 900)
|
||||
}, {
|
||||
"base_weight": 1.0,
|
||||
"id": "late_guest",
|
||||
"intent": "enter",
|
||||
"kind": "routine",
|
||||
"npc_type": 9,
|
||||
"phase": 3,
|
||||
"probability": 0.2,
|
||||
"time_range_s": Vector2(700, 900)
|
||||
}, {
|
||||
"base_weight": 1.0,
|
||||
"id": "late_trespasser",
|
||||
"intent": "trespass",
|
||||
"kind": "routine",
|
||||
"npc_type": 5,
|
||||
"phase": 3,
|
||||
"probability": 0.14,
|
||||
"time_range_s": Vector2(700, 900)
|
||||
}, {
|
||||
"base_weight": 1.0,
|
||||
"hint": "hint.body_camera_reason",
|
||||
"id": "building_filming_boundary",
|
||||
"intent": "film",
|
||||
|
||||
@@ -22,6 +22,11 @@ display_name = "Tourist"
|
||||
temperament = 0.1
|
||||
base_compliance = 0.9
|
||||
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
||||
context_lines = {
|
||||
"deescalation": ["Okay, I'll move to the sidewalk.", "Sorry, I'll put the camera away."],
|
||||
"recognition": ["You remember me from the tour, right?"],
|
||||
"refusal": ["I just want a photo, I'm not staying.", "I didn't know this entrance was restricted."]
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_r873e"]
|
||||
script = ExtResource("2_l60ad")
|
||||
@@ -31,6 +36,10 @@ npc_type = 3
|
||||
temperament = 0.2
|
||||
base_compliance = 0.85
|
||||
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
||||
context_lines = {
|
||||
"deescalation": ["Oh, I understand. I'll check with the box office."],
|
||||
"refusal": ["I'm sure the event is here. Let me check my phone."]
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_3xy2u"]
|
||||
script = ExtResource("2_l60ad")
|
||||
|
||||
@@ -22,6 +22,11 @@ display_name = "Tourist"
|
||||
temperament = 0.1
|
||||
base_compliance = 0.9
|
||||
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
||||
context_lines = {
|
||||
"deescalation": ["Okay, I'll move to the sidewalk.", "Sorry, I'll put the camera away."],
|
||||
"recognition": ["You remember me from the tour, right?"],
|
||||
"refusal": ["I just want a photo, I'm not staying.", "I didn't know this entrance was restricted."]
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_lmn2c"]
|
||||
script = ExtResource("2_g7rkt")
|
||||
@@ -31,6 +36,10 @@ npc_type = 3
|
||||
temperament = 0.2
|
||||
base_compliance = 0.85
|
||||
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
||||
context_lines = {
|
||||
"deescalation": ["Oh, I understand. I'll check with the box office."],
|
||||
"refusal": ["I'm sure the event is here. Let me check my phone."]
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_if2cj"]
|
||||
script = ExtResource("2_g7rkt")
|
||||
|
||||
@@ -154,6 +154,7 @@ reputation, crowd reaction and shift review read it rather than resolving gamepl
|
||||
2. `GameManager` loads the matching `VenueScenario` and keeps the guard/UI state.
|
||||
3. `WorldBuilder` rebuilds only `world_root` from `VENUE_SPECS`, including geometry, props and the navigation mesh.
|
||||
4. Shift state, NPCs and incidents are reset for the new venue; phase lighting and the door state are reapplied.
|
||||
5. The menu's **Hollywood Night** row resolves to `hollywood_building`, then sets `ShiftCoordinator.phase_offset_s` so elapsed time starts at 0 but phase lookup starts at `LATE`. The offset is part of the checkpoint snapshot, so a resumed night shift stays at night.
|
||||
|
||||
### Asset fallback
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Implemented so far:
|
||||
|
||||
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
|
||||
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
|
||||
- Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
||||
- Headless unit tests: **250 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
|
||||
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
||||
- Headless unit tests: **253 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
|
||||
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
|
||||
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
|
||||
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
|
||||
@@ -46,6 +46,8 @@ Implemented so far:
|
||||
- Tutorial pass (0.8.0 work): the eight-beat first shift is wired through a non-blocking prompt overlay, a safe compliant visitor, real wheel/inspection/warning/radio panels, persisted completion/skip and a Settings replay action. Scripted tests/captures bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers the full flow.
|
||||
- Vision judge (v0.7.2): discovers the cheapest current OpenRouter image+JSON model, reports the model/fallbacks used, and clears the full screen-design judge (13/13 match, 0 diff).
|
||||
- Desktop HQ (v0.7.1): optional Forward+ Windows/Linux zip with volumetric fog, SDFGI and a volumetric flashlight; the default Compatibility/Web builds and headless tests gate the effect off. A/B captures are generated locally under `docs/screens/realism/` (gitignored).
|
||||
- Hollywood landmark map (map optimization): the default venue is now a stylized reconstruction of 6840 Hollywood Blvd (the 1921 Masonic Temple / El Capitan Entertainment Centre) with a six-column Ionic portico, eight stone steps, Masonic parapet motto and medallions, three red event banners, central and side entrances, the El Capitan Theatre and east retail neighbours, the Ovation Hollywood arcade opposite, the Walk of Fame terrazzo and a four-lane boulevard with red kerb paint. The facade uses the shared limestone/stone material; the whole venue measures **58,884 triangles across 409 meshes** in the map-budget test. The venue menu exposes separate **Day** and **Night** Hollywood rows; the night row resolves to the same scenario with a late-phase offset, so its whole shift stays after dark. Real-site references and the geometry mapping are in [`hollywood-boulevard-map.md`](hollywood-boulevard-map.md).
|
||||
- Day/night scenery variants (map optimization): the Hollywood row in the venue menu is split into Day and Night entries, with localized names/blurbs. The night entry uses `ShiftCoordinator.phase_offset_s` to start a full-length shift in `LATE` while the clock still starts at 00:00; the scheduler reads the offset time so late-night routine traffic and the late violent-threat event still fire, and the offset round-trips through checkpoints. Three `LATE` routine events keep the night street populated. Local lights were retuned from constant falloff to linear attenuation so sconces pool instead of flooding the stone.
|
||||
- Venue geometry (v0.7.0): the Joke Shop now builds as a narrow brick alley with a warm wall lantern, shutters, dumpster/AC clutter and no skyline; the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes.
|
||||
- Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report.
|
||||
|
||||
|
||||
@@ -20,9 +20,16 @@ The first shift starts with an in-game tutorial covering movement/look, focus cy
|
||||
|
||||
## Venues
|
||||
|
||||
### Hollywood Boulevard Building
|
||||
### El Capitan Entertainment Centre (6840 Hollywood Blvd)
|
||||
|
||||
The original scenario places the guard at the top of the steps outside a fictional building. Tourists, street performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people experiencing a mental-health crisis, and rare violent threats may approach the entrance.
|
||||
The map is a stylized reconstruction of the real building and block; the
|
||||
reference notes and geometry mapping are in
|
||||
[`hollywood-boulevard-map.md`](hollywood-boulevard-map.md). The venue menu
|
||||
offers the same map as **Hollywood Boulevard — Day** and **Hollywood Boulevard
|
||||
— Night**; the night variant starts directly in the late phase instead of
|
||||
waiting for the shift clock.
|
||||
|
||||
The original scenario places the guard at the top of the eight stone steps outside the historic Masonic Temple / El Capitan Entertainment Centre on Hollywood Boulevard. Tourists, street performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people experiencing a mental-health crisis, and rare violent threats may approach the entrance.
|
||||
|
||||
Authorized people must either be recognized by the guard or present a valid wristband. Wristbands can have different colors, patterns, dates, or event markings. They may be valid, expired, counterfeit, or missing.
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Hollywood Boulevard map reference
|
||||
|
||||
The Hollywood venue is a stylized reconstruction of the real block around the
|
||||
El Capitan Entertainment Centre. It is a game space, not a survey-accurate
|
||||
model: the landmark silhouette, street wall and approach read from the player's
|
||||
eye height, while side streets and interiors are simplified.
|
||||
|
||||
## Real site
|
||||
|
||||
- **Address:** 6840 Hollywood Boulevard, Hollywood, Los Angeles, CA 90028.
|
||||
- **Coordinates:** 34.10118 N, 118.34009 W (OSM way `424024147`).
|
||||
- **Building:** the 1921 Hollywood Masonic Temple, designed by John C. Austin in
|
||||
the Classical Revival style; now the El Capitan Entertainment Centre and the
|
||||
home of *Jimmy Kimmel Live!*.
|
||||
- **Facade:** a light stone/render Neoclassical front with six oversized Ionic
|
||||
columns, a recessed main entrance reached by eight stone steps, ornamental
|
||||
iron door grilles, a continuous crosscut-grille transom, two street-level side
|
||||
entrances, and a parapet with the Masonic motto and circular medallions.
|
||||
- **Neighbours:** the El Capitan Theatre (6838) immediately west, a modern
|
||||
retail block east, and the Ovation Hollywood frontage (6801) across the
|
||||
four-lane boulevard.
|
||||
- **Sidewalk:** the Hollywood Walk of Fame terrazzo strip with brass stars runs
|
||||
along the kerb; the kerb is painted red in the reference photographs.
|
||||
|
||||
## Game mapping
|
||||
|
||||
| Real feature | In-game representation |
|
||||
| --- | --- |
|
||||
| 29.6 m wide, 14.5 m high facade | `VENUE_SPECS["hollywood_building"]`: `facade_width` 29.6, `facade_height` 14.5, front at `z = -4` |
|
||||
| Eight stone steps | `stair_count` 8, `stair_height` 0.10, `stair_tread` 0.38, 12 m wide |
|
||||
| Six Ionic columns | `IonicColonnade` at x = -12.4 … 12.4, 4.96 m bay spacing |
|
||||
| Recessed main entrance + two side doors | central 4.2 m door and 2.5 m side doors with transoms |
|
||||
| Red event banners | three stylized `EL / CAPITAN / LIVE` banners in the central bays |
|
||||
| Masonic motto + medallions | `FriezeInscription` label and two stone discs |
|
||||
| El Capitan Theatre | `NeighbourBlocks/ElCapitanTheatre` with marquee, blade sign and practical light |
|
||||
| Retail block east | `NeighbourBlocks/EastRetailBlock` with glazed shopfront |
|
||||
| Ovation Hollywood | `OvationHollywood` arcade with seven arched openings, sign and south sidewalk |
|
||||
| Walk of Fame | `WalkOfFame` dark terrazzo band with ten stars |
|
||||
| Four-lane boulevard | double-yellow centre, broken white lane lines, red kerb paint and crosswalk |
|
||||
|
||||
The generated AI references live in
|
||||
[`docs/screens/concepts/hollywood_boulevard/`](screens/concepts/hollywood_boulevard/README.md).
|
||||
They are art direction for lighting, material scale and composition; they are
|
||||
not texture or geometry sources.
|
||||
|
||||
## Deliberate simplifications
|
||||
|
||||
- Show branding is replaced by the venue-neutral `EL / CAPITAN / LIVE` banner
|
||||
text. The building name and address are retained.
|
||||
- The opposite Ovation frontage is represented as a single arcade block rather
|
||||
than its full mall plan. Interiors, the Metro entrance and side streets are
|
||||
out of playable bounds.
|
||||
- The AI concept frames are 1280×720 references. The in-game geometry is built
|
||||
from boxes, cylinders and a small number of custom arch/star meshes so it
|
||||
stays inside the Compatibility/Web budget.
|
||||
- The venue menu offers Day and Night Hollywood rows. Night is the same
|
||||
geometry and scenario with a late-phase offset, not a second baked map; the
|
||||
phase still drives sun, sky, fog, windows, signage and practical lights.
|
||||
- The landmark facade uses the shared smooth limestone/stone texture rather
|
||||
than the noisier granite panel tile. The whole venue measures **58,884
|
||||
triangles across 409 meshes** in `test_hollywood_geometry_stays_inside_the_map_budget`,
|
||||
well inside the 150k/600 budget.
|
||||
- Local lights use linear attenuation (`omni_attenuation = 1.0`). Godot treats
|
||||
`0.0` as constant falloff, which previously turned the night facade into a
|
||||
flat yellow wall; the sconces now pool light instead.
|
||||
|
||||
## References
|
||||
|
||||
- [Hollywood Masonic Temple — Wikipedia](https://en.wikipedia.org/wiki/Hollywood_Masonic_Temple)
|
||||
- [El Capitan Entertainment Centre — OpenStreetMap way 424024147](https://www.openstreetmap.org/way/424024147)
|
||||
- [El Capitan Theatre — OpenStreetMap way 424024148](https://www.openstreetmap.org/way/424024148)
|
||||
- [Ovation Hollywood — OpenStreetMap way 151485232](https://www.openstreetmap.org/way/151485232)
|
||||
- [Hollywood Masonic Temple facade — Wikimedia Commons](https://commons.wikimedia.org/wiki/File:El_Capitan_Theater_frontal_fachade-The_Masonic_Temple.jpg)
|
||||
- [Hollywood Masonic Temple and El Capitan Theatre, 2013 — Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Hollywood_Masonic_Temple_2013-10-13_10-02-25.jpg)
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
godot --headless --path . --script res://src/test/test_runner.gd
|
||||
godot --path . --script res://src/test/capture_realism.gd -- --out=/tmp/gig-line-hollywood --stages=day,evening,late
|
||||
python3 tools/check_docs.py
|
||||
```
|
||||
@@ -14,3 +14,4 @@ Visual references and generated QA evidence for Gig Line.
|
||||
| Venue | <img src="designs/venue_overview.png" width="320" alt="Venue design"> | <img src="../screenshots/01-venue-overview.png" width="320" alt="Venue capture"> |
|
||||
| Joke Shop | <img src="designs/joke_shop_queue.png" width="320" alt="Joke Shop design"> | <img src="../screenshots/12-joke-shop.png" width="320" alt="Joke Shop capture"> |
|
||||
| TV Studio | <img src="designs/studio_arrival.png" width="320" alt="Studio design"> | <img src="../screenshots/13-studio-arrival.png" width="320" alt="Studio capture"> |
|
||||
| El Capitan | <img src="concepts/hollywood_boulevard/hollywood_boulevard_entrance_day.png" width="320" alt="El Capitan reference"> | <img src="../screenshots/01-venue-overview.png" width="320" alt="El Capitan capture"> |
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
These are AI-generated example frames (`python3 tools/scene_concepts.py generate --group scene`, `openai/gpt-5-image-mini`, 1280×720) for the realism pass. They are art direction, not assets. `tools/scene_concepts.py realism` pairs them with the in-game shots from `src/test/capture_realism.gd` (see `SHOTS` in the tool). New generations never overwrite a file here; they are saved as `<name>-vN.png`.
|
||||
|
||||
## Real-world Hollywood block
|
||||
|
||||
The map-optimization references for the El Capitan Entertainment Centre block
|
||||
(6840 Hollywood Blvd) are in
|
||||
[`hollywood_boulevard/README.md`](hollywood_boulevard/README.md). The real-site
|
||||
facts and game mapping are documented in
|
||||
[`../../hollywood-boulevard-map.md`](../../hollywood-boulevard-map.md).
|
||||
|
||||
## Concept list
|
||||
|
||||
### Hollywood Entrance
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Hollywood Boulevard references
|
||||
|
||||
AI-generated art-direction frames for the real El Capitan Entertainment Centre
|
||||
block at 6840 Hollywood Boulevard. They are generated with
|
||||
`tools/scene_concepts.py generate --group scene --only ...` and are not shipped
|
||||
game textures. The geometry mapping and source links are in
|
||||
[`../../../hollywood-boulevard-map.md`](../../../hollywood-boulevard-map.md).
|
||||
|
||||
## Entrance — day
|
||||
|
||||

|
||||
|
||||
## Entrance — night
|
||||
|
||||

|
||||
|
||||
## Across the boulevard
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 731 KiB After Width: | Height: | Size: 729 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 553 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 680 KiB |
|
Before Width: | Height: | Size: 667 KiB After Width: | Height: | Size: 665 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 756 KiB After Width: | Height: | Size: 756 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 744 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 628 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 837 KiB |
@@ -42,3 +42,4 @@
|
||||
40. Help remains readable with the largest HUD font scale, and CI/local verification use the same Godot 4.7.2 baseline.
|
||||
41. UI copy: inspection/wand use "A = admit · D = refuse entry", review returns with "Return to menu", and application exit reads "Quit game".
|
||||
42. Button states remain readable on hover/focus/pressed; settings Apply/Cancel and radio normal-send/emergency-hold behave as labelled.
|
||||
43. The venue menu offers Hollywood Boulevard — Day and — Night; either starts the same map, and Night starts in the late phase with a full shift clock.
|
||||
@@ -30,6 +30,7 @@ Status: audit in progress for the 0.9.2 UI polish patch.
|
||||
- **Continue campaign** — advance to the next venue.
|
||||
- **Return to menu** — leave the review to the venue menu (it does not start a new shift by itself).
|
||||
- **Send request** — non-emergency assistance; **Send emergency** — emergency call.
|
||||
- **Hollywood Boulevard — Day / Night** — the two scenery variants in the venue menu. Use an em dash and title case for the variant; do not call them “maps” in player-facing copy.
|
||||
|
||||
## Button labels and states
|
||||
|
||||
|
||||
@@ -312,8 +312,8 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
static func hollywood_building() -> VenueScenario:
|
||||
var scenario := VenueScenario.new()
|
||||
scenario.id = "hollywood_building"
|
||||
scenario.display_name = "Hollywood Boulevard Building"
|
||||
scenario.description = "Steps and entrance of a private building off Hollywood Boulevard."
|
||||
scenario.display_name = "El Capitan Entertainment Centre"
|
||||
scenario.description = "The historic 1921 Masonic Temple at 6840 Hollywood Boulevard, now the El Capitan Entertainment Centre."
|
||||
scenario.shift_duration_s = 900.0
|
||||
|
||||
var policy := AccessPolicy.new()
|
||||
@@ -352,6 +352,9 @@ static func hollywood_building() -> VenueScenario:
|
||||
_building_routine("staff_entry", Rules.NpcType.AUTHORIZED_STAFF, "enter", Vector2(0, 600), 0.35),
|
||||
_building_routine("guest_entry", Rules.NpcType.AUTHORIZED_GUEST, "enter", Vector2(0, 900), 0.25),
|
||||
_building_routine("performer_show", Rules.NpcType.PERFORMER, "perform", Vector2(300, 800), 0.24),
|
||||
_late_routine("late_tourist", Rules.NpcType.TOURIST, "photo", 0.22),
|
||||
_late_routine("late_guest", Rules.NpcType.AUTHORIZED_GUEST, "enter", 0.20),
|
||||
_late_routine("late_trespasser", Rules.NpcType.TRESPASSER, "trespass", 0.14),
|
||||
{
|
||||
"id": "building_filming_boundary",
|
||||
"phase": Rules.Phase.MIDDAY,
|
||||
@@ -710,6 +713,13 @@ static func profile_dictionary() -> Dictionary:
|
||||
|
||||
# --- Event builders ----------------------------------------------------------------
|
||||
|
||||
## Late-phase routine traffic. The range-based helper would classify a 700 s
|
||||
## start as evening, so the phase is set explicitly.
|
||||
static func _late_routine(id: String, npc_type: int, intent: String, prob: float) -> Dictionary:
|
||||
var event := _building_routine(id, npc_type, intent, Vector2(700, 900), prob)
|
||||
event["phase"] = Rules.Phase.LATE
|
||||
return event
|
||||
|
||||
static func _building_routine(id: String, npc_type: int, intent: String, time_range: Vector2, prob: float) -> Dictionary:
|
||||
return {
|
||||
"id": id, "phase": _phase_for_range(time_range, 900.0),
|
||||
|
||||
@@ -24,6 +24,9 @@ var elapsed_s: float = 0.0
|
||||
## Wall-clock start, so the shift review can show event times as mm:ss.
|
||||
var started_at_ms: int = 0
|
||||
var current_phase: int = Rules.Phase.EARLY
|
||||
## Shift time added when resolving the phase. A night variant starts at 0:00
|
||||
## elapsed but reads LATE, so its full shift stays after dark.
|
||||
var phase_offset_s: float = 0.0
|
||||
var running := false
|
||||
|
||||
var event_bus := EventBus.new()
|
||||
@@ -50,6 +53,8 @@ func setup(p_scenario: VenueScenario, p_duration_s: float = 900.0, p_seed: int =
|
||||
duration_s = p_duration_s
|
||||
spawn_seed = p_seed
|
||||
scheduler.setup(scenario, p_seed)
|
||||
phase_offset_s = 0.0
|
||||
current_phase = Rules.Phase.EARLY
|
||||
event_bus = EventBus.new()
|
||||
responder_outcomes.clear()
|
||||
investigations.clear()
|
||||
@@ -101,17 +106,21 @@ func tick(delta: float) -> void:
|
||||
func _phase_for_time(t: float) -> int:
|
||||
if scenario == null or scenario.shift_phases.is_empty():
|
||||
return Rules.Phase.EARLY
|
||||
var shifted := t + phase_offset_s
|
||||
var last_phase := Rules.Phase.EARLY
|
||||
var keys := scenario.shift_phases.keys()
|
||||
keys.sort()
|
||||
for phase in keys:
|
||||
var start_s: float = scenario.shift_phases[phase].get("start_s", 0.0)
|
||||
if t >= start_s:
|
||||
if shifted >= start_s:
|
||||
last_phase = phase
|
||||
return last_phase
|
||||
|
||||
func schedule_time_s() -> float:
|
||||
return elapsed_s + phase_offset_s
|
||||
|
||||
func _spawn_due(delta: float) -> void:
|
||||
var due := scheduler.pull_due(elapsed_s, current_phase)
|
||||
var due := scheduler.pull_due(schedule_time_s(), current_phase)
|
||||
var fallback := scheduler.fallback_routine_traffic(current_phase)
|
||||
var all := due
|
||||
all.append_array(fallback)
|
||||
@@ -161,6 +170,7 @@ func snapshot() -> Dictionary:
|
||||
"duration_s": duration_s,
|
||||
"elapsed_s": elapsed_s,
|
||||
"current_phase": current_phase,
|
||||
"phase_offset_s": phase_offset_s,
|
||||
"spawned_count": spawned_count,
|
||||
"responder_outcomes": responder_outcomes.duplicate(),
|
||||
"investigations": investigations.duplicate(),
|
||||
@@ -175,6 +185,7 @@ func restore(data: Dictionary) -> void:
|
||||
duration_s = float(data.get("duration_s", duration_s))
|
||||
elapsed_s = float(data.get("elapsed_s", 0.0))
|
||||
current_phase = int(data.get("current_phase", Rules.Phase.EARLY))
|
||||
phase_offset_s = float(data.get("phase_offset_s", 0.0))
|
||||
spawned_count = int(data.get("spawned_count", 0))
|
||||
responder_outcomes.clear()
|
||||
for value in data.get("responder_outcomes", []):
|
||||
|
||||
@@ -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"},
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -44,6 +44,13 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
RAW_DIR = ROOT / "assets" / "textures" / "ai" / "v2" / "raw"
|
||||
READY_DIR = ROOT / "assets" / "textures" / "ai" / "game_ready" / "v2"
|
||||
CONCEPT_DIR = ROOT / "docs" / "screens" / "concepts"
|
||||
## Realistic-block references live in a dedicated folder so the original
|
||||
## Hollywood entrance concepts stay comparable.
|
||||
CONCEPT_SUBDIRS = {
|
||||
"hollywood_boulevard_entrance_day": "hollywood_boulevard",
|
||||
"hollywood_boulevard_entrance_night": "hollywood_boulevard",
|
||||
"hollywood_boulevard_across_street": "hollywood_boulevard",
|
||||
}
|
||||
REALISM_DIR = ROOT / "docs" / "screens" / "realism"
|
||||
|
||||
DEFAULT_MODEL = "openai/gpt-5-image-mini"
|
||||
@@ -129,6 +136,28 @@ CATALOGUE = {
|
||||
"wide shot of a fire engine and firefighters in dark turnout gear arriving at "
|
||||
"a brick venue entrance at night, red and blue light reflections on wet "
|
||||
"pavement, onlookers kept back by a guard")),
|
||||
# -- Real-world Hollywood block (map optimization) ----------------------
|
||||
"hollywood_boulevard_entrance_day": ("scene", (
|
||||
"photorealistic eye-level view of the El Capitan Entertainment Centre at "
|
||||
"6840 Hollywood Boulevard, the 1921 Hollywood Masonic Temple: light limestone "
|
||||
"Neoclassical facade, six oversized Ionic columns, eight stone steps up to "
|
||||
"recessed dark double doors with ornamental iron grilles, a continuous "
|
||||
"crosscut-grille transom, carved Masonic motto and circular medallions on the "
|
||||
"parapet, three tall red event banners, the Hollywood Walk of Fame terrazzo "
|
||||
"sidewalk with brass stars, palm trees, the neighbouring El Capitan Theatre "
|
||||
"marquee at the left edge, a four-lane boulevard, clear morning light")),
|
||||
"hollywood_boulevard_entrance_night": ("scene", (
|
||||
"the same El Capitan Entertainment Centre on Hollywood Boulevard after dark: "
|
||||
"six Ionic columns and limestone facade lit by warm practical lamps, red event "
|
||||
"banners, the El Capitan Theatre marquee glowing next door, wet asphalt "
|
||||
"reflecting the lights, a few pedestrians on the Walk of Fame terrazzo, palm "
|
||||
"silhouettes, distant city haze, cinematic night photography")),
|
||||
"hollywood_boulevard_across_street": ("scene", (
|
||||
"view from the steps of the El Capitan Entertainment Centre across Hollywood "
|
||||
"Boulevard: a four-lane street with yellow centre lines, red-painted kerb and "
|
||||
"crosswalk, the Ovation Hollywood retail frontage opposite with a broad stone "
|
||||
"arcade and sign band, palm trees, the Walk of Fame sidewalk with brass stars, "
|
||||
"traffic and pedestrians, late-afternoon haze, realistic game environment")),
|
||||
# -- Skyline backdrops (replace assets/textures/skyline.png) -------------
|
||||
"skyline_day": ("backdrop", (
|
||||
"Los Angeles Hollywood skyline in daylight: varied mid-rise and high-rise "
|
||||
@@ -177,6 +206,7 @@ PICKS = {
|
||||
"skyline_dusk": "skyline_dusk-v5",
|
||||
"skyline_night": "skyline_night-v4",
|
||||
"brick_facade": "brick_facade",
|
||||
"stone_steps": "stone_steps-v2",
|
||||
"sidewalk_pavers": "sidewalk_pavers",
|
||||
"asphalt": "asphalt",
|
||||
"asphalt_wet": "asphalt_wet",
|
||||
@@ -185,13 +215,13 @@ PICKS = {
|
||||
|
||||
# Realism trial: capture shot -> concept it is judged against.
|
||||
SHOTS = {
|
||||
"entrance_day": "hollywood_entrance_morning",
|
||||
"entrance_day": "hollywood_boulevard_entrance_day",
|
||||
"boulevard_day": "boulevard_skyline_day",
|
||||
"street_day": "boulevard_skyline_day",
|
||||
"street_day": "hollywood_boulevard_across_street",
|
||||
"boulevard_night": "hollywood_entrance_rain_night",
|
||||
"street_night": "boulevard_skyline_dusk",
|
||||
"entrance_late": "hollywood_entrance_rain_night",
|
||||
"street_late": "boulevard_skyline_dusk",
|
||||
"street_night": "hollywood_boulevard_entrance_night",
|
||||
"entrance_late": "hollywood_boulevard_entrance_night",
|
||||
"street_late": "hollywood_boulevard_across_street",
|
||||
# Each venue is judged on its own frame; the real geometry no longer shares
|
||||
# the Hollywood layout.
|
||||
"joke_shop_door": "joke_shop_door_closeup",
|
||||
@@ -223,7 +253,7 @@ def next_free_path(path: Path) -> Path:
|
||||
def raw_path(name: str) -> Path:
|
||||
group = CATALOGUE[name][0]
|
||||
if group == "scene":
|
||||
return CONCEPT_DIR / f"{name}.png"
|
||||
return CONCEPT_DIR / CONCEPT_SUBDIRS.get(name, "") / f"{name}.png"
|
||||
return RAW_DIR / group / f"{name}.png"
|
||||
|
||||
|
||||
@@ -820,7 +850,7 @@ def cmd_realism(args) -> int:
|
||||
rows = {}
|
||||
for capture in sorted(capture_set.glob("*.png")):
|
||||
concept_name = SHOTS.get(capture.stem)
|
||||
target = CONCEPT_DIR / f"{concept_name}.png" if concept_name else None
|
||||
target = raw_path(concept_name) if concept_name else None
|
||||
if target is None or not target.is_file():
|
||||
continue
|
||||
cap_m, tgt_m = image_metrics(capture), image_metrics(target)
|
||||
@@ -862,7 +892,7 @@ def write_sheet(run: Path, sets) -> Path:
|
||||
draw.text((c * cw + 6, 4), label, fill=(235, 235, 235))
|
||||
for r, shot in enumerate(shots):
|
||||
y = 20 + r * (ch + 20)
|
||||
tiles = [CONCEPT_DIR / f"{SHOTS[shot]}.png"] + [d / f"{shot}.png" for d in sets]
|
||||
tiles = [raw_path(SHOTS[shot])] + [d / f"{shot}.png" for d in sets]
|
||||
for c, path in enumerate(tiles):
|
||||
if path.is_file():
|
||||
with Image.open(path) as image:
|
||||
|
||||
@@ -23,6 +23,8 @@ class CatalogueTests(unittest.TestCase):
|
||||
|
||||
def test_paths_stay_in_their_folders(self):
|
||||
self.assertTrue(sc.raw_path("boulevard_skyline_day").is_relative_to(sc.CONCEPT_DIR))
|
||||
self.assertTrue(sc.raw_path("hollywood_boulevard_entrance_day").is_relative_to(
|
||||
sc.CONCEPT_DIR / "hollywood_boulevard"))
|
||||
self.assertTrue(sc.raw_path("skyline_day").is_relative_to(sc.RAW_DIR))
|
||||
self.assertTrue(sc.raw_path("asphalt").is_relative_to(sc.RAW_DIR))
|
||||
|
||||
|
||||