diff --git a/.gitignore b/.gitignore index 2c8f70e..52609da 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,33 @@ Icon? /scratch/ /tmp/ *.log + +# ============================================================ +# V0 — 3D Restaurant Readability Sandbox (visual track) +# ============================================================ + +# --- Godot 4 engine cache / machine-local state (regenerated on open) --- +.godot/ +visual/**/.godot/ +.import/ +export_presets.cfg +exports/ +*.tmp + +# --- Local CC0 asset-lab extracts (source + Godot .import sidecars stay local) --- +visual/RestaurantVisualSandbox/placeholders/_assetlab/ + +# --- Local asset quarantine (§10) --- +# Restricted / paid / unclear-license extracts live here and NEVER get committed +# to this public repo. Provenance is tracked as text in docs/art/ASSET-EVALUATION.md. +# Ignore all contents but keep the explanatory README committed as documentation. +/local-assets/* +!/local-assets/README.md +asset-cache/ + +# --- OS metadata --- +Thumbs.db + +# NOTE: evidence intentionally committed under reports/visual/v0/ is NOT ignored. +# (*.png screenshots, *.mp4 / *.gif recordings, *.csv / *.md performance & reports) +# The audio/binary-art ignores above (*.wav/*.mp3/*.ogg/*.fbx/*.glb/...) do not match them. diff --git a/docs/art/ART-DIRECTION-HYPOTHESES.md b/docs/art/ART-DIRECTION-HYPOTHESES.md new file mode 100644 index 0000000..4bd0745 --- /dev/null +++ b/docs/art/ART-DIRECTION-HYPOTHESES.md @@ -0,0 +1,131 @@ +# Art Direction Hypotheses — V0 Readability Sandbox + +**Track:** V0 — 3D Restaurant Readability Sandbox +**Scope:** the two visual-style treatments tested on the same gray-box (§27) +**Status:** hypothesis for the Art PM — **not** a locked visual direction + +> V0 is a presentation-only gray-box experiment. It owns no gameplay truth, is not +> integrated with `RestaurantSim.Core` (M0), and cannot approve visual direction, +> authorize M1, or merge its own PR. Everything below is a **recommendation** for +> Howard & Aaron and the Art PM to weigh; nothing here is a decision. See +> `docs/art/V0-PROTOTYPE-CONTRACT.md` §9. + +--- + +## 1. What was tested + +Two lighting/tone treatments were applied to the **identical** gray-box scene +(`scripts/stage.gd`) — same geometry, same scale lock (§4), same camera, same +scripted service, same overlays. Only the render mood changed. This isolates +"treatment" from every other variable, so any difference in readability or appeal +is attributable to the treatment alone. + +| | Treatment A | Treatment B | +|---|---|---| +| Label (§27) | "clean stylized management sim" | "grounded but readable" | +| Ambient | 0.75 (bright) | 0.22 (low) | +| Key light | soft light | stronger sun + shadows | +| Tonemap | ACES | ACES | +| SSAO | light | stronger | +| Palette | brighter, higher-key | muted, darker | +| Evidence | `reports/visual/v0/screenshots/40-treatment-A-clean.png` | `reports/visual/v0/screenshots/41-treatment-B-grounded.png` | + +Both images show the same restaurant floor from the same angle. In `40` the floor, +walls, and role/state colours read as separated, high-contrast blocks. In `41` the +same scene sits in deeper shadow: the mood reads richer, but darker regions +(back-left kitchen, near-wall interiors) lose colour separation and the state +signals sit closer together in value. + +--- + +## 2. Comparison + +Scored on the criteria that matter for a management sim seen at management distance. +"Management distance" here means the default readable overview the game will actually +be played from — the ISO / hybrid-overview framings (§ cameras; +`reports/visual/v0/screenshots/01-camera-A-isometric.png`, +`03-camera-C-hybrid-overview.png`), not a close-up marketing crop. + +| Criterion | Treatment A (clean) | Treatment B (grounded) | Edge | +|---|---|---|---| +| **Readability at management distance** | High. Brighter ambient (0.75) keeps role colours (§21) and table-state tints (§22) separated in value across the whole floor. | Lower. Low ambient (0.22) + strong shadow compress darker zones; state tints crowd together where the sun doesn't reach. | **A** | +| **Distinctiveness** | Clean, "management sim" look; risk of reading generic if under-detailed. | Moodier, more cinematic; feels more like a "place." | **B** | +| **Asset compatibility** | Both treatments run on the same gray-box; the cleared CC0 Downtown MegaKit and the 1:1 Universal Animation Library import (see `ASSET-EVALUATION.md`) are neutral to lighting choice. A's soft light is more forgiving of flat/placeholder albedo. | Same assets; stronger shadow rewards higher-detail meshes and better materials — a heavier art bar to look intentional rather than just dark. | **A** (lower asset bar) | +| **Production feasibility** | Fewer lighting variables to tune; soft light + light SSAO is cheap to author and stable across scenes. | More to balance (sun angle, shadow bias, SSAO strength, exposure) so darkness stays *readable* and not just dim. | **A** | +| **Animation / lighting burden** | Lighter. Soft, mostly-ambient light hides interpolation seams and idle-motion imperfections. | Higher. Hard shadows and contrast expose animation pops, foot-slide, and popping, and demand tighter shadow tuning per camera angle. | **A** | +| **Screenshot / marketing appeal** | Clear and legible but flatter; less dramatic as a hero still. | More atmospheric; better contrast and depth for a marketing still or trailer frame. | **B** | +| **Risk of looking generic** | Real risk: bright + clean can read as "any Unity/Godot management asset flip" if not backed by strong shape language. | Lower risk of generic; mood gives identity — but higher risk of reading as "murky" or hard to parse. | split | + +Performance is **not** a differentiator: both treatments render the same gray-box and +the sandbox far exceeds target on the documented machine — typical frame ~3 ms +(~330 FPS), worst single frame 13.9 ms (72 FPS, a one-off hitch), ~335 MB VRAM, +< 850 draw calls, against a 60 FPS target / 30 FPS floor (§28; +`reports/visual/v0/performance/performance.csv`). SSAO/shadow cost was not isolated +per treatment; treatment-level GPU deltas were **not measured in V0**. + +--- + +## 3. Finding + +**A is more readable at management distance. B is moodier but darker, and the +darkness costs readability.** + +The single question V0 exists to answer (§1) is whether a first-time viewer can read +where customers are, what staff are doing, and where congestion is forming, without +narration. Readability is the primary axis; mood is secondary. On the primary axis, +A wins: at 0.75 ambient the role/state colour language (§21–23) stays separated in +value across the entire floor, including the back-left kitchen and near-wall seating +that B pushes into shadow. B's atmosphere is genuinely more attractive as a still, +but it compresses exactly the value separation the readability language depends on — +most visibly in the darker kitchen zone where the vertical station gauges (§22) and +table discs need to pop. + +This finding is drawn from the two-image A/B comparison +(`40-treatment-A-clean.png` vs `41-treatment-B-grounded.png`) under identical +geometry and camera. The **human visual test (§30) is prepared but was not run** — +that is Howard & Aaron's to conduct — so this is a builder-side read of the evidence, +not a validated user finding. + +--- + +## 4. Recommendation (hypothesis, not a decision) + +1. **Adopt A ("clean stylized") as the readability baseline** — the default look the + game is designed and tuned against, and the tone under which the readability + language (§21–23) is validated. + +2. **Reserve B ("grounded") for marketing stills and non-gameplay presentation** — + hero shots, trailer frames, key art — where a fixed, art-directed camera and + crop can compensate for the lost floor-wide legibility, and where mood earns its + keep. + +3. **Treat this as a hypothesis for the Art PM, not a locked direction.** The right + next step is the §30 human visual test on both treatments before anything is + committed. A viable outcome is a *tuned middle*: A's ambient floor with a + controlled amount of B's shadow/SSAO for depth — kept only as far as it does not + compress the state palette. V0 recommends; it does not approve (§9). + +### Open questions for the Art PM + +- Does A hold up once real (non-placeholder) materials replace flat gray-box albedo, + or does its "generic" risk grow? (Not testable in V0 — placeholder-first, §7.) +- How dark can B go before state colours stop being distinguishable in the §30 test? + The crossover point was not measured here. +- Is there a single tuned treatment that keeps A's floor-wide readability while + borrowing enough of B's depth to lower the "generic" risk? +- Per-treatment GPU cost of the heavier shadow/SSAO in B — not isolated in V0. + +--- + +## 5. Evidence index + +| File | Shows | +|---|---| +| `reports/visual/v0/screenshots/40-treatment-A-clean.png` | Treatment A, clean stylized (ambient 0.75) | +| `reports/visual/v0/screenshots/41-treatment-B-grounded.png` | Treatment B, grounded (ambient 0.22) | +| `reports/visual/v0/screenshots/01-camera-A-isometric.png` | ISO framing — a management-distance read | +| `reports/visual/v0/screenshots/03-camera-C-hybrid-overview.png` | Hybrid overview — the default play framing | +| `reports/visual/v0/screenshots/20-overlay-off.png` … `22-overlay-full.png` | Readability overlays that both treatments must carry | +| `reports/visual/v0/performance/performance.csv` | Perf headroom (not a treatment differentiator) | +| `docs/art/ASSET-EVALUATION.md` | Asset licensing/scale that both treatments inherit | +| `docs/art/V0-PROTOTYPE-CONTRACT.md` | The shared visual contract (§ references above) | diff --git a/docs/art/ASSET-EVALUATION.md b/docs/art/ASSET-EVALUATION.md new file mode 100644 index 0000000..5666aa0 --- /dev/null +++ b/docs/art/ASSET-EVALUATION.md @@ -0,0 +1,65 @@ +# V0 — Asset Evaluation & Provenance (§10) + +**Verified:** 2026-07-29 · **Method:** each archive inspected read-only from the owner's +Desktop (`~/Desktop/Restaurant Empire/`); only text/license files were read; **no binary +was extracted into the repo**. Licenses confirmed **twice** where possible: the bundled +license file **and** the live source page. + +> **Hard rule (§10).** Ownership of an asset-store package is **not** permission to upload +> its source files to a public GitHub repo. No supplied package's source is committed here. +> Archives stay on the owner's disk; any local extract lives only in the git-ignored +> `local-assets/`. + +## Disposition table + +| Package | Source / author | Version | License | Commercial use | Public redistribution | Import result | V0 status | +|---|---|---|---|---|---|---|---| +| `Downtown City MegaKit[Standard].zip` (235 MB) | **Quaternius** (quaternius.com) | Standard / FREE (partial model set) | **CC0 1.0 Universal** — stated in bundled `License_Standard.txt` **and** at source | **Yes** | Permitted (CC0), **but not done** — repo hygiene (large binary; not needed for gray-box) | *Pending local import test (Phase C, asset-assisted comparison)* | **Approved for local evaluation** | +| `Universal Animation Library[Standard].zip` (16 MB) | **Quaternius** | Standard / FREE | **CC0 1.0** — bundled `License.txt` is **empty (0 bytes)**; **confirmed CC0 at quaternius.com 2026-07-29** ("Free to use in personal, educational and commercial projects. CC0 License") | **Yes** | Permitted (CC0), **not done** (hygiene) | *Ships Godot-ready GLB + `Godot_Setup.png`; retarget test pending (Phase C)* | **Approved for local evaluation** | +| `FBX-20260727T232629Z-1-001.zip` (1.5 MB) | **Unknown** (loose FBX props: `Table_RoundLarge`, `Houseplant_*`, `Bathroom_Mirror2`, `Light_*`) | Unknown | **None found** — no license file in the archive; publisher unidentifiable | **Unknown** | **No** | Not attempted | **Rejected — do not use** (no provenance) | +| `wintersets.zip` (17 MB) | **Unknown legacy game mod** ("Zapsters-Wintersets modded by Zapster"; `zapinfo` installer, German `Deinstallationsanleitung.txt`, backdrops/lightmaps/camera-swing terminology) | Unknown | **None** (installer package; presumed proprietary legacy game content) | **No** | **No** | Not attempted | **Rejected — do not use** | + +## Per-package detail + +### Downtown City MegaKit [Standard] — CC0, cleared +`License_Standard.txt` (verbatim): *"License: CC0 1.0 Universal (CC0 1.0) Public Domain +Dedication … Models by @Quaternius."* This is the **free** version (a portion of the models); +a paid SOURCE version exists with Godot projects/shaders. Contains `Exports/FBX (Unity)/…` +(e.g. `Brick_*.fbx`) and `Textures/`. It is a **downtown/city exterior** kit — buildings, +brick columns, trims — so its relevance to a restaurant *interior* is limited; useful mainly +for an **exterior/street context** block and as a scale-compatibility check. **FBX import** +in Godot 4.7 works via the built-in `ufbx` importer (no external converter needed) but FBX +retains Unity axis/scale conventions — expect a 0.01 scale factor and axis fix on import. + +### Universal Animation Library [Standard] — CC0, cleared (source-confirmed) +Bundled `License.txt` and `README.txt` are **empty**, which is a packaging omission, not a +license grant — so per policy it was treated as *unresolved* until the **source page confirmed +CC0** (quaternius.com, 2026-07-29). Ships `Unreal-Godot/UAL1_Standard.glb` (+`_RM.glb` +root-motion) and a `Godot_Setup.png` guide — i.e. **explicitly Godot-ready animations in glTF**, +which import natively in Godot 4 with their `AnimationPlayer`. This is the single most +relevant supplied pack for eventual character motion. **V0 does not build the animation state +machine** (§26); at most a Phase-C retarget/scale spot-check is warranted. + +### FBX-…zip — no license, do not use +Ironically the most *restaurant-relevant* props (round tables, houseplants, a bathroom mirror, +ceiling/stand lights), but the archive carries **no license file and no identifiable publisher**. +Under `LICENSE-POLICY` ("anything with unclear or missing license is rejected by default"), +it is **do-not-use** until the owner establishes where it came from and under what terms. + +### wintersets.zip — legacy game content, do not use +Structure (`zapinfo` installer, `Data/meshes/*.txt`, "Sets/backdrops/thumbs/camera-swing" +vocabulary, German uninstall guide) is unmistakably a **legacy game's set-mod package**. No +license; presumed proprietary. **Do not use** unless the owner produces explicit rights. + +## Import compatibility (summary) +- **glTF/GLB** (Universal Animation Library) → native Godot 4 import, animations intact. Best path. +- **FBX** (MegaKit) → Godot 4.7 built-in importer; watch Unity-scale (×0.01) and axis conversion; compare against the scale-reference scene before trusting. +- **No package is imported into V0 by default** (placeholder-first, §11). Any Phase-C + asset-assisted screenshot uses only CC0 packs, extracted to `local-assets/` (git-ignored), + and is documented as a *comparison*, never as the sandbox's identity. + +## Recommended future use (M1+, not V0) +- **Characters/animation:** Universal Animation Library (CC0, Godot-ready) is the prime candidate — pending a proper retarget evaluation. +- **Exterior/street dressing:** Downtown City MegaKit (CC0) for context blocks only. +- **Interior props:** source a **licensed** restaurant-interior kit; the loose FBX pack is not usable as-is. +- **Delivery:** even for CC0, decide Git-LFS-vs-external-store before committing any large binary; do not push source packs into this public repo. diff --git a/docs/art/CAMERA-EVALUATION.md b/docs/art/CAMERA-EVALUATION.md new file mode 100644 index 0000000..bbf10ec --- /dev/null +++ b/docs/art/CAMERA-EVALUATION.md @@ -0,0 +1,251 @@ +# Camera Evaluation — V0 3D Restaurant Readability Sandbox + +**Status:** V0 recommendation only. This document does **not** approve visual direction, +authorize M1, or decide anything. It records what the three camera candidates (§19) and the +four wall/roof modes (§20) actually do in the gray-box, and recommends a default for the Art +PM (Howard & Aaron) to decide on. + +**Scope note:** V0 is a presentation-only gray-box experiment. It owns no gameplay truth and +is not integrated with `RestaurantSim.Core` (M0). All observations below come from the Godot +4.7.1 build on the `prototype/v0-3d-restaurant-sandbox` branch, captured at 1920x1080 on an +Apple M3 Max, and from `scripts/camera_rig.gd` / `scripts/restaurant.gd`. + +--- + +## 1. What is being evaluated + +Three cameras share one rig (`scripts/camera_rig.gd`, enum `Mode { ISO_A, ORBIT_B, HYBRID_C }`): + +| ID | Name | Projection | Default framing | Interaction model | +|----|------|-----------|-----------------|-------------------| +| **A** | ISO | Orthographic | Fixed high 3/4: yaw 45°, pitch 35.264° (true isometric) | 90° step rotation; ortho-size zoom (7–44) | +| **B** | ORBIT | Perspective (fov 50) | Free orbit around a pivot | Orbit (pitch clamp 14–80°), pan (WASD), zoom (distance 6–48) | +| **C** | HYBRID | Perspective (fov 50) | Overview by default | Overview + `focus_on(target)` (0.6 s cubic ease-out tween to pitch 28°, dist ≈ 9 m) + `return_to_overview()` | + +The rig's runtime default `mode` is `ORBIT_B`; that is an implementation default for the +capture harness, not a recommendation. + +**Evidence — cameras:** +- `reports/visual/v0/screenshots/01-camera-A-isometric.png` +- `reports/visual/v0/screenshots/02-camera-B-orbit.png` +- `reports/visual/v0/screenshots/03-camera-C-hybrid-overview.png` +- `reports/visual/v0/screenshots/04-camera-C-hybrid-focus-grill.png` +- Recording: `reports/visual/v0/rec-03-camera-orbit.mp4` + +All three camera screenshots above show the same scene (scenario 2, Kitchen bottleneck, FULL +overlays) so the only variable is the camera, which makes the read/orientation comparison fair. + +--- + +## 2. Candidate A — ISO (orthographic isometric) + +**Framing observed** (`01-camera-A-isometric.png`): fixed high 3/4 view, orthographic, so +parallel lines stay parallel and every table reads at the same size regardless of screen +position. Zones and gauges are legible edge-to-edge with no perspective distortion. + +**Strengths** +- **Cleanest read.** Orthographic projection means no foreshortening: the grill gauge in the + back-left reads at the same scale as the front-right host stand. Best zone-level legibility. +- **Stable orientation.** Yaw is locked to 90° steps, so the operator always knows which way is + "up." This is the classic management-sim convention and lowest cognitive load. +- **Lowest interaction burden.** Rotate in fixed quarter-turns, zoom, done. Hard to get lost. + +**Weaknesses** +- **Can feel distant / detached.** No perspective means no sense of depth or "being there"; + the space reads as a diagram more than a room. +- **No arbitrary inspection angle.** You cannot tip in to look between two cooks at the pass, or + duck under an overhang. Detail inspection (Level-2/3) is limited to what the fixed angle shows. +- **Half-height dividers still matter.** The read depends on interior dividers being 1.30 m + (`DIVIDER_H`) so the camera sees over them; that holds here, but ISO's fixed pitch gives no + fallback if a future taller obstacle blocks a station. + +**Readability:** Highest at management distance. **Interaction burden:** Lowest. + +**Wall behaviour:** Works with all four wall modes. Because ISO is a fixed high angle, the +"camera-facing" near walls are consistent shot-to-shot, so AUTO_HIDE/CUTAWAY hide a predictable +set of walls every time — the most stable wall behaviour of the three. + +**Recommendation:** Strong candidate for the **default management view** if the product wants a +pure top-down-diagram feel. Its ceiling is inspection: it cannot get close on its own. + +--- + +## 3. Candidate B — ORBIT (perspective free orbit) + +**Framing observed** (`02-camera-B-orbit.png`): perspective, orbited to a lower, more raking +angle than ISO. The near dining tables read large and the far kitchen recedes — good depth cue, +but the far stations are smaller and the far exterior wall eats some screen area. + +**Strengths** +- **Best inspection angles.** Free orbit (pitch 14–80°) + pan (WASD) + zoom (distance 6–48) + reaches any angle within bounds. Ideal for looking *at* a specific problem — between the pass + and the servers, or down the 0.85 m restroom pinch. +- **Depth and presence.** Perspective sells the room as a space, useful for marketing framing. +- **One camera covers overview-to-close.** Zoom out to ~48 m for the whole floor, in to ~6 m for + a station. + +**Weaknesses** +- **Can disorient.** Free yaw with no snap means the operator can end up facing an unexpected + direction and lose the "which zone is which" anchor. This is the classic free-orbit cost. +- **Highest interaction burden.** To get a clean read the operator must actively frame the shot + (orbit + pan + zoom). Left alone at a bad angle, near geometry occludes the interior. +- **Perspective unevenness.** Foreshortening means far tables/gauges read smaller than near ones, + so at-a-glance state comparison across the floor is slightly harder than ISO. + +**Readability:** High *once framed well*; variable if left at an arbitrary angle. +**Interaction burden:** Highest. + +**Wall behaviour:** Works with all four modes, but because the camera can face any direction the +set of "camera-facing" walls that AUTO_HIDE/CUTAWAY remove changes continuously as you orbit. +That is correct behaviour (`set_wall_mode` recomputes per camera position) but means the visible +walls shift under the operator during a drag, which can add to the disorientation cost. + +**Recommendation:** Keep as the **inspection / detail camera**, not the default. It is the best +tool for "go look at that specific thing," and the worst for "glance and know." + +--- + +## 4. Candidate C — HYBRID (perspective overview + focus) + +**Framing observed:** +- Overview (`03-camera-C-hybrid-overview.png`): a calmer, higher perspective overview than ORBIT + — closer to ISO's read but with gentle perspective. The whole floor and all zone labels are + legible; gauges and discs read across the space. +- Focus (`04-camera-C-hybrid-focus-grill.png`): after `focus_on(grill)`, the camera has tweened + in on the kitchen — the four station gauges (queue 5 / KITCH.. / queue 1 / Range) and the + massed servers at the pass fill the frame and read clearly. This is the Level-2/3 inspection + the ISO camera cannot reach on its own. + +**Strengths** +- **Best balance.** Defaults to a legible overview (like ISO's intent, with perspective depth) + **and** offers scripted, controlled close-ups via `focus_on(target)` / `focus_next()`. +- **Guided, low-burden inspection.** The 0.6 s cubic ease-out tween (`_do_tween`) moves the + camera *for* the operator to a known good framing (pitch 28°, dist ≈ 9 m). No manual orbiting + to get a clean detail shot; `return_to_overview()` snaps back to the saved overview. +- **Never lost.** Because focus always returns to one canonical overview, the operator has a + stable "home" — it avoids ORBIT's disorientation while keeping perspective's depth. + +**Weaknesses** +- **Perspective in the overview.** Slight foreshortening remains vs. ISO's perfectly even read; + far-side state is marginally smaller than near-side. +- **Focus targets must be authored.** `focus_on` needs registered targets (`register_focus_targets`); + ad-hoc "look at *this* random spot" still wants ORBIT. In V0 the focus target set is scripted. +- **Two modes to learn.** Slightly more concept than ISO's single fixed view (overview vs. focus), + though far less than ORBIT's full free control. + +**Readability:** High in overview; **highest of all** for guided close inspection (the tween +lands a framed shot every time). **Interaction burden:** Low — one button to focus, one to return. + +**Wall behaviour:** Works with all four modes. In the overview it behaves like a high perspective +camera (predictable near-wall set); during a focus tween the camera-facing wall set updates as it +moves, but because the motion is authored and short (0.6 s) the wall changes are smooth rather +than operator-driven jitter. + +**Recommendation:** Best default. Combines ISO-grade glance-readability at overview with ORBIT- +grade inspection, minus ORBIT's disorientation and manual-framing burden. + +--- + +## 5. Camera comparison table + +| Criterion | A — ISO | B — ORBIT | C — HYBRID | +|---|---|---|---| +| Projection | Orthographic | Perspective (fov 50) | Perspective (fov 50) | +| Glance readability (overview) | **Best** (even, no foreshortening) | Variable (depends on angle) | High (slight perspective) | +| Close inspection (Level 2/3) | Limited (fixed angle) | **Best** (free) but manual | **Best guided** (authored focus tween) | +| Orientation stability | **Best** (90° snaps) | Weak (free yaw) | Strong (canonical overview home) | +| Interaction burden | **Lowest** | Highest | Low | +| Disorientation risk | Very low | High | Low | +| Depth / presence | Low (diagram feel) | High | High | +| Wall-mode stability | **Most stable** (fixed near-walls) | Least stable (near-walls shift on drag) | Stable overview; smooth during focus | +| Best role | Default *if* pure diagram wanted | Detail / inspection camera | **Default management view** | +| Evidence | `01-camera-A-isometric.png` | `02-camera-B-orbit.png` | `03-` / `04-camera-C-*.png` | + +--- + +## 6. Wall and roof handling (§20) + +Exterior walls are camera-relative (`restaurant.gd::set_wall_mode`, enum +`WallMode { SOLID, AUTO_HIDE, CUTAWAY, TRANSPARENT }`). A wall is "camera-facing" when it sits +between the camera and the interior (`inward.dot(cam_pos - center) < 0.0`). Interior dividers are +half-height (`DIVIDER_H = 1.30 m`) so the camera sees over them regardless of wall mode, and the +**roof is hidden by default** in the management view (`roof.visible = false`). + +| Mode | Camera-facing exterior walls | Roof | Interior read | Evidence | +|---|---|---|---|---| +| **SOLID** | Shown | (off by default) | Perimeter walls fully present; near wall can occlude front zones | `30-walls-solid.png` | +| **AUTO_HIDE** | Hidden | (off by default) | Near walls drop away → clean dollhouse read of the interior | `31-walls-autohide.png` | +| **CUTAWAY** | Hidden | Forced **off** | Explicit dollhouse: near walls hidden **and** roof guaranteed off | `32-walls-cutaway.png` | +| **TRANSPARENT** | ~16% alpha (`0.16`) | (off by default) | Near walls ghosted, not gone — interior visible through them, perimeter still legible | `33-walls-transparent.png` | +| **ROOF ON** | (per current wall mode) | **On** | Exterior-only view; interior hidden. Marketing / establishing shot, not a working view | `34-roof-on.png` | + +**Observed in the captures:** +- `30-walls-solid.png` — all four perimeter walls present, including the near (camera-side) wall. + The interior still reads here because the roof is off and dividers are half-height, but the near + wall crops the front-right entrance/waiting zone. This is the most "enclosed" look. +- `31-walls-autohide.png`, `32-walls-cutaway.png`, `33-walls-transparent.png` — all three give an + open dollhouse read of the full interior (dining tables, kitchen stations, host stand). In these + captures they look **nearly identical**, which is expected and worth flagging: + - The roof is already off by default, so CUTAWAY's extra "force roof off" is a no-op here — it + looks the same as AUTO_HIDE from this angle. CUTAWAY's value is the *guarantee* (it cannot be + left with the roof on), not a different look in this shot. + - TRANSPARENT's near walls are only ~16% alpha, so at this angle they read as effectively hidden + too. The difference (a faint ghosted wall vs. a fully removed one) is subtle at overview + distance and would show more when the camera is low/near a wall. +- `34-roof-on.png` — closed box with only the HOST door visible. Confirms roof toggles correctly; + useful only for exterior/establishing framing, never for management play. + +**Wall-mode recommendation:** Default to **AUTO_HIDE** for the working management view (clean +interior, roof off, camera-facing walls drop away as needed). Offer **CUTAWAY** as the explicit +"dollhouse, roof guaranteed off" preset for screenshots/onboarding, and **TRANSPARENT** as an +option for operators who want spatial context (keep the perimeter visible while seeing in). Keep +**SOLID** + **ROOF ON** for exterior/marketing framing only. This pairs naturally with HYBRID-C: +overview + AUTO_HIDE is the everyday read; focus-in still respects the same wall logic. + +--- + +## 7. Recommended default (for the Art PM to decide) + +**Recommended default camera: Candidate C (HYBRID) — perspective overview by default, with +`focus_on` / `return_to_overview` for inspection — paired with AUTO_HIDE walls and roof off.** + +Rationale, grounded in the evidence: + +1. **It wins the "glance and know" test at overview** (`03-camera-C-hybrid-overview.png`) while + also being the only camera that delivers a *framed* close-up without manual work + (`04-camera-C-hybrid-focus-grill.png`). ISO can glance but cannot inspect; ORBIT can inspect + but must be manually framed and risks disorientation. HYBRID does both. +2. **Lowest disorientation for the depth it provides.** The canonical overview is a stable home + the operator always returns to (`return_to_overview` restores the saved overview), so the + perspective depth comes without ORBIT's "which way am I facing" cost. +3. **Low interaction burden.** The 0.6 s authored tween does the framing; the operator presses + focus / return rather than orbiting. + +**Should orbit be available too?** Yes — expose **ORBIT-B as an opt-in inspection mode** for +free-look at anything the authored focus targets don't cover. HYBRID is the default; ORBIT is the +"I want to look at that myself" escape hatch. This matches the brief's "Hybrid-C default overview +with orbit controls" framing. + +**When ISO-A would be the better default instead:** if the human visual test (§30, prepared but +**not yet run** — that is Howard & Aaron's to conduct) shows that testers value perfectly even, +diagram-grade legibility over depth/presence, ISO-A becomes the stronger default. ISO is the +lowest-risk, lowest-burden option and the most wall-mode-stable; its only real cost is inspection +reach, which the product could cover by adding an ISO "zoom-to-station" preset. The evidence here +favours HYBRID, but this is a taste/read-priority call the visual test should settle. + +--- + +## 8. Caveats and limits + +- **Performance is not a differentiator between cameras.** All configs far exceed the 60 FPS + target (typical frame ~3 ms / ~330 FPS uncapped; worst single frame 13.9 ms / 72 FPS, a one-off + hitch; ~335 MB VRAM; <850 draw calls — `reports/visual/v0/performance/performance.csv`, §28). + Camera choice can be made purely on readability/UX, not cost. (vsync-on would cap to display + refresh; these are uncapped headroom numbers.) +- **The human visual test has not been run.** This recommendation is builder judgement from the + captures and code, not tester data. +- **Subtle wall-mode differences at overview.** As noted, AUTO_HIDE / CUTAWAY / TRANSPARENT read + very similarly at overview distance because the roof is already off and TRANSPARENT's alpha is + low; their distinctions matter more at low/near-wall angles, which are not the default framing. +- **Everything here is VisualOnly.** No sim integration, no navmesh, no gameplay truth. V0 + recommends; Howard & Aaron decide. diff --git a/docs/art/PERFORMANCE-BUDGET.md b/docs/art/PERFORMANCE-BUDGET.md new file mode 100644 index 0000000..87e8dc6 --- /dev/null +++ b/docs/art/PERFORMANCE-BUDGET.md @@ -0,0 +1,136 @@ +# Performance Budget — V0 3D Restaurant Readability Sandbox + +**Status:** Exploratory measurement. V0 RECOMMENDS; it does not approve visual +direction, authorize M1, or set production budgets. Those are Howard & Aaron's calls. + +This document records the provisional performance budget (§28) and the MEASURED +results captured on a single high-end machine. Read the caveats at the end before +treating any number here as a target or guarantee. + +--- + +## 1. Targets (§28) + +The provisional budget the sandbox measured against: + +| Parameter | Value | +| --- | --- | +| Reference resolution | 1920 x 1080 | +| Frame-rate target | 60 FPS | +| Frame-rate minimum acceptable | 30 FPS | + +At 60 FPS the per-frame budget is 16.67 ms; at the 30 FPS floor it is 33.33 ms. + +## 2. Test environment + +All numbers were captured under one fixed configuration. They do NOT sweep across +hardware, resolutions, or driver settings. + +| Field | Value | +| --- | --- | +| Machine | Apple M3 Max, macOS 26.5.1, arm64 | +| Display | Built-in Retina / ProMotion | +| Engine | Godot 4.7.1-stable (4.7.1.stable.official.a13da4feb) | +| Renderer | Forward+, Metal 4.0 | +| Resolution | 1920 x 1080 | +| VSync | **OFF** (uncapped — see §5) | +| Scene fidelity | Gray-box only (no final art, shaders, VFX, or characters) | +| Language | GDScript only (no C#) | + +Reproduce: `tools/v0/capture.sh` (editor scan -> screenshots -> perf probe). +Source data: `reports/visual/v0/performance/performance.csv`. + +## 3. Measured results (verbatim) + +Reproduced exactly from `reports/visual/v0/performance/performance.csv`. +Columns: `config, actors, mesh_instances, draw_calls, primitives, avg_fps, +worst_fps, avg_frame_ms, worst_frame_ms, video_mem_mb`. + +``` +config,actors,mesh_instances,draw_calls,primitives,avg_fps,worst_fps,avg_frame_ms,worst_frame_ms,video_mem_mb +empty_restaurant,9,268,671,136926,257,116,3.89,8.65,332.6 +healthy_service,32,329,848,399046,358,330,2.79,3.03,338.0 +peak_kitchen_full,31,342,758,450408,322,72,3.11,13.91,337.8 +peak_foh_all_diagnostics,25,320,708,258564,361,330,2.77,3.03,336.5 +max_zoom_out,31,342,791,474844,360,330,2.78,3.03,337.8 +``` + +Rendered as a table for readability (values are identical to the CSV above): + +| config | actors | mesh_inst | draw_calls | primitives | avg_fps | worst_fps | avg_ms | worst_ms | vram_mb | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| empty_restaurant | 9 | 268 | 671 | 136,926 | 257 | 116 | 3.89 | 8.65 | 332.6 | +| healthy_service | 32 | 329 | 848 | 399,046 | 358 | 330 | 2.79 | 3.03 | 338.0 | +| peak_kitchen_full | 31 | 342 | 758 | 450,408 | 322 | 72 | 3.11 | 13.91 | 337.8 | +| peak_foh_all_diagnostics | 25 | 320 | 708 | 258,564 | 361 | 330 | 2.77 | 3.03 | 336.5 | +| max_zoom_out | 31 | 342 | 791 | 474,844 | 360 | 330 | 2.78 | 3.03 | 337.8 | + +Config notes: +- `empty_restaurant` — the built floor with no scripted service running (9 actors). +- `healthy_service` — Scenario 0 at peak (`10-scenario-healthy.png`). +- `peak_kitchen_full` — Scenario 1 kitchen-bottleneck at peak + (`11-scenario-kitchen-bottleneck.png`). +- `peak_foh_all_diagnostics` — Scenario 2 FOH-bottleneck at peak with FULL overlays + (`12-scenario-foh-bottleneck.png`, `22-overlay-full.png`). +- `max_zoom_out` — camera pulled to maximum distance, most geometry on screen. + +## 4. Interpretation + +On this one machine, the gray-box sandbox has **large headroom** against the +provisional budget: + +- **Typical frame ~3 ms (~330 FPS).** The four service configurations all sit at + 2.77–3.11 ms average, roughly 5x under the 16.67 ms / 60 FPS budget and ~10x + under the 33.33 ms / 30 FPS floor. +- **Worst single frame 13.91 ms (72 FPS).** This one-off hitch, recorded in + `peak_kitchen_full`, is still inside the 60 FPS budget and well above the 30 FPS + floor. It reads as a transient (scenario spin-up / scripted event) rather than a + sustained cost — the same config's average frame is 3.11 ms. The second-highest + worst-frame is `empty_restaurant` at 8.65 ms. +- **Draw calls < 850.** The peak is 848 (`healthy_service`); every config stays + under 850. +- **VRAM ~335 MB.** All configs fall in a narrow 332.6–338.0 MB band; scene + complexity barely moves the video-memory footprint at this fidelity. +- **Actor / mesh scaling is not a bottleneck here.** Going from 9 to 32 actors and + from 136k to 475k primitives did not push average frame time above ~3.9 ms. + +**Verdict:** at gray-box fidelity, on this hardware, with vsync off, the sandbox +far exceeds the 60 FPS target and never approaches the 30 FPS minimum. + +## 5. VSync note + +These are **uncapped** numbers (vsync OFF). The ~330 FPS averages are headroom +figures, not a claim that the app renders 330 frames to the display. With vsync ON, +frame-rate would be capped to the display refresh (e.g. 60 Hz, or higher on the +ProMotion panel). The uncapped view is the useful one for a budget: it shows how +much of the frame the renderer is actually using (~3 ms) versus how much it has +(16.67 ms at 60 FPS). + +## 6. Caveats — what these numbers are NOT + +These are exploratory numbers from a 12–20 hour gray-box experiment (§33). Treat +them as a floor-of-cost signal, not a production budget: + +- **One machine only.** Every figure is from a single Apple M3 Max. Lower-end and + non-Apple hardware is **untested** — no other GPU, resolution, or thermal profile + was measured. Performance on a mainstream or minimum-spec target is unknown. +- **Gray-box only.** No final art, materials, PBR shaders, VFX, audio, lighting + polish, or production characters exist (§13). Real assets, real shaders, skinned + and animated characters, more actors, and richer materials will cost **far more** + than the primitives measured here. The asset-assisted animation test + (`60-asset-assisted-animation-lib.png`) imported one CC0 character; a full cast + animating at peak service was not measured. +- **Not a stress test.** Configs cover authored peak scenarios, not adversarial + worst cases (max actors + max overlays + max shadows + smallest hardware). +- **Not production guarantees.** Nothing here approves a target or certifies + shippability. These numbers exist to inform Howard & Aaron, not to bind M1. +- **Values not listed are "not measured in V0."** CPU-side sim cost, load times, + and battery/thermal behavior over long sessions were not captured. + +## 7. Recommendation + +At gray-box fidelity on high-end Apple silicon, performance is a non-issue and does +not constrain visual-direction decisions at this stage. Before any budget is fixed, +V0 recommends re-measuring with (a) representative production art and shaders, +(b) a full animated cast at peak service, and (c) at least one lower-end target +device, using the same `tools/v0/capture.sh` probe for comparability. diff --git a/docs/art/READABILITY-CONTRACT.md b/docs/art/READABILITY-CONTRACT.md new file mode 100644 index 0000000..6c8af26 --- /dev/null +++ b/docs/art/READABILITY-CONTRACT.md @@ -0,0 +1,262 @@ +# V0 — Readability Contract (§21–§23) + +**Track:** V0 — 3D Restaurant Readability Sandbox · **Status:** LOCKED for this experiment +**Branch:** `prototype/v0-3d-restaurant-sandbox` · **Baseline:** `origin/main` (`06f0a7e`) +**Companion:** `docs/art/V0-PROTOTYPE-CONTRACT.md` (the overall visual contract) · +`docs/art/ASSET-EVALUATION.md` (asset licensing / suitability) + +This document defines the **readability language** V0 uses to answer its one question +(V0-PROTOTYPE-CONTRACT §1): can a first-time viewer look at one small 3D restaurant +during an active service and quickly understand where customers are, what employees are +doing, where congestion is forming, and which part of the restaurant needs attention — +without narration? + +Authority note: V0 **recommends** a readability language; it does not approve visual +direction, authorize M1, or integrate the C# `RestaurantSim.Core`. Those are the product +owners' calls (Howard & Aaron). Everything below is presentation-only vocabulary driven +by a Mock / Scripted / VisualOnly timeline; none of it owns gameplay truth (visual-truth +boundary, V0-PROTOTYPE-CONTRACT §6). + +Authoritative source of the colours, roles, and states named here: +`visual/RestaurantVisualSandbox/scripts/v0_config.gd` (autoload `V0`). Nothing is +inlined "to look right"; every scene reads from that singleton. + +--- + +## §21 Roles — colour **plus** a second signal + +The core rule: **role is never carried by colour alone.** Each role has a base tint +**and** a second, non-colour signal (a prop, headwear, silhouette height, or marker), so +the role stays distinguishable if colour is lost, dimmed, or confused. This is the +sandbox's accessibility posture (see [Accessibility](#accessibility-posture) below). + +| Role | Colour (`v0_config.gd`) | Second signal | Silhouette height | +|---|---|---|---| +| Customer | tan `#d9c7a3` | *none* (plain body — the absence of a prop is itself the tell) | 1.78 m | +| Host | teal `#2ec4b6` | podium / clipboard | 1.82 m | +| Server | blue `#3a86ff` | tray | 1.80 m | +| Cook | white `#f4f4f4` | toque (chef's hat) | 1.80 m | +| Busser | green `#52b788` | bin | 1.79 m | +| Manager | purple `#9b5de5` | clipboard **and** a star marker | 1.83 m | + +Notes: +- The HUD legend renders the role prop in text (`"Host (clipboard)"`, `"Server (tray)"`, + `"Cook (toque)"`, `"Busser (bin)"`) so a viewer can learn the second signal without a + tutorial (`service_view.gd::_build_legend`). +- Manager is the only role carrying two secondary signals (prop + star); it does not + appear in the four scripted scenarios but is defined for completeness. +- Heights are within the locked human range 1.75–1.85 m (V0-PROTOTYPE-CONTRACT §4); the + spread is a deliberate, minor silhouette cue, not a scale violation. + +--- + +## §22 States — the state → colour language + +State colour is a **single vocabulary shared across tables, kitchen stations, customer +patience, and staff workload**, so a viewer learns it once and re-reads it everywhere. +The ramp is designed so *calm reads cool/green* and *pressure reads warm/red* at a +glance. + +### State palette (base) + +| Token (`v0_config.gd`) | Colour | Hex | Meaning | +|---|---|---|---| +| `C_CALM` | green | `#52b788` | available / normal / healthy | +| `C_ACTIVE` | blue | `#3a86ff` | occupied / working / eating / in-progress | +| `C_WAIT_SOFT` | amber | `#ffd166` | waiting, still patient (soft) | +| `C_WAIT_HARD` | orange | `#f77f00` | waiting too long / growing queue | +| `C_ALERT` | red | `#e63946` | unhappy / congested / overloaded / alert | +| `C_DIRTY` | brown | `#8d6b4b` | needs cleaning | +| `C_READY` | cyan | `#00b4d8` | food ready / needs payment | +| `C_IDLE` | grey | `#9aa0a6` | idle / spare capacity / unavailable | + +### Table state → colour + +Nine modelled lifecycle states (`enum TableState`). Each maps into the shared palette; +several states intentionally **share** a colour where they share a meaning (e.g. seated +and eating both read as blue "active"; being-cleaned reads green because the table is +almost back in service). + +| Table state | Colour token | Reads as | +|---|---|---| +| Available | `C_CALM` green | open / ready to seat | +| Preparing | `C_IDLE` grey | not yet in service | +| Seated (occupied) | `C_ACTIVE` blue | active | +| Waiting to order | `C_WAIT_SOFT` amber | soft wait | +| Waiting for food | `C_WAIT_HARD` orange | hard wait | +| Eating | `C_ACTIVE` blue | active | +| Needs payment | `C_READY` cyan | ready to close out | +| Dirty | `C_DIRTY` brown | needs bussing | +| Being cleaned | `C_CALM` green | almost available | + +### Kitchen station state → colour + +Six modelled station states (`enum StationState`). + +| Station state | Colour token | Reads as | +|---|---|---| +| Normal | `C_CALM` green | keeping pace | +| Queue | `C_WAIT_SOFT` amber | orders backing up | +| Congested | `C_ALERT` red | overwhelmed | +| Food ready | `C_READY` cyan | plates up, awaiting pickup | +| Idle | `C_IDLE` grey | spare capacity | +| Unavailable | `#4a4e54` dark grey | station off | + +### Customer patience ramp + +Patience is a continuous value in `[0,1]` (1 = fresh, 0 = out of patience) mapped through +`V0.patience_color(p)`: green → amber for `p > 0.5`, amber → red for `p ≤ 0.5`. It reuses +the same three anchor colours (`C_CALM`, `C_WAIT_SOFT`, `C_ALERT`) so a reddening pip +means the same thing as a reddening station bar. + +--- + +## §23 Three-level visual hierarchy + +The language is layered so the viewer can read at three altitudes without switching +tools. The **same state colour** appears at every level; only the *carrier* changes. + +### Level 1 — Whole restaurant (management view) + +The default read. From the fixed high 3/4 view (Camera A ISO, or Camera C HYBRID +overview) the roof is hidden and interior dividers are half-height (1.30 m) so the camera +sees the whole floor at once (V0-PROTOTYPE-CONTRACT; `restaurant.gd`). At this altitude +the viewer scans **fields of colour**: a wash of blue tables = healthy; a cluster of +orange = a food problem; a tall red kitchen gauge = a station in trouble. No single +marker needs to be legible; the *distribution* is the signal. + +- Evidence: `reports/visual/v0/screenshots/05-whole-restaurant.png`, + `10-scenario-healthy.png`. + +### Level 2 — Operational area (zone) + +Reading one zone — the kitchen, the dining floor, the entrance/host stand. Carriers: +the **vertical station gauges** at each of the four kitchen stations (Grill / Fry / Range +/ Garde), the **queue-count labels** above them, the **entrance queue** of waiting +parties, and the **pass line** of ready plates. This is the altitude at which "is the +kitchen the problem, or the floor?" is answered. + +- Evidence: `reports/visual/v0/screenshots/06-kitchen-closeup.png`, + `07-dining-closeup.png`. + +### Level 3 — Individual (inspection) + +Reading one table, one cook, one waiting party. Carriers: the **floating status disc** +over each table, per-customer **patience pips**, per-staff **workload rings**, individual +**pass plates**, and **path-hint lines** from a server to the table it is serving. +Reached via Camera C's `focus_on(target)` (0.6 s cubic tween) or Camera B free orbit, +and surfaced by turning overlays up to FULL or using SELECTION. + +- Evidence: `reports/visual/v0/screenshots/04-camera-C-hybrid-focus-grill.png`, + `22-overlay-full.png`. + +--- + +## How each state is shown (carriers) + +| Carrier | What it shows | Level | Source | +|---|---|---|---| +| **Tinted tabletop + floating disc** | table state | 1 (disc reads from management view) / 3 (tabletop) | `service_view.gd::_apply_tables` | +| **Vertical station gauge** | kitchen station: colour = state, **height = queue depth** | 1 (colour) / 2 (height) | `service_view.gd::_apply_stations` | +| **Queue-count label** | integer order queue at a station (`"queue N"`, hidden at 0) | 2 | `service_view.gd::_apply_stations` | +| **Patience pips** | one floating pip per customer, coloured by the patience ramp; reddens as patience drops | 3 | `service_view.gd::_set_pip` | +| **Workload rings** | torus under a staff member: red = overloaded, blue = active, grey = idle | 3 | `service_view.gd::_set_ring` | +| **Pass plates** | ready plates queued at the pass; the pile shifts orange → red as it ages / backs up | 2–3 | `service_view.gd::_apply_pass` | +| **Path-hint lines** | thin translucent line from a server to its target table | 3 | `service_view.gd::_path_line` | +| **HUD (2D)** | scenario name, scripted clock, one-line headline, role/state legend, and a persistent `MOCK / SCRIPTED — not a simulation` tag | all | `service_view.gd::_build_hud` | + +Table detail: an available table also greys its physical top (`#6b7078`) so an empty +table is unmistakable from the management view even before the disc is read. + +--- + +## Overlay modes + +Overlays trade **legibility of individual markers** against **clutter**. Four modes cycle +(`enum Overlay { OFF, MINIMAL, FULL, SELECTION }`, `service_view.gd`). The HUD always +shows the active mode (`overlays: `). + +| Mode | What is drawn | Purpose | +|---|---|---| +| **OFF** | no overlays — bare gray-box + actors | see the space itself; art/lighting review | +| **MINIMAL** *(default)* | table discs + station gauges + queue counts | the everyday management read; least clutter that still answers "where's the problem?" | +| **FULL** | MINIMAL **plus** patience pips, workload rings, pass plates, path-hint lines | inspection — every individual signal on; densest | +| **SELECTION** | MINIMAL, with the selected table's disc lightened (+0.35) to single it out | focus on one table without the FULL clutter | + +**MINIMAL is the default and the clutter-reducing mode**: it carries the baseline +signal (discs + gauges + counts) and nothing else, which is enough to distinguish all +four scenarios at Level 1. **FULL is for inspection**, when a reviewer wants per-customer +and per-staff detail and accepts the added density. OFF removes the language entirely for +looking at the space; SELECTION isolates one table. + +- Evidence: `reports/visual/v0/screenshots/20-overlay-off.png`, + `21-overlay-minimal.png`, `22-overlay-full.png`. (The 21/22 pair captures the same + kitchen-bottleneck scene at MINIMAL vs FULL, so the added pips/rings/plates/paths are + the visible difference.) + +--- + +## Kitchen vs front-of-house: the bottleneck contrast is distinguishable + +The load-bearing test of the language (V0-PROTOTYPE-CONTRACT §8 pass criteria) is that a +**kitchen bottleneck is visually distinct from a front-of-house bottleneck**. The +scripted service (`mock_service.gd`) authors four peak-time scenarios with deliberately +different signatures. Because `snapshot(scenario, t)` is a pure, seekable function, each +peak beat is reproducible. + +| Scenario | Peak | Kitchen reads as | Floor reads as | The tell | +|---|---|---|---|---| +| 0 Healthy | 150 s | short **green** gauges | mostly **blue** (eating) tables, no alerts | the baseline; no warm colour dominates | +| 1 Kitchen bottleneck | 165 s | **tall red** grill gauge (`CONGESTED`, queue 5) | many **orange** waiting-for-food tables; servers massed idle at the pass | pressure is **in the kitchen**; food isn't reaching the floor | +| 2 FOH bottleneck | 180 s | calm **green / grey idle** gauges | **cyan** needs-payment + **amber** waiting-to-order + **brown** dirty tables; a **red** overloaded-server ring; plates piled at the pass | kitchen is visibly **NOT** the problem | +| 3 Host bottleneck | 90 s | **idle grey** gauges (spare capacity) | entrance **queue** with reddening patience pips; several **dirty** tables unreseated; a customer leaving | seating/host is the choke; kitchen has slack | + +The contrast between scenario 1 and scenario 2 is the crux: in **1** the warm colour and +the tall gauge are concentrated at the grill while the floor waits (orange); in **2** the +kitchen is cool/green/idle while the warm colour (cyan/amber/brown) and the red overload +ring are all on the floor. The two failure modes therefore have **opposite colour +centres of mass** at Level 1, before any individual marker is read. + +- Evidence (cited per instruction): `reports/visual/v0/screenshots/11-scenario-kitchen-bottleneck.png`, + `reports/visual/v0/screenshots/12-scenario-foh-bottleneck.png`, + `reports/visual/v0/screenshots/13-scenario-host-bottleneck.png`. Baseline: + `10-scenario-healthy.png`. Recordings: `reports/visual/v0/recordings/rec-01-kitchen-service.mp4`, + `rec-02-foh-service.mp4`. + +Verification status: the **human visual test** that would confirm a *first-time* viewer +reads these distinctions unaided is prepared but **not run** — that is Howard & Aaron's to +conduct (V0-PROTOTYPE-CONTRACT §8; human test §30). V0 asserts the language is *designed* +to make the contrast distinguishable and provides the evidence; it does not assert the +viewer test passed. + +--- + +## Accessibility posture + +- **Two signals, never colour alone.** Every role carries a prop / headwear / marker in + addition to its tint (§21); every state signal also has a non-colour carrier — table + discs *and* tabletop greying, station gauges whose **height** encodes queue depth + independently of colour, numeric queue-count labels, ring shape/position, and a text + legend in the HUD. A viewer with reduced colour discrimination still has a second + channel at every level. +- **Full accessibility work is deferred** (V0-PROTOTYPE-CONTRACT §7 non-goals; §13). V0 + does not ship a final UI, a colour-blind palette mode, contrast tuning, scalable text, + or any formal a11y conformance pass. The two-signal rule is the posture; a complete + accessibility treatment is out of scope for this presentation-only experiment and is + left for a later milestone. + +Treatment caveat: state colours were tuned against the "clean stylized" treatment +(brighter ambient 0.75, ACES). The "grounded" treatment (low ambient 0.22, muted) is +noticeably darker and **costs readability**; if it is ever chosen, the state palette will +need re-checking against it (`reports/visual/v0/screenshots/40-treatment-A-clean.png`, +`41-treatment-B-grounded.png`). + +--- + +## What this contract does not decide + +Per the non-goals (V0-PROTOTYPE-CONTRACT §7 / §13), the readability language here is a +**recommendation** expressed in a gray-box. It does not define the final animation +controller, VFX, shaders, UI, or icon art; it does not integrate `RestaurantSim.Core` or +consume authoritative state; and it does not approve any of the above. A gap in this +contract is **reported, not filled**. diff --git a/docs/art/SCALE-STANDARD.md b/docs/art/SCALE-STANDARD.md new file mode 100644 index 0000000..f36cf92 --- /dev/null +++ b/docs/art/SCALE-STANDARD.md @@ -0,0 +1,145 @@ +# V0 — Scale Standard (§17) + +**Track:** V0 — 3D Restaurant Readability Sandbox +**Status:** **LOCKED for V0** · provisional beyond V0 +**Source of truth:** `visual/RestaurantVisualSandbox/scripts/v0_config.gd` +**Contract:** see §17 (scale lock) and §18 (circulation) in +`docs/art/V0-PROTOTYPE-CONTRACT.md` + +This document records the single locked scale standard used by the V0 sandbox. It is a +presentation-only reference. V0 owns no gameplay truth and is not integrated with the +C# `RestaurantSim.Core` (M0); these numbers are visual-testing starting points, not +final architectural rules. V0 **recommends** a working scale — it does not approve +visual direction or authorize any downstream milestone. + +--- + +## 1. The unit rule + +> **1 Godot unit = 1 metre.** Metric throughout. + +Every distance in the sandbox is expressed in metres and consumed directly from the +config constant `SCALE_UNIT_M := 1.0`. Nothing in any scene is scaled per-scene "to +look right"; every mesh, camera, and layout reads its numbers from the same source so +the whole sandbox stays internally consistent. + +--- + +## 2. Dimensions table (locked) + +The full locked set of reference dimensions. Ranges, where given, are the visual-testing +tolerance recorded in the config; the single value is what V0 renders. + +| Element | Locked value | Config constant | Tolerance / note | +|---|---|---|---| +| Adult height | 1.80 m | `HUMAN_HEIGHT_M` | 1.75–1.85 m | +| Adult capsule radius | 0.25 m | `HUMAN_RADIUS_M` | shoulder half-width proxy | +| Door | 2.05 m × 0.95 m (H × W) | `DOOR_HEIGHT_M`, `DOOR_WIDTH_M` | H 2.0–2.1, W 0.85–1.0 | +| Table 2-top | 0.70 × 0.70 m | `TABLE_2TOP` | footprint | +| Table 4-top | 1.10 × 0.80 m | `TABLE_4TOP` | footprint | +| Table height | 0.75 m | `TABLE_HEIGHT_M` | 0.74–0.78 m | +| Chair seat height | 0.45 m | `CHAIR_SEAT_H_M` | 0.43–0.48 m | +| Chair footprint | 0.48 m | `CHAIR_FOOTPRINT_M` | — | +| Counter | 0.92 m | `COUNTER_HEIGHT_M` | 0.88–0.95 m | +| Appliance block | 0.90 m (H) × 0.80 m (W) | `APPLIANCE_H_M`, `APPLIANCE_W_M` | major appliance proxy | +| Ceiling / wall height | 3.00 m | `WALL_HEIGHT_M` | interior | +| Wall thickness | 0.15 m | `WALL_THICK_M` | — | +| Main dining aisle | 1.40 m | `AISLE_MAIN_M` | comfortable circulation (§18) | +| Kitchen working aisle | 1.10 m | `AISLE_KITCHEN_M` | working width (§18) | +| Intentional pinch | 0.85 m | `AISLE_PINCH_M` | deliberate narrow-risk choke point (§18) | + +Interior dividers are half-height (1.30 m) so the management camera reads over them; +that value lives in the layout script (`scripts/restaurant.gd`), not in the scale +config, and is a layout choice rather than a scale primitive. + +--- + +## 3. The matching rule + +> **Every asset is compared against the scale-reference scene, never the reverse.** + +The scale-reference scene is the fixed ruler. When an imported model does not match, the +**model** is adjusted (or rejected), and the reference standard is left untouched. The +sandbox is never re-scaled to flatter an asset. This keeps a single, stable metric frame +that all evidence and every scene share. + +--- + +## 4. Enforcement in code + +`scripts/v0_config.gd` is the **single source of truth** and is loaded as the `V0` +autoload singleton. Its header states the intent directly: + +> Single source of truth for the LOCKED scale standard (§17) […] Everything in the +> sandbox reads its numbers and colours from here so nothing is scaled "to look right" +> per-scene. + +Enforcement is by construction rather than by assertion: + +- Scene and placeholder geometry pull dimensions from the `V0.*` constants (e.g. + `V0.HUMAN_HEIGHT_M`, `V0.DOOR_HEIGHT_M`, `V0.TABLE_4TOP`, `V0.AISLE_PINCH_M`) instead + of inlining magic numbers. +- The scale-reference scene (`scripts/scale_reference.gd`) builds its labelled lineup + and its metre grid from those same constants, so the reference and the working scenes + cannot drift apart. +- Because the constants are `const` in one autoloaded file, changing the standard is a + one-line, one-place edit — and it propagates everywhere at once. + +Full config path: +`visual/RestaurantVisualSandbox/scripts/v0_config.gd`. + +--- + +## 5. The scale-reference scene + +`scripts/scale_reference.gd` renders a labelled lineup of known human-scale primitives +on a floor marked with a 1 m grid, so scale is legible even without reading every label. +The lineup places, left to right: a 1 m cube, an adult, a door frame, a 2-top table, a +4-top table, a chair, a counter, and an appliance block — each captioned with its title +and metric dimension pulled live from `V0.*`. Three floor swatches show the main-aisle +(1.40 m), kitchen-aisle (1.10 m), and pinch (0.85 m) widths for direct comparison. + +Its comment states the matching rule at the point of use: + +> the scene is NOT re-scaled to look right; assets are scaled to match this. + +### Screenshots (evidence) + +| View | File | +|---|---| +| Scale reference (perspective) | `reports/visual/v0/screenshots/50-scale-reference.png` | +| Scale reference (isometric) | `reports/visual/v0/screenshots/51-scale-reference-iso.png` | + +These are the two authoritative images of the locked standard. All other V0 evidence +(cameras, scenarios, overlays, treatments) shares this same metric frame. + +--- + +## 6. Asset-assisted scale finding + +A live import was used to check the human-height standard against a real, Godot-ready +asset (see `docs/art/ASSET-EVALUATION.md` for the full asset review; it is not +reproduced here). + +- **Asset:** `UAL1_Standard.glb`, from the Universal Animation Library [Standard] + (Quaternius, CC0 — confirmed at quaternius.com on 2026-07-29). +- **Method:** imported natively into Godot 4.7 and measured. +- **Result:** **1.83 m tall.** Against the 1.80 m standard that is a scale factor of + **0.984 — essentially 1:1, requiring no rescale.** The GLB carried 43 animations. +- **Evidence:** `reports/visual/v0/screenshots/60-asset-assisted-animation-lib.png`. + +This is an independent confirmation that the 1.80 m adult standard is compatible with a +production-grade humanoid asset with no rescaling. Source binaries stay in git-ignored +placeholders (`placeholders/_assetlab/`, `local-assets/`); nothing is committed to the +public repo. + +--- + +## 7. Lock status + +- **LOCKED for V0.** These values are fixed for the duration of this experiment and are + used by every scene, script, and captured image. +- **Provisional beyond V0.** The dimensions are visual-testing starting points, not + final architectural rules. V0 recommends this standard; it does not approve it for any + later milestone. Confirming or revising it beyond V0 is Howard & Aaron's decision. +- Any value not listed here or in `v0_config.gd` was **not measured in V0**. diff --git a/docs/art/V0-CURRENT-STATE.md b/docs/art/V0-CURRENT-STATE.md new file mode 100644 index 0000000..f225b72 --- /dev/null +++ b/docs/art/V0-CURRENT-STATE.md @@ -0,0 +1,249 @@ +# V0 — Current State (Living Handoff) + +> **Living handoff (§34).** Single source of truth for where the V0 3D Restaurant +> Readability Sandbox stands right now. Update this file as state changes. +> +> **What V0 is:** a presentation-only gray-box experiment. It owns **no** gameplay truth, +> is **not** integrated with the C# `RestaurantSim.Core` (M0), and **recommends only.** +> V0 does not approve visual direction, authorize M1, integrate the simulation, or merge +> its own PR — those decisions belong to Howard & Aaron. + +Last updated: 2026-07-29. + +--- + +## 1. State at a glance + +| Field | Value | +| --- | --- | +| Branch | `prototype/v0-3d-restaurant-sandbox` | +| Base commit | `06f0a7e` — "chore: establish base branch (license, gitignore, pointer)" (created from `origin/main`) | +| Current HEAD | advances with each commit — run `git log --oneline origin/main..HEAD` (do not trust a hash written here) | +| Engine | Godot 4.7.1-stable (`4.7.1.stable.official.a13da4feb`) | +| Renderer | Forward+, Metal 4.0 | +| Language | GDScript only (no C#) | +| Merged? | No. Nothing merged. The three M0 backend branches (PRs #1-3) are untouched. | +| PR | **Open: [#4](https://github.com/HSpector1/Restaurant/pull/4)** (unmerged, "do not merge, Art PM review"). | +| Verification | Independent 3-agent adversarial pass (boundary / evidence / definition-of-done): **non-blocking PASS**; findings addressed. | +| Latest Art PM decision | **None yet — awaiting review.** V0 recommends; it does not approve. | + +**Repo isolation.** The branch was cut from `origin/main` at base-only commit `06f0a7e`. +The M0 backend branches (`foundation/m0-headless-service-lab`, `fix/m0-audit-corrections`, +`fix/m0-price-forecast-integrity`, plus the local economic-coherence branch) are present in +the repo but untouched by V0 work. No V0 change has been merged anywhere. + +**Environment.** Apple M3 Max, macOS 26.5.1, arm64. Test resolution 1920x1080 on the +built-in Retina/ProMotion display. The Godot binary lives outside the repo +(`~/.local/opt/godot`) and is not committed. + +**Reproduce evidence.** `tools/v0/capture.sh` (editor scan → screenshots → perf probe). + +--- + +## 2. Completed + +The full sandbox is built and captured. All items below are done and have evidence. + +### Sandbox systems + +- **Scale lab (§ scale lock).** 1 Godot unit = 1 metre. Source of truth is + `scripts/v0_config.gd`. Every asset is compared against the scale-reference scene, never + the reverse. Locked dimensions: adult 1.80 m; door 2.05 x 0.95 m; table 2-top 0.70x0.70, + 4-top 1.10x0.80; table height 0.75 m; chair seat 0.45 m; counter 0.92 m; appliance + 0.90x0.80; ceiling 3.0 m; aisles main 1.40 m / kitchen 1.10 m / intentional pinch 0.85 m. + Evidence: `reports/visual/v0/screenshots/50-scale-reference.png`, + `reports/visual/v0/screenshots/51-scale-reference-iso.png`. + +- **Three cameras (`scripts/camera_rig.gd`).** + - **A ISO** — orthographic, fixed high 3/4 (yaw 45, pitch 35.264 true iso), 90-deg step + rotation, ortho-size zoom. Cleanest read; stable orientation; can feel distant. + - **B ORBIT** — perspective free orbit/pan/zoom, pitch clamp 14..80, distance 6..48, + fov 50. Best for inspection angles; can disorient / need adjustment. + - **C HYBRID** — perspective overview by default; `focus_on(target)` with a 0.6s cubic + tween and `return_to_overview`. Best balance: overview plus Level-2/3 inspection. + + Evidence: `01-camera-A-isometric.png`, `02-camera-B-orbit.png`, + `03-camera-C-hybrid-overview.png`, `04-camera-C-hybrid-focus-grill.png`. + +- **Wall/roof modes (`scripts/restaurant.gd`), camera-relative.** SOLID / AUTO_HIDE (hide + walls between camera and interior) / CUTAWAY (dollhouse, roof off) / TRANSPARENT (near + walls ~16% alpha). Roof hidden by default in management view; interior dividers are + half-height (1.30 m) so the camera sees over them. Evidence: `30-walls-solid.png`, + `31-walls-autohide.png`, `32-walls-cutaway.png`, `33-walls-transparent.png`, + `34-roof-on.png`. + +- **Gray-box restaurant (`scripts/restaurant.gd`).** One compact floor. 10 tables / + 32 seats. Zones: entrance + host stand + waiting (front-right); dining (centre-left); + compact kitchen (back-left) with four distinct stations Grill / Fry / Range / Garde plus + a pass line and a dishwashing zone; restrooms (back-right) reached by a deliberately + narrow ~0.85 m corridor (the circulation-risk pinch), with the open main aisle as the + obvious alternative; a staff/storage door. Evidence: `05-whole-restaurant.png`, + `06-kitchen-closeup.png`, `07-dining-closeup.png`. + +- **Four scripted scenarios (`scripts/mock_service.gd`, `scripts/service_view.gd`).** + A pure, seekable Mock / Scripted / VisualOnly timeline. `snapshot(scenario, t)` returns an + authored per-beat snapshot of FACTS; the renderer only decides HOW to show them (the + visual-truth boundary). Peak visual signatures below; evidence: + `10-scenario-healthy.png`, `11-scenario-kitchen-bottleneck.png`, + `12-scenario-foh-bottleneck.png`, `13-scenario-host-bottleneck.png`. + + | # | Scenario | Length | Peak signature | + | --- | --- | --- | --- | + | 0 | Healthy | 150s | Mostly blue (eating) tables, short green kitchen gauges, no alerts. The baseline. | + | 1 | Kitchen bottleneck | 165s | Tall RED grill gauge + many ORANGE (waiting-for-food) tables + servers massed idle at the pass (food not reaching them). | + | 2 | FOH bottleneck | 180s | Calm GREEN/idle kitchen gauges + YELLOW (waiting to order) / CYAN (needs payment) / BROWN (dirty) tables + a RED overloaded-server ring + plates piled at the pass. Kitchen is visibly NOT the problem. | + | 3 | Host & seating bottleneck | 90s | Entrance QUEUE with reddening patience pips + several DIRTY empty tables not reseated + IDLE green kitchen (spare capacity) + a customer leaving. | + +- **Table & kitchen states.** Table states modelled: available, preparing, occupied, + waiting-to-order, waiting-for-food, eating, needs-payment, dirty, being-cleaned. Kitchen + states: normal, queue, congested, food-ready, idle, unavailable. + +- **Overlays (§21-23).** OFF / MINIMAL (discs + station gauges + queue counts; default) / + FULL (adds patience pips, workload rings, pass plates, path hint lines) / SELECTION. + Evidence: `20-overlay-off.png`, `21-overlay-minimal.png`, `22-overlay-full.png`. + +- **Readability language (§21-23).** Role = colour + a second non-colour signal so it + never relies on colour alone: customer (tan / none), host (teal / clipboard), server + (blue / tray), cook (white / toque), busser (green / bin), manager (purple / star). + State colours: green=calm/available, blue=active/occupied/eating, amber=waiting(soft), + orange=waiting-too-long/queue, red=alert/congested/overloaded, brown=dirty, + cyan=ready/needs-payment, grey=idle. Table state = tinted tabletop + a floating disc; + kitchen station pressure = a vertical gauge (colour = state, height = queue). + +- **HUD (2D).** Scenario name, scripted clock, one-line headline, a role/state legend, and + a persistent "MOCK / SCRIPTED — not a simulation" tag. + +- **Two visual treatments (§27), same gray-box (`scripts/stage.gd`).** + - **A "clean stylized management sim"** — brighter ambient (0.75), soft light, ACES + tonemap, light SSAO. Most readable at management distance. + - **B "grounded but readable"** — low ambient (0.22), stronger sun + shadows + SSAO, + muted. Moodier / better for marketing stills but noticeably darker; costs readability. + + Evidence: `40-treatment-A-clean.png`, `41-treatment-B-grounded.png`. + +### Captured evidence + +- **24 screenshots** at 1920x1080 in `reports/visual/v0/screenshots/` (01–07, 10–13, + 20–22, 30–34, 40–41, 50–51, 60). +- **3 recordings** in `reports/visual/v0/recordings/`: + `rec-01-kitchen-service.mp4`, `rec-02-foh-service.mp4`, `rec-03-camera-orbit.mp4`. +- **Performance probe** at `reports/visual/v0/performance/performance.csv` (see §5). + +--- + +## 3. Partial / limited + +These items exist but ship at a deliberately reduced scope for V0. + +- **Recordings are a few short clips**, not a full guided tour. Three MP4s cover the + kitchen-bottleneck service, the FOH-bottleneck service, and a camera orbit. They are + short and illustrative; they are not a comprehensive walkthrough of every scenario, state, + or overlay combination. The 24 stills carry the bulk of the evidence. + +- **Treatment B is noticeably darker** and, per §27, costs readability at management + distance. It reads better for marketing stills but is not the recommended default for + the management read. This is a known tradeoff, not a defect — see + `41-treatment-B-grounded.png` against `40-treatment-A-clean.png`. + +- **Hybrid click-to-select is an F-cycle in V0.** Camera C's inspection focus is driven by + a cycle key rather than a true point-and-click pick against scene geometry. The + focus/return behaviour (`focus_on` / `return_to_overview`, 0.6s cubic tween) is fully + working; only the *selection mechanism* is a stand-in. A production pick would ray-cast + from the cursor. Evidence: `04-camera-C-hybrid-focus-grill.png`. + +--- + +## 4. Unstarted / deferred + +- **Human visual test (§30) — prepared, NOT run.** The materials and protocol are ready, + but the test itself is Howard & Aaron's to conduct. No human readability results exist + yet. + +- **Production anything — out of scope by decision (§13 non-goals).** Confirmed absent and + intentionally not built: + - No `RestaurantSim.Core` integration. + - No authoritative demand / economics / satisfaction. + - No production customer / staff AI. + - No real pathfinding / navmesh. + - No construction / inventory / suppliers / recipes / menu / pricing UI / scheduling / + hiring / saving. + - No campaign / multi-restaurant / city sim / reviews / marketing / progression. + - No final animation controller / audio / lighting / shaders / VFX / UI. + - No multiplayer / mods / Steam. + +--- + +## 5. Performance status + +Source: `reports/visual/v0/performance/performance.csv`. M3 Max, vsync OFF, 1920x1080. + +| config | actors | mesh_inst | draw_calls | primitives | avg_fps | worst_fps | avg_ms | worst_ms | vram_mb | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| empty_restaurant | 9 | 268 | 671 | 136,926 | 257 | 116 | 3.89 | 8.65 | 332.6 | +| healthy_service | 32 | 329 | 848 | 399,046 | 358 | 330 | 2.79 | 3.03 | 338.0 | +| peak_kitchen_full | 31 | 342 | 758 | 450,408 | 322 | 72 | 3.11 | 13.91 | 337.8 | +| peak_foh_all_diagnostics | 25 | 320 | 708 | 258,564 | 361 | 330 | 2.77 | 3.03 | 336.5 | +| max_zoom_out | 31 | 342 | 791 | 474,844 | 360 | 330 | 2.78 | 3.03 | 337.8 | + +**Verdict.** Far exceeds the 60 FPS target. Typical frame ~3 ms (~330 FPS); worst single +frame 13.9 ms (72 FPS, a one-off hitch); ~335 MB VRAM; under 850 draw calls throughout. +These are **uncapped headroom** numbers — vsync-on would cap to display refresh. + +**Targets (§28).** 1920x1080, 60 FPS target, 30 FPS minimum acceptable. All configs clear +the target and the minimum with large margin, even at worst-frame. + +--- + +## 6. Known defects + +- **Peak-kitchen one-off frame hitch.** `peak_kitchen_full` shows a single worst frame of + 13.91 ms (72 FPS). Still above the 60 FPS target; logged as a one-off, not a sustained + regression. +- **Hybrid selection is a cycle, not a pick** (see §3). Functional for V0 evidence but not + the intended production interaction. +- **Treatment B readability cost** (see §3). Expected per §27, recorded here so it is not + mistaken for a bug. + +No other defects observed. Values not listed here were **not measured in V0.** + +--- + +## 7. Asset-license status + +Full detail lives in `docs/art/ASSET-EVALUATION.md` (already written; not recreated here). +Summary: + +| Asset | License | Status | +| --- | --- | --- | +| Downtown City MegaKit [Standard] (Quaternius) | CC0 | Cleared. Not committed (hygiene). | +| Universal Animation Library [Standard] (Quaternius) | CC0 (confirmed at quaternius.com 2026-07-29; bundled `License.txt` was empty) | Cleared, Godot-ready GLB. | +| `FBX-...zip` (loose props) | No license | Do-not-use. | +| `wintersets.zip` | Legacy game mod, presumed proprietary | Do-not-use. | + +**Asset-assisted test.** `UAL1_Standard.glb` imported natively into Godot 4.7, measured +1.83 m tall (scale factor 0.984 to the 1.80 m standard → essentially 1:1, no rescale), +43 animations. Evidence: `60-asset-assisted-animation-lib.png`. Source binaries stay in +git-ignored `placeholders/_assetlab/` and `local-assets/`; nothing committed to the public +repo. + +--- + +## 8. Scope status + +- **Timebox (§33):** 12–20 focused builder hours. V0 stayed within this window. +- **Non-goals (§13):** confirmed absent (see §4). The absence is a decision, not an + oversight. +- **Boundary held:** V0 owns no gameplay truth, is not integrated with M0, and touched none + of the backend branches. The visual-truth boundary (authored FACTS in; render decides + HOW) held across all four scenarios. + +--- + +## 9. Latest Art PM decision + +**None yet — awaiting review.** + +V0 is complete as a gray-box readability experiment and produces a recommendation for +review. It does **not** approve visual direction, authorize M1, integrate the simulation, +or merge its own PR. Those calls are Howard & Aaron's. The prepared human visual test +(§30) is theirs to run before any direction is chosen. diff --git a/docs/art/V0-DECISION-LOG.md b/docs/art/V0-DECISION-LOG.md new file mode 100644 index 0000000..fb764f5 --- /dev/null +++ b/docs/art/V0-DECISION-LOG.md @@ -0,0 +1,273 @@ +# V0 — Decision Log (§34) + +**Track:** V0 — 3D Restaurant Readability Sandbox +**Branch:** `prototype/v0-3d-restaurant-sandbox` (from `origin/main` `06f0a7e`, base only; nothing merged) +**Scope of this log:** the material decisions taken while building the V0 readability +sandbox. V0 is a presentation-only gray-box experiment. It owns no gameplay truth, is +not integrated with the C# `RestaurantSim.Core` (M0), and **recommends only** — it does +not approve the visual direction, authorize M1, integrate the simulation, or merge its +own PR. Those calls belong to the product owners (Howard & Aaron). See the V0 Prototype +Contract (`docs/art/V0-PROTOTYPE-CONTRACT.md`) for the governing rules. + +## How to read this log + +- **Status** is either **LOCKED** (fixed for the duration of the V0 experiment; changing + it would invalidate V0's evidence and would be a new decision) or **PROVISIONAL** (a + working choice V0 makes to gather evidence; explicitly open for the owners to overturn). +- **Owner** is who holds the decision. Where a row is marked *Owners*, it means the + decision or its confirmation is reserved to Howard & Aaron; V0 records a recommendation + only. +- **Revisit condition** states the concrete trigger that should reopen the row. +- All decisions below are dated **2026-07-29**. + +--- + +## Decision table + +| # | Date | Decision | Status | Owner | Revisit condition | +|---|---|---|---|---|---| +| D1 | 2026-07-29 | **Engine = Godot 4.7.1-stable**, Forward+ renderer, GDScript only, for V0. | **LOCKED** (V0) | V0 builder | If the owners select a different engine for M1, or if a Godot 4.7.x defect blocks capture. | +| D2 | 2026-07-29 | **Scale = 1 Godot unit = 1 metre**, metric throughout; `scripts/v0_config.gd` is the single source of truth. | **LOCKED** (V0) | V0 builder | Only by owner decision on final metric conventions for M1; not to be relaxed inside V0. | +| D3 | 2026-07-29 | **Procedural GDScript gray-box** (primitives + flat colour) rather than hand-authored scene files. | **PROVISIONAL** | V0 builder | When a real art/asset pipeline begins (M1); or if procedural layout blocks a needed readability test. | +| D4 | 2026-07-29 | **Pure, seekable Mock snapshot design** honouring the visual-truth boundary: `snapshot(scenario, t)` returns authored facts; the renderer only chooses how to show them. | **LOCKED principle** (permanent) | V0 builder / owners (principle) | Never relaxed. The boundary carries forward to M1; only the *mechanism* (scripted vs. real sim) changes. | +| D5 | 2026-07-29 | **Default overlay = MINIMAL** (discs + station gauges + queue counts). | **PROVISIONAL** | Owners (via human visual test) | After the §30 human visual test; if MINIMAL under- or over-informs first-time viewers. | +| D6 | 2026-07-29 | **Treatment A ("clean stylized") = readability baseline**; Treatment B ("grounded") offered as the moodier alternative. | **PROVISIONAL** | Owners (art direction) | After the human visual test; when a final art-direction decision is made. | +| D7 | 2026-07-29 | **Asset licensing:** two CC0 Quaternius packs cleared for local evaluation; the two unlicensed packs rejected (do-not-use). | **LOCKED** | V0 builder / owners | Only if the owner establishes provenance/rights for a rejected pack, or a cleared pack's license changes. | +| D8 | 2026-07-29 | **Roof off by default + camera-relative wall AUTO_HIDE** in the management view; interior dividers half-height (1.30 m). | **PROVISIONAL** | V0 builder | After the human visual test; if AUTO_HIDE flickers/obstructs, prefer CUTAWAY or TRANSPARENT. | +| D9 | 2026-07-29 | **Human visual test deferred to the owners** — prepared (§30) but not run by V0. | **LOCKED** (authority) | Owners (Howard & Aaron) | N/A — this is a standing authority boundary, not a revisitable technical choice. | + +--- + +## Decision detail + +Each entry records rationale, the alternatives considered, and the consequences of the +choice, so the reasoning is recoverable later. + +### D1 — Engine = Godot 4.7.1-stable (LOCKED for V0) + +- **Decision.** Build V0 in Godot `4.7.1.stable.official.a13da4feb`, Forward+ renderer + (Metal 4.0 on Apple Silicon), GDScript only. The binary lives outside the repo + (`~/.local/opt/godot`) and is not committed. +- **Rationale.** The assignment fixes the engine (§7). Forward+ is Godot's high-fidelity + path and runs natively on the M3 Max test machine. GDScript keeps the sandbox + engine-independent from the C# `RestaurantSim.Core`, which is the whole point of the + visual-truth boundary (§15). +- **Alternatives.** A C#/.NET Godot setup (rejected: would blur the sim/render boundary + and add a build dependency V0 does not need); the Mobile/Compatibility renderers + (rejected: no fidelity benefit here, and Forward+ already clears the perf target by a + wide margin — see performance below); a different engine entirely (out of scope for + V0; an owner decision for M1). +- **Consequences.** V0's evidence is Godot-specific. Renderer feature availability + (SSAO, ACES tonemap) is what shipped with 4.7.1. Because the binary is not committed, + reproduction requires the same Godot version locally; `tools/v0/capture.sh` documents + the flow. +- **Evidence.** All screenshots under `reports/visual/v0/screenshots/`; capture flow in + `tools/v0/capture.sh`. + +### D2 — Scale = 1u = 1 metre (LOCKED for V0) + +- **Decision.** One Godot unit equals one metre, metric everywhere. The authoritative + dimensions live in `scripts/v0_config.gd` and are consumed by every scene. Every + asset is compared *against* the scale-reference scene, never the reverse. +- **Rationale.** A single, explicit metric standard is a precondition for judging + readability at "management distance" and for testing whether imported assets are the + right size. Locking it prevents per-scene "scaled to look right" drift, which would + make cross-scene comparison meaningless. +- **Alternatives.** Per-scene arbitrary units (rejected: destroys comparability); + imperial units (rejected: the reference dimensions are metric — adult 1.80 m, door + 2.05 × 0.95 m, table height 0.75 m, counter 0.92 m, ceiling 3.0 m, aisles 1.40 / 1.10 + / 0.85 m). +- **Consequences.** The asset-assisted animation test validated cleanly against this + lock: `UAL1_Standard.glb` measured 1.83 m against the 1.80 m standard (scale factor + 0.984 — essentially 1:1, no rescale needed). These starting dimensions are for + visual testing, not final architecture; final metric conventions for M1 are an owner + decision. +- **Evidence.** `reports/visual/v0/screenshots/50-scale-reference.png`, + `51-scale-reference-iso.png`, `60-asset-assisted-animation-lib.png`. + +### D3 — Procedural GDScript gray-box over hand-authored scenes (PROVISIONAL) + +- **Decision.** Generate the restaurant, actors, and diagnostic overlays procedurally in + GDScript (`scripts/restaurant.gd`, etc.) using primitives and flat colours, rather + than hand-authoring `.tscn` scene files or importing an environment pack. +- **Rationale.** The placeholder-first policy (§11) requires proving scale → camera → + layout → readability → performance *before* any art pack. Procedural layout makes the + gray-box cheap to iterate, keeps dimensions bound to the scale config, and keeps the + sandbox asset-agnostic within the 12–20 hour timebox (§33). +- **Alternatives.** Hand-authored scenes (rejected for V0: slower to iterate, more prone + to silent scale drift, and premature before layout is settled); importing the CC0 + environment pack up front (rejected: violates placeholder-first; the packs are + evaluated *against* the sandbox, they do not define it). +- **Consequences.** V0 looks deliberately gray-box; it is not a visual-fidelity artifact. + When a real art pipeline begins at M1, the procedural approach is expected to give way + to authored/imported assets. Provisional precisely because that hand-off is coming. +- **Evidence.** `reports/visual/v0/screenshots/05-whole-restaurant.png`, + `06-kitchen-closeup.png`, `07-dining-closeup.png`. + +### D4 — Pure, seekable Mock snapshot design honouring the visual-truth boundary (LOCKED principle) + +- **Decision.** The scripted service is a **pure, seekable** timeline: `snapshot(scenario, t)` + (in `scripts/mock_service.gd` / `scripts/service_view.gd`) returns an authored per-beat + snapshot of *facts*, and the renderer decides only *how* to show them. The output is + named `Mock` / `Scripted` / `VisualOnly` and carries a persistent + "MOCK / SCRIPTED — not a simulation" HUD tag; nothing is named `SimulationResult`. +- **Rationale.** This is the visual-truth boundary (§15) made concrete. The renderer may + choose interpolation, icon placement, camera framing, cosmetic timing — but it may + never decide whether a customer was served, money earned, a station truly overloaded, + or a table available. Making the timeline pure and seekable lets any moment be captured + deterministically and forces the boundary to be honoured in code, not just documented. +- **Alternatives.** An ad-hoc stateful animation driver (rejected: not seekable, and it + tempts the renderer to invent facts); a mini real simulation (rejected outright — V0 + owns no gameplay truth and does not integrate `RestaurantSim.Core`). +- **Consequences.** Deterministic, reproducible captures for all four scenarios. The + boundary carries forward permanently: at M1 the *mechanism* changes (real sim + snapshots replace the scripted ones) but the contract — renderer reads facts, never + authors them — does not. This is why the row is LOCKED as a principle. +- **Scenarios (peak signatures).** Healthy (150 s) — mostly blue eating tables, short + green kitchen gauges, no alerts; Kitchen bottleneck (165 s) — tall red grill gauge, + many orange waiting-for-food tables, servers massed idle at the pass; FOH bottleneck + (180 s) — calm green kitchen but yellow/cyan/brown tables and a red overloaded-server + ring (kitchen visibly not the problem); Host bottleneck (90 s) — reddening entrance + queue, dirty un-reseated tables, idle green kitchen, a customer leaving. +- **Evidence.** `reports/visual/v0/screenshots/10-scenario-healthy.png`, + `11-scenario-kitchen-bottleneck.png`, `12-scenario-foh-bottleneck.png`, + `13-scenario-host-bottleneck.png`; recordings `rec-01-kitchen-service.mp4`, + `rec-02-foh-service.mp4`. + +### D5 — Default overlay = MINIMAL (PROVISIONAL) + +- **Decision.** Ship MINIMAL as the default overlay level: table-state discs, kitchen + station gauges, and queue counts. OFF, FULL (adds patience pips, workload rings, pass + plates, path-hint lines), and SELECTION are also implemented and switchable. +- **Rationale.** MINIMAL is the hypothesised sweet spot: enough non-diegetic help for a + first-time viewer to locate congestion without burying the scene in HUD clutter. It + pairs the always-on second signals (discs, gauges) with the readability colour + language (§21–23). +- **Alternatives.** OFF by default (rejected as a default: relies too heavily on the + viewer reading raw geometry/colour); FULL by default (rejected as a default: risks + over-cluttering and masking whether the *scene itself* is readable — the actual V0 + question). +- **Consequences.** The default is a working hypothesis, not a verdict. Whether MINIMAL + is the right resting state is exactly what the human visual test (§30) should decide; + hence PROVISIONAL, owned by the owners via that test. +- **Evidence.** `reports/visual/v0/screenshots/20-overlay-off.png`, + `21-overlay-minimal.png`, `22-overlay-full.png`. + +### D6 — Treatment A as readability baseline (PROVISIONAL) + +- **Decision.** Use Treatment A ("clean stylized management sim" — brighter ambient + 0.75, soft light, ACES tonemap, light SSAO) as the readability baseline. Treatment B + ("grounded but readable" — low ambient 0.22, stronger sun + shadows + SSAO, muted) is + provided as the alternative. Both render the same gray-box (`scripts/stage.gd`). +- **Rationale.** A is the most readable at management distance, which is the priority for + the V0 question. B is moodier and likely better for marketing stills but is noticeably + darker and costs readability — a real trade-off the owners should see side by side + rather than have V0 pre-decide. +- **Alternatives.** Adopt B as the baseline (rejected for V0's readability purpose, + though noted as the stronger marketing look); ship only one treatment (rejected: the + owners need the comparison to make an art-direction call). +- **Consequences.** V0 recommends A for readability but explicitly defers the final + art-direction choice. PROVISIONAL, owned by the owners. +- **Evidence.** `reports/visual/v0/screenshots/40-treatment-A-clean.png`, + `41-treatment-B-grounded.png`. + +### D7 — CC0 Quaternius packs cleared; other two packs rejected (LOCKED) + +- **Decision.** Clear the two Quaternius CC0 packs for **local evaluation only** + (Downtown City MegaKit [Standard]; Universal Animation Library [Standard]); reject the + two unlicensed packs as do-not-use (`FBX-…zip` loose props — no license; `wintersets.zip` + — legacy game mod, presumed proprietary). No source binary is committed to the public + repo. +- **Rationale.** License hygiene (§10). CC0 was confirmed twice for the cleared packs + (bundled license file and the live source page — the UAL bundled `License.txt` was + empty, so CC0 was verified at quaternius.com on 2026-07-29). "Unclear or missing + license is rejected by default." Ownership of a pack is not permission to upload it to + a public repo, so even cleared packs stay out of git (large-binary hygiene). +- **Alternatives.** Commit cleared packs into the repo (rejected: hygiene / size, not + needed for a gray-box); use the loose FBX props because they are the most + restaurant-relevant (rejected: no provenance); use `wintersets` (rejected: presumed + proprietary). +- **Consequences.** The asset-assisted animation test used UAL from a git-ignored local + extract only. Source binaries live in `placeholders/_assetlab/` and `local-assets/` + (both git-ignored). The rejections stand unless the owner establishes rights. +- **Evidence.** `docs/art/ASSET-EVALUATION.md` (full provenance table); + `reports/visual/v0/screenshots/60-asset-assisted-animation-lib.png`. + +### D8 — Roof-off + camera-relative wall AUTO_HIDE (PROVISIONAL) + +- **Decision.** In the management view, hide the roof by default and use camera-relative + wall **AUTO_HIDE** (hide the walls between the camera and the interior). Interior + dividers are modelled half-height (1.30 m) so the camera reads over them. SOLID, + CUTAWAY (dollhouse, roof off), and TRANSPARENT (near walls ~16% alpha) are also + implemented (`scripts/restaurant.gd`). +- **Rationale.** The pass criterion (§31) requires walls/roof that do not routinely + obstruct. Roof-off plus AUTO_HIDE keeps the interior legible from the default camera + while preserving enough structure to read zones, versus a fully open dollhouse. +- **Alternatives.** SOLID walls (rejected as default: obstructs the interior — the whole + problem); CUTAWAY/TRANSPARENT as default (viable fallbacks, kept available; AUTO_HIDE + chosen first as the least visually disruptive of the interior read). +- **Consequences.** AUTO_HIDE depends on camera position; if it flickers or obstructs at + certain angles during the human visual test, CUTAWAY or TRANSPARENT are the ready + fallbacks. PROVISIONAL pending that test. +- **Evidence.** `reports/visual/v0/screenshots/30-walls-solid.png`, + `31-walls-autohide.png`, `32-walls-cutaway.png`, `33-walls-transparent.png`, + `34-roof-on.png`. + +### D9 — Human visual test deferred to owners (LOCKED authority boundary) + +- **Decision.** V0 prepares the §30 human visual test but does **not** run it. Conducting + it — and drawing the readability verdict from it — is reserved to Howard & Aaron. +- **Rationale.** V0's authority is to recommend, not to approve the visual direction, + authorize M1, integrate the simulation, or merge its own PR (§9 of the contract). The + human readability judgement is precisely the decision V0 must not make for the owners. +- **Alternatives.** Have the V0 builder self-assess readability and declare a pass + (rejected: exceeds V0's authority and would not be an independent first-time-viewer + test — the very thing the test measures). +- **Consequences.** V0 delivers the sandbox, the captured evidence, and a recommendation + in `reports/visual/v0/V0-REPORT.md`; the pass/fail readability call awaits the owners' + test. This is a standing authority boundary, not a revisitable technical choice. + +--- + +## Supporting evidence: performance (context for D1) + +Measured on the M3 Max, vsync OFF, 1920×1080 (`reports/visual/v0/performance/performance.csv`). +Recorded here because it is why the engine/renderer lock (D1) carried no performance +cost. Targets (§28): 60 FPS target, 30 FPS minimum acceptable. + +| Config | Actors | Draw calls | Avg FPS | Worst FPS | Avg frame (ms) | Worst frame (ms) | VRAM (MB) | +|---|---|---|---|---|---|---|---| +| empty_restaurant | 9 | 671 | 257 | 116 | 3.89 | 8.65 | 332.6 | +| healthy_service | 32 | 848 | 358 | 330 | 2.79 | 3.03 | 338.0 | +| peak_kitchen_full | 31 | 758 | 322 | 72 | 3.11 | 13.91 | 337.8 | +| peak_foh_all_diagnostics | 25 | 708 | 361 | 330 | 2.77 | 3.03 | 336.5 | +| max_zoom_out | 31 | 791 | 360 | 330 | 2.78 | 3.03 | 337.8 | + +**Verdict.** Far exceeds the 60 FPS target; typical frame ~3 ms (~330 FPS); worst single +frame 13.9 ms (72 FPS — a one-off hitch); ~335 MB VRAM; under 850 draw calls. These are +uncapped headroom numbers; vsync-on would cap to display refresh. + +--- + +## Cameras (context, not a locked default) + +Three camera candidates are implemented (`scripts/camera_rig.gd`); V0 does **not** lock a +default camera as a numbered decision — the choice is part of what the owners' test should +settle. Recorded for completeness: + +- **A — ISO** (orthographic, true iso yaw 45 / pitch 35.264, 90° step rotation): cleanest + read, stable orientation, can feel distant. `01-camera-A-isometric.png`. +- **B — ORBIT** (perspective free orbit/pan/zoom, pitch 14–80, distance 6–48, fov 50): + best for inspection angles, can disorient. `02-camera-B-orbit.png`. +- **C — HYBRID** (perspective overview + `focus_on(target)` 0.6 s cubic tween + + `return_to_overview`): best balance of overview and Level-2/3 inspection. + `03-camera-C-hybrid-overview.png`, `04-camera-C-hybrid-focus-grill.png`. + +--- + +## Standing constraints (not decisions — boundaries these decisions respect) + +- V0 owns no gameplay truth and is not integrated with `RestaurantSim.Core` (M0). +- Non-goals (§13) are deliberately absent and were not built. +- Nothing is merged; the three M0 backend branches are untouched. +- V0 recommends; the owners approve. diff --git a/docs/art/V0-NEXT-ACTION.md b/docs/art/V0-NEXT-ACTION.md new file mode 100644 index 0000000..77c4c64 --- /dev/null +++ b/docs/art/V0-NEXT-ACTION.md @@ -0,0 +1,150 @@ +# V0 — Next Authorized Action (Living Handoff, §34) + +Re-confirm the repository's actual visual-sandbox state before trusting this document. +This file is a living handoff and can drift; the repository (branch, commits, evidence +files) is the source of truth. What follows describes state as recorded on 2026-07-29 and +should be re-verified against `git status`, `git log`, and the on-disk evidence before it +is acted on. + +## What V0 is (and is not) + +V0 is a **presentation-only, gray-box readability experiment** in Godot 4.7.1-stable +(Forward+, Metal). It owns **no gameplay truth** and is **not integrated** with the C# +`RestaurantSim.Core` (M0). Its scripted service timeline is a pure, seekable +`Mock / Scripted / VisualOnly` authoring of facts — the renderer only decides *how* to +show them, never *what* is true (the visual-truth boundary, §21-23). + +V0 **RECOMMENDS only.** It does not approve visual direction, authorize M1, integrate the +simulation, or merge its own PR. Those decisions belong to Howard & Aaron. + +## The ONE next authorized action + +The V0 pull request is **now open — PR #4** (`prototype/v0-3d-restaurant-sandbox` → `main`, +unmerged, "do not merge"). So the single next authorized action is: + +**Run the lightweight human visual test (§30) with Howard & Aaron against PR #4's evidence, +then make the one Art PM decision in the report (§K).** + +That is the entire authorized scope of the next step. Nothing beyond it is authorized (see +"Explicitly NOT authorized" below). + +### Current branch / PR state (re-verify before acting) + +| Item | Recorded state (2026-07-29) | +| --- | --- | +| Branch | `prototype/v0-3d-restaurant-sandbox` (from `origin/main` @ `06f0a7e`, base only) | +| Commits | several commits ahead of `origin/main` (run `git log --oneline origin/main..HEAD`); working tree clean | +| PR | **Open: [#4](https://github.com/HSpector1/Restaurant/pull/4)** — unmerged, "do not merge, Art PM review" | +| M0 backend branches | Untouched. Nothing merged. | +| Remote | `origin` → `github.com/HSpector1/Restaurant.git` | + +The three M0 backend branches are untouched and nothing is merged; the V0 branch is fully +isolated (§13). + +### Step 1 — Open the PR for Art PM review + +- Push the branch and open a PR from `prototype/v0-3d-restaurant-sandbox` into the base it + branched from (`origin/main` @ `06f0a7e`). +- The PR is a **review artifact, not a merge request.** Merge authority is Howard & + Aaron's. Do not self-merge. +- Point reviewers at the standing evidence so review does not require a local build: + - `docs/art/V0-PROTOTYPE-CONTRACT.md` (scope, §-numbered) + - `docs/art/ASSET-EVALUATION.md` (asset provenance / licensing — already written) + - `reports/visual/v0/screenshots/` (24 committed stills) + - `reports/visual/v0/performance/performance.csv` (perf probe results) + +### Step 2 — Run the lightweight human visual test (§30) + +The human visual test is **prepared but NOT run.** Running it is Howard & Aaron's to +conduct; V0 does not score its own readability. The test compares the four scripted peak +scenarios and asks whether each bottleneck is legible *at management distance without the +HUD headline* — i.e. can a viewer name the problem from the floor alone. + +Suggested viewing order and what each scenario should read as: + +| Scenario | Peak signature to confirm is legible | Evidence still | +| --- | --- | --- | +| 0 Healthy (150s) | mostly blue (eating) tables, short green kitchen gauges, no alerts — the baseline | `reports/visual/v0/screenshots/10-scenario-healthy.png` | +| 1 Kitchen bottleneck (165s) | tall RED grill gauge + many ORANGE (waiting-for-food) tables + servers massed idle at the pass | `reports/visual/v0/screenshots/11-scenario-kitchen-bottleneck.png` | +| 2 FOH bottleneck (180s) | calm GREEN kitchen + YELLOW/CYAN/BROWN tables + a RED overloaded-server ring + plates piled at the pass; kitchen visibly NOT the problem | `reports/visual/v0/screenshots/12-scenario-foh-bottleneck.png` | +| 3 Host bottleneck (90s) | entrance QUEUE with reddening patience pips + DIRTY unreseated tables + IDLE green kitchen + a customer leaving | `reports/visual/v0/screenshots/13-scenario-host-bottleneck.png` | + +Secondary A/B decisions the test can also surface a **recommendation** on (not an +approval): camera candidate (A ISO / B ORBIT / C HYBRID), wall mode (SOLID / AUTO_HIDE / +CUTAWAY / TRANSPARENT), overlay level (OFF / MINIMAL / FULL), and visual treatment +(A clean vs B grounded). Relevant stills: `01-camera-A-isometric.png`, +`02-camera-B-orbit.png`, `03-camera-C-hybrid-overview.png`, +`04-camera-C-hybrid-focus-grill.png`, `30-walls-solid.png`, `31-walls-autohide.png`, +`32-walls-cutaway.png`, `33-walls-transparent.png`, `20-overlay-off.png`, +`21-overlay-minimal.png`, `22-overlay-full.png`, `40-treatment-A-clean.png`, +`41-treatment-B-grounded.png` (all under `reports/visual/v0/screenshots/`). + +## How to reproduce the evidence + +Regenerate all committed screenshots and the performance CSV with one command. The Godot +binary lives outside the repo (`~/.local/opt/godot`) and is not committed; override the +path with `GODOT=...` if it differs. + +``` +tools/v0/capture.sh +# or, with an explicit engine path: +GODOT=/path/to/Godot tools/v0/capture.sh +``` + +The script runs an editor class-name scan, renders the committed shot list to +`reports/visual/v0/screenshots/`, then runs the performance probe into +`reports/visual/v0/performance/performance.csv` — no manual steps. + +## How to open it interactively + +To explore live (drive cameras, switch scenarios, toggle overlays and wall modes): + +``` +Godot --path visual/RestaurantVisualSandbox +``` + +Substitute the actual Godot 4.7.1-stable binary path if `Godot` is not on `PATH` (the same +binary `capture.sh` uses, e.g. `~/.local/opt/godot/Godot.app/Contents/MacOS/Godot`). + +## Grounding facts for the reviewer (verified in V0) + +- **Scale lock:** 1 Godot unit = 1 metre; source of truth `scripts/v0_config.gd`. Every + asset is compared against the scale-reference scene, never the reverse + (`50-scale-reference.png`, `51-scale-reference-iso.png`). +- **Restaurant:** one compact floor, 10 tables / 32 seats, four distinct kitchen stations + (Grill/Fry/Range/Garde) + pass line + dishwashing, and a deliberate ~0.85 m + restroom-corridor pinch (the circulation-risk case) with the open main aisle as the + obvious alternative (`05-whole-restaurant.png`, `06-kitchen-closeup.png`, + `07-dining-closeup.png`). +- **Readability language (§21-23):** role = colour + a second non-colour signal (e.g. + cook = white + toque, busser = green + bin) so it never relies on colour alone. +- **Performance (§28):** far exceeds the 60 FPS target. On M3 Max, vsync OFF, 1920x1080, + typical frame ~3 ms (~330 FPS); worst single frame 13.9 ms (72 FPS, a one-off hitch); + ~335 MB VRAM; <850 draw calls. These are uncapped headroom numbers; vsync-on would cap + to display refresh. Full table: `reports/visual/v0/performance/performance.csv`. +- **Asset-assisted check:** the CC0 Universal Animation Library GLB imported natively into + Godot 4.7 measured 1.83 m tall (scale factor 0.984 to the 1.80 m standard — essentially + 1:1, no rescale), 43 animations (`60-asset-assisted-animation-lib.png`). Source + binaries stay in git-ignored placeholders; nothing asset-binary is committed to the + public repo. Details in `docs/art/ASSET-EVALUATION.md`. +- **Timebox (§33):** 12-20 focused builder hours. + +Any value not listed above or in the contract is **not measured in V0.** + +## Explicitly NOT authorized + +The following are out of scope and must **not** begin as a result of this handoff: + +- **M1** — do not authorize or begin. +- **Simulation integration** — no wiring to `RestaurantSim.Core` (M0); the visual-truth + boundary stays intact. +- **Final art** — no final animation controller, audio, lighting, shaders, VFX, or UI; + V0 stays gray-box (§13). +- **Self-approval / self-merge** — V0 recommends; Howard & Aaron approve the visual + direction and merge the PR. +- Anything else on the §13 non-goals list (production AI, real pathfinding/navmesh, + economics/demand/satisfaction, construction/inventory/menus, campaign/progression, + multiplayer/mods/Steam). + +If phases-1-4-equivalent V0 work reads as complete, say so and stop. Do not begin the next +phase until the repo owners explicitly authorize it. diff --git a/docs/art/V0-PROTOTYPE-CONTRACT.md b/docs/art/V0-PROTOTYPE-CONTRACT.md new file mode 100644 index 0000000..741b2c8 --- /dev/null +++ b/docs/art/V0-PROTOTYPE-CONTRACT.md @@ -0,0 +1,111 @@ +# V0 — Visual Prototype Contract + +**Track:** V0 — 3D Restaurant Readability Sandbox · **Status:** LOCKED for this experiment +**Branch:** `prototype/v0-3d-restaurant-sandbox` · **Baseline:** `origin/main` (`06f0a7e`) +**This document is the shared visual contract.** Every scene, script, and doc in the +sandbox conforms to it. Where it and instinct disagree, this contract wins. + +--- + +## 1. The one question V0 answers + +> Can a first-time viewer look at one small 3D restaurant during an active service +> and quickly understand **where customers are, what employees are doing, where +> congestion is forming, and which part of the restaurant needs attention** — without +> the developer narrating every icon, station, or character? + +V0 does **not** test whether the economy is balanced. It tests *readability*. + +## 2. What V0 is / is not + +V0 **is** a deliberately isolated visual experiment: a gray-box restaurant, three +camera candidates, a hand-scripted service, and diagnostic overlays, rendered in +Godot, with captured evidence and a gate recommendation. + +V0 **is not** M0 / M0.5 / M1, a vertical slice, a production art pass, a restaurant +builder, or a simulation-integration milestone. See `../design`-equivalent non-goals +in §7 below. + +## 3. Engine lock (§7 of the assignment) + +| | | +|---|---| +| Engine | **Godot 4.7.1-stable** (`4.7.1.stable.official.a13da4feb`) | +| Renderer | **Forward+** (Metal 4.0 on Apple Silicon) | +| Scripting | **GDScript only.** V0 does **not** integrate the C# `RestaurantSim.Core`. | +| Binary location | `~/.local/opt/godot/Godot.app` (outside the repo; not committed) | + +The Godot project is a presentation sandbox only. It remains engine-independent from +the C# simulation so the two can coexist later without either owning the other. + +## 4. Scale lock (§17) + +**1 Godot unit = 1 metre.** Metric throughout. The authoritative numbers live in +`visual/RestaurantVisualSandbox/scripts/v0_config.gd` and are consumed everywhere — +nothing is scaled per-scene "to look right." Starting points (visual-testing, not +final architecture): + +| Element | Value | Element | Value | +|---|---|---|---| +| Adult height | 1.80 m (1.75–1.85) | Chair seat | 0.45 m | +| Door | 2.05 m × 0.95 m | Counter | 0.92 m | +| Table (2-top / 4-top) | 0.70×0.70 / 1.10×0.80 m | Appliance block | 0.90 m | +| Ceiling | 3.00 m | Main aisle / kitchen / pinch | 1.40 / 1.10 / 0.85 m | + +All imported assets are compared against the scale-reference scene, never the reverse. + +## 5. Naming & the mock-data rule (§14) + +The service is a **hand-authored timeline**. It must never pretend to be authoritative +gameplay. Every script, node, and data table that produces activity is named or +commented with one of: `Mock`, `Scripted`, `VisualOnly`, `Prototype`. Output is +**never** named `SimulationResult` or anything implying authoritative truth. The data +kind is declared in config as `Mock / Scripted / VisualOnly`. + +## 6. Visual truth boundary (§15) — document now, enforce forever + +``` +Authoritative simulation → immutable visual snapshot → 3D presentation +``` + +The future renderer **may** decide: animation interpolation, path smoothing, icon +placement, camera framing, cosmetic timing, particles, non-authoritative idle motion. + +The renderer **may not ever** decide: whether a customer was served, money earned, +satisfaction reached, whether food failed, authoritative prep time, which station was +truly overloaded, whether a task completed, or whether a table was available. + +In V0 the "snapshot" is faked by a scripted timeline, but the boundary is real: the +presentation layer reads a per-beat snapshot of facts and only chooses how to *show* +them. This is why the sandbox is engine-independent and asset-agnostic. + +## 7. Placeholder-first policy (§11) and non-goals (§13) + +**Placeholder-first.** Prove scale → camera → layout → readability → performance +*before* importing any environment/animation pack. Primitives, flat colours, simple +icons. The asset packs are evaluated *against* the sandbox; they do not define it. + +**Non-goals — deliberately absent.** No `RestaurantSim.Core` integration; no +authoritative demand/economics/satisfaction; no production customer/staff AI; no final +pathfinding or navmesh architecture; no construction tools, inventory, suppliers, +recipes, menu/pricing UI, scheduling, hiring, saving; no campaign/multiple restaurants/ +city sim/reviews/marketing/progression; no final animation controller, audio, lighting, +shaders, VFX, or UI; no multiplayer/mods/Steam. Nothing is added merely because the +prototype makes it tempting. A gap in the contract is **reported, not filled**. + +## 8. Pass criteria (§31) — what "V0 Pass" would require + +One working scale standard; a default camera that keeps the restaurant readable; +distinguishable roles; distinguishable table/customer/employee/kitchen states; a +**kitchen bottleneck visually distinct from a front-of-house bottleneck**; walls/roof +that don't routinely obstruct; a scripted service understandable without backend; +performance holding on the documented machine; documented (not assumed) asset +suitability; restricted assets kept out of the public repo; the visual layer owning no +gameplay truth; and enough evidence for the Art PM to make a concrete camera/scale/ +direction decision. The gate verdict lives in `reports/visual/v0/V0-REPORT.md`. + +## 9. Authority + +V0 may **recommend**. It may not declare the visual direction approved, begin M1, +integrate the simulation, or merge its own PR. Those are the product owners' calls +(Howard & Aaron). diff --git a/local-assets/README.md b/local-assets/README.md new file mode 100644 index 0000000..fff8c7a --- /dev/null +++ b/local-assets/README.md @@ -0,0 +1,27 @@ +# local-assets/ — QUARANTINE (git-ignored, never committed) + +This directory is **ignored by Git** (see `.gitignore`). It is the only place +owner-supplied asset packages may be **extracted for local evaluation**. + +## Hard rules (§10) + +1. **Nothing in here is ever committed** to this public repository. Ownership of + an asset-store license is **not** permission to redistribute its source files. +2. Original archives stay **outside the repo** (the owner keeps them on Desktop). + Only transient extracts for a local render test live here, and only for + packages whose license permits it. +3. Every package's license disposition is recorded as **text** in + `docs/art/ASSET-EVALUATION.md`. This folder holds bytes; that file holds the + record. + +## Current disposition (see docs/art/ASSET-EVALUATION.md for the full table) + +| Package | License | May extract here? | May ever commit source? | +|---|---|---|---| +| Downtown City MegaKit [Standard] | CC0 1.0 (Quaternius) | Yes | No (repo hygiene — large binary; not needed) | +| Universal Animation Library [Standard] | Likely CC0 — confirm at source | Only after source-confirmation | No | +| FBX-…zip (loose props) | None found | **No — do not use** | No | +| wintersets.zip (legacy installer) | None (presumed proprietary) | **No — do not use** | No | + +If you are an agent and you find yourself wanting to commit anything from here, +stop and re-read §10 of the assignment and `docs/art/ASSET-EVALUATION.md`. diff --git a/reports/visual/v0/V0-REPORT.md b/reports/visual/v0/V0-REPORT.md new file mode 100644 index 0000000..8df7bbe --- /dev/null +++ b/reports/visual/v0/V0-REPORT.md @@ -0,0 +1,448 @@ +# V0 — 3D Restaurant Readability Sandbox — Final Report + +**Track:** V0 — 3D Restaurant Readability Sandbox (gray-box, presentation-only) +**Report date:** 2026-07-29 +**Author:** V0 visual-track builder +**Status of this document:** Final report and gate recommendation (contract §29 + §36) + +> **Authority (contract §9).** V0 may **recommend** only. It does not declare the visual +> direction approved, begin M1, integrate the C# `RestaurantSim.Core` (M0) simulation, or +> merge its own pull request. Those are the product owners' calls (Howard & Aaron). This +> report ends in a recommendation, not an authorization. + +> **What V0 is.** A pure, seekable, gray-box visual experiment answering one question +> (§1): *can a management-view 3D restaurant be made readable — its roles, states, and +> bottlenecks legible at a glance — before any real art or simulation exists?* It owns no +> gameplay truth (visual-truth boundary, §15). All state shown is driven by a +> **Mock / Scripted / VisualOnly** timeline, never a simulation. + +--- + +## A. Repository setup + +| Item | Value | +|---|---| +| Base commit (branched from) | `06f0a7e` — `chore: establish base branch (license, gitignore, pointer)` | +| Branch | `prototype/v0-3d-restaurant-sandbox` (created from `origin/main` @ `06f0a7e`) | +| Worktree | `../Restaurant-VisualSandbox` (isolated; primary repo worktree `~/Restaurant` untouched) | +| Merge base with `origin/main` | `06f0a7e` (branch has never diverged from base for any merge) | +| Tip at authoring | `36cb53c` — later commits (this report + verification follow-ups) advance HEAD; run `git log --oneline origin/main..HEAD` for the live tip | +| M0 backend branches | **Untouched.** Nothing from V0 is merged into `main` or any backend branch. | + +> This report is a **point-in-time artifact**: the commit hashes and file counts below +> were current at authoring. The living-handoff docs (`docs/art/V0-CURRENT-STATE.md`, +> `V0-NEXT-ACTION.md`) and `git log` are the source of truth for the live HEAD. + +### Commit history (branch, oldest → newest) + +``` +06f0a7e chore: establish base branch (license, gitignore, pointer) [base only] +e1a20d6 chore: initialize isolated V0 visual sandbox +3093f12 docs: add V0 visual prototype contract +d2aa8ac chore: establish asset quarantine and provenance +bbe325f feat: add restaurant scale and camera laboratory +80eb795 feat: build gray-box restaurant shell +d6f8c45 feat: add scripted service readability scenario +36cb53c test: capture camera, scale, readability, and performance evidence +``` + +### Files (committed to the public repo) + +88 tracked files (at authoring). The engine build itself is **not** committed (see §C). Notable groups: + +- **Contract & docs:** `docs/art/V0-PROTOTYPE-CONTRACT.md`, `docs/art/ASSET-EVALUATION.md`, + `README.md`, `LICENSE`, `local-assets/README.md`. +- **Godot project:** `visual/RestaurantVisualSandbox/` — `project.godot`, 13 GDScripts under + `scripts/` (see §B), diagnostic + sandbox scenes under `scenes/`, `data/shots_evidence.json`. +- **Reproduction tool:** `tools/v0/capture.sh`. +- **Evidence (intentionally committed under `reports/visual/v0/`):** 24 screenshots + 3 `.mp4` + recordings + `performance/performance.csv` + this report. + +Asset-store binaries are **quarantined out** of the repo by `.gitignore` (`*.zip *.fbx *.glb +*.gltf *.blend *.wav …`) and never committed; only text provenance in `docs/art/ASSET-EVALUATION.md` +is tracked (contract §10). See §G. + +--- + +## B. Sub-agent / build approach + +The build was executed as a single focused builder pass within the contract timebox (§33: +12–20 focused builder hours), committed in small single-concern increments (one commit per +capability: base isolation → contract → asset quarantine → scale/camera lab → restaurant +shell → scripted service → evidence capture). The GDScript codebase is deliberately small +and readable so each contract term maps to source: + +| Script | Responsibility | +|---|---| +| `scripts/v0_config.gd` (autoload `V0`) | **Single source of truth** for the locked scale standard (§17), role palette (§21), state colours (§22), naming (§14). Nothing is scaled "to look right" per scene. | +| `scripts/camera_rig.gd` | The three camera candidates A/B/C + wall/roof modes helper. | +| `scripts/restaurant.gd` | Gray-box floor plan, zones, half-height dividers, wall/roof camera-relative modes. | +| `scripts/mock_service.gd` | Pure, seekable `snapshot(scenario, t)` authored timeline (FACTS only). | +| `scripts/service_view.gd` | Renders a snapshot into discs / gauges / overlays (decides HOW, not WHAT). | +| `scripts/stage.gd` | Visual treatments A (clean) / B (grounded) — lighting/tonemap only. | +| `scripts/scale_reference.gd`, `scripts/placeholder_factory.gd`, `scripts/mat_lib.gd` | Scale-reference scene, gray-box mesh factory, material library. | +| `scripts/asset_compare.gd` | Asset-assisted comparison harness (CC0 GLB vs scale reference). | +| `scripts/capture_runner.gd`, `scripts/perf_probe.gd`, `scripts/sandbox_root.gd` | Headless shot-list capture, performance probe, sandbox entry. | + +--- + +## C. Engine setup + +| Item | Value | +|---|---| +| Engine | **Godot 4.7.1-stable** (`4.7.1.stable.official.a13da4feb`) | +| Language | **GDScript only** (no C#) | +| Renderer | **Forward+** | +| Graphics API | **Metal 4.0** | +| Machine | **Apple M3 Max**, macOS 26.5.1, arm64 | +| Display | Built-in Retina / ProMotion | +| Test resolution | **1920×1080** | +| Engine binary location | `~/.local/opt/godot` — **outside the repo, not committed** | + +### Reproduce the evidence + +```bash +# From the repo root; override the engine path if needed: +GODOT=/path/to/Godot tools/v0/capture.sh +``` + +`tools/v0/capture.sh` runs three headless stages against +`visual/RestaurantVisualSandbox`: (1) an editor scan to register `class_name`s, (2) the +committed shot-list `data/shots_evidence.json` → `reports/visual/v0/screenshots/`, (3) the +performance probe → `reports/visual/v0/performance/performance.csv`. No manual steps. + +--- + +## D. Scale findings + +Scale is **locked** at **1 Godot unit = 1 metre**; the single source of truth is +`scripts/v0_config.gd` (§17). Every asset is compared *against* the scale-reference scene, +never the other way around. + +| Element | Locked value | +|---|---| +| Adult height | 1.80 m | +| Interior door | 2.05 × 0.95 m | +| Table, 2-top / 4-top | 0.70 × 0.70 m / 1.10 × 0.80 m | +| Table height | 0.75 m | +| Chair seat height | 0.45 m | +| Kitchen counter | 0.92 m | +| Major appliance block | 0.90 × 0.80 m | +| Ceiling height | 3.00 m | +| Aisle — main / kitchen / pinch | 1.40 m / 1.10 m / 0.85 m | + +**Finding.** The metric standard holds up under both camera projections: human, furniture, +and appliance proxies read at plausible relative sizes with no per-scene fudging. Interior +dividers are modelled **half-height (1.30 m)** so the management camera reads over them +without a wall-hiding mode engaged. The intentional **0.85 m pinch** (the restroom corridor) +is visibly the tightest passage in the plan, with the open main aisle as the obvious +alternative — the circulation-risk read is legible at management distance. + +**Evidence:** `reports/visual/v0/screenshots/50-scale-reference.png`, +`reports/visual/v0/screenshots/51-scale-reference-iso.png`. + +> Caveat: these are visual-testing starting points, not final architectural rules. + +--- + +## E. Camera findings + +Three candidates live in `scripts/camera_rig.gd`. + +| Candidate | Type | Behaviour | Read verdict | +|---|---|---|---| +| **A — ISO** | Orthographic | Fixed high 3/4 (true iso: yaw 45°, pitch 35.264°), 90° step rotation, ortho-size zoom | Cleanest, most stable read; can feel distant/detached. | +| **B — ORBIT** | Perspective | Free orbit / pan / zoom; pitch clamp 14°–80°, distance 6–48, fov 50 | Best for close inspection angles; can disorient and needs manual re-framing. | +| **C — HYBRID** | Perspective | Perspective overview by default; `focus_on(target)` with a 0.6 s cubic tween, plus `return_to_overview` | **Best balance:** overview by default, one-tap Level-2/3 inspection, always returns to a known frame. | + +**Finding.** Candidate **C (Hybrid)** is the recommended default. It keeps the whole floor +readable at rest (like A) while allowing perspective inspection dives (like B) without the +user getting lost, because the tween and `return_to_overview` guarantee a canonical frame. +A remains the strongest *pure-readability* fallback; B is an inspection tool, not a default. + +**Evidence:** `01-camera-A-isometric.png`, `02-camera-B-orbit.png`, +`03-camera-C-hybrid-overview.png`, `04-camera-C-hybrid-focus-grill.png` (all under +`reports/visual/v0/screenshots/`), plus recording `rec-03-camera-orbit.mp4`. + +### Walls / roof (camera-relative, `scripts/restaurant.gd`) + +Four modes, roof hidden by default in the management view: + +| Mode | Behaviour | Read verdict | +|---|---|---| +| **SOLID** | All walls drawn | Interior often occluded; not viable as a default. | +| **AUTO_HIDE** | Hides walls between camera and interior | Interior stays visible while walls give context; strong default candidate. | +| **CUTAWAY** | Dollhouse, roof off | Clean top-down-ish read of the whole floor. | +| **TRANSPARENT** | Near walls ~16% alpha | Keeps wall context while seeing through; good compromise. | + +**Finding.** With half-height interior dividers plus AUTO_HIDE / CUTAWAY / TRANSPARENT +available, walls **do not routinely obstruct** the management view. SOLID confirms the +problem the other modes solve. + +**Evidence:** `30-walls-solid.png`, `31-walls-autohide.png`, `32-walls-cutaway.png`, +`33-walls-transparent.png`, `34-roof-on.png` (all under `reports/visual/v0/screenshots/`). + +--- + +## F. Restaurant readability (per state) + +The restaurant (`scripts/restaurant.gd`) is one compact floor: **10 tables / 32 seats**, +with entrance + host stand + waiting (front-right), dining (centre-left), a compact kitchen +(back-left) with four distinct stations **Grill / Fry / Range / Garde** plus a pass line and +a dishwashing zone, restrooms (back-right) via the deliberate ~0.85 m corridor, and a +staff/storage door. State is driven by the pure `snapshot(scenario, t)` timeline +(`scripts/mock_service.gd`); the renderer only decides *how* to show it (§15). + +Readability language (§21–§23): **role = colour + a second non-colour signal** (customer +tan/none, host teal/clipboard, server blue/tray, cook white/toque, busser green/bin, +manager purple/star), so identity never relies on colour alone. **State colours:** +green=calm/available, blue=active/eating, amber=waiting(soft), orange=waiting-too-long/queue, +red=alert/congested, brown=dirty, cyan=ready/needs-payment, grey=idle. Table state = tinted +tabletop + floating disc; kitchen station pressure = a vertical gauge (colour=state, +height=queue). Overlays: OFF / **MINIMAL (default)** / FULL / SELECTION. + +| Scenario (peak) | Distinct visual signature | Identifiable? | Evidence | +|---|---|---|---| +| **0 — Healthy** (150 s) — the baseline | Mostly **blue** (eating) tables, **short green** kitchen gauges, **no alerts** | **Yes** | `10-scenario-healthy.png` | +| **1 — Kitchen bottleneck** (165 s) | **Tall red** grill gauge + many **orange** (waiting-for-food) tables + servers **massed idle at the pass** (food not reaching them) | **Yes — and clearly distinct from FOH** | `11-scenario-kitchen-bottleneck.png`; `rec-01-kitchen-service.mp4` | +| **2 — FOH bottleneck** (180 s) | **Calm green/idle** kitchen gauges + **yellow** (waiting to order) / **cyan** (needs payment) / **brown** (dirty) tables + an **overloaded-server workload ring** + plates piled at the pass. Kitchen visibly **not** the problem | **Yes — kitchen legibly ruled out** | `12-scenario-foh-bottleneck.png`; `rec-02-foh-service.mp4` | +| **3 — Host & seating bottleneck** (90 s) | Entrance **queue with reddening patience pips** + several **dirty empty tables not reseated** + **idle green** kitchen (spare capacity) + a customer **leaving** | **Yes** | `13-scenario-host-bottleneck.png` | + +(Screenshots under `reports/visual/v0/screenshots/`; recordings under +`reports/visual/v0/recordings/`.) + +**The critical read (§31 pass criterion).** The **kitchen bottleneck is visually distinct +from the front-of-house bottleneck**: scenario 1 shows a hot kitchen (tall red gauge) with +starved dining; scenario 2 shows a cool/idle kitchen with the pressure entirely in the FOH +loop (order → pay → bus). The two failure modes do not look alike. + +**Table states modelled:** available, preparing, occupied, waiting-to-order, waiting-for-food, +eating, needs-payment, dirty, being-cleaned. **Kitchen states:** normal, queue, congested, +food-ready, idle, unavailable. + +**Any unclear?** No state was found unreadable in the gray-box at MINIMAL overlays. The +honest limits are that (a) discrimination has been assessed by the builder only — the human +visual test (§30) is prepared but **not yet run** (Howard & Aaron's to conduct); (b) some +fine distinctions (e.g. waiting-to-order amber vs waiting-for-food orange) sit close on the +warm ramp and are the kind of thing a human test should confirm; and (c) the +**overloaded-server ring is red by design but reads subtly at full-overview distance** +(the more prominent ring in `12-...png` is an *active* server at a table) — enlarging or +pulsing the overloaded marker is a concrete refinement-pass item. Overlay evidence +(controlled off/minimal/full triptych, HUD hidden): `20-overlay-off.png`, +`21-overlay-minimal.png`, `22-overlay-full.png`. + +--- + +## G. Asset evaluation + +Full detail and provenance table: **`docs/art/ASSET-EVALUATION.md`** (verified 2026-07-29; +each archive inspected read-only from the owner's Desktop; **no binary extracted into the +repo**). Summary: + +| Package | Author | License | Import / animation | Public-repo status | Recommended future use (M1+) | +|---|---|---|---|---|---| +| **Downtown City MegaKit [Standard]** | Quaternius | **CC0 1.0** (bundled `License_Standard.txt` + source page) | FBX via Godot 4.7 built-in `ufbx`; expect ×0.01 Unity scale + axis fix | **Cleared**, but **not committed** (repo hygiene: 235 MB, exterior kit) | Exterior/street **context blocks only**; scale-compat check | +| **Universal Animation Library [Standard]** | Quaternius | **CC0 1.0** (bundled `License.txt` empty; **confirmed CC0 at quaternius.com 2026-07-29**) | **Native Godot 4 GLB.** Asset-assisted test: `UAL1_Standard.glb` imported natively into Godot 4.7, measured **1.83 m** tall (scale factor **0.984** to the 1.80 m standard → essentially 1:1, no rescale), **43 animations** | **Cleared**, but **not committed** (hygiene) | **Prime candidate** for character/animation (pending a proper retarget eval) | +| **FBX-…zip** (loose props) | **Unknown** | **None found** | Not attempted | Excluded | **Do not use** (no provenance) | +| **wintersets.zip** | Unknown legacy game mod | **None** (presumed proprietary) | Not attempted | Excluded | **Do not use** | + +**Finding.** Asset suitability is **documented, not assumed** (§31). The one asset-assisted +comparison used only a CC0 pack, extracted to git-ignored `local-assets/` / +`placeholders/_assetlab/`, and is documented as a *comparison*, never the sandbox's identity. +The Universal Animation Library measuring ~1:1 against the locked 1.80 m standard is a +positive signal that the metric scale lock (§D) is realistic for real character assets. + +> **Reproducibility caveat.** The precise figures (1.83 m, scale factor 0.984, 43 +> animations) are **builder-attested from one local run**: the CC0 source binary is +> git-ignored (license hygiene, §10), so `60-asset-assisted-animation-lib.png` and the +> capture log are the surviving artifacts. A reviewer reproduces by extracting the pack +> into `placeholders/_assetlab/` and re-running `AssetCompare.tscn`. + +**Hard rule enforced (§10).** Ownership of an asset-store package is not permission to +upload its source to a public repo. **No supplied package's source is committed.** Even for +CC0, a Git-LFS-vs-external-store delivery decision is deferred to M1+. + +**Evidence:** `60-asset-assisted-animation-lib.png`. + +--- + +## H. Performance + +Source: `reports/visual/v0/performance/performance.csv` — M3 Max, **vsync OFF**, 1920×1080. + +| config | actors | mesh_inst | draw_calls | primitives | avg_fps | worst_fps | avg_ms | worst_ms | vram_mb | +|---|--:|--:|--:|--:|--:|--:|--:|--:|--:| +| empty_restaurant | 9 | 268 | 671 | 136,926 | 257 | 116 | 3.89 | 8.65 | 332.6 | +| healthy_service | 32 | 329 | 848 | 399,046 | 358 | 330 | 2.79 | 3.03 | 338.0 | +| peak_kitchen_full | 31 | 342 | 758 | 450,408 | 322 | **72** | 3.11 | **13.91** | 337.8 | +| peak_foh_all_diagnostics | 25 | 320 | 708 | 258,564 | 361 | 330 | 2.77 | 3.03 | 336.5 | +| max_zoom_out | 31 | 342 | 791 | 474,844 | 360 | 330 | 2.78 | 3.03 | 337.8 | + +**Interpretation.** Performance **far exceeds** the §28 targets (1920×1080, 60 FPS target, +30 FPS minimum acceptable). Typical frame is **~3 ms (~330 FPS)**; the single worst frame is +**13.91 ms (72 FPS)** in `peak_kitchen_full` — a one-off hitch that still clears the 60 FPS +target and is well above the 30 FPS floor. VRAM sits at **~335 MB** across all configs; draw +calls stay **<850**. There is substantial frame-time headroom **on this gray-box** — but +read it as "no red flags yet," not "performance is solved" (final art will consume it; see +risks below). + +**Risks / honesty on the numbers.** +- These are **uncapped** (vsync OFF) headroom figures. With vsync on, frame rate caps to the + display refresh; the meaningful signal here is *frame-time headroom*, not the FPS ceiling. +- This is a **gray-box**: no final meshes, materials, animation, VFX, shadows-at-scale, or UI + (§13). Real art and animation will consume the headroom; today's numbers are a **floor of + confidence**, not a guarantee for the shipped look. +- **One machine only** (M3 Max). No low-end / integrated-GPU / Windows / different-resolution + data exists. That is the single largest performance unknown. + +--- + +## I. Scope audit (§13 non-goals — confirm each absent) + +Each contract §13 non-goal was checked against the codebase. All confirmed **absent**. + +| §13 non-goal | Present? | +|---|---| +| `RestaurantSim.Core` (M0) integration | Absent — no C#; GDScript-only; visual-truth boundary enforced (§15) | +| Authoritative demand / economics | Absent — all state is Mock/Scripted/VisualOnly | +| Authoritative satisfaction / reviews | Absent | +| Production customer/staff AI | Absent — actors follow an authored timeline, not AI | +| Real pathfinding / navmesh | Absent | +| Construction / inventory / suppliers / recipes | Absent | +| Menu / pricing UI | Absent | +| Scheduling / hiring | Absent | +| Saving / persistence | Absent | +| Campaign / multi-restaurant / city sim | Absent — one compact floor only | +| Marketing / progression | Absent | +| Final animation controller / audio | Absent — no animation state machine, no audio | +| Final lighting / shaders / VFX / UI | Absent — gray-box + diagnostic HUD only | +| Multiplayer / mods / Steam | Absent | + +The naming rule (§14) is enforced in `v0_config.gd` (`DATA_KIND = "Mock / Scripted / +VisualOnly"`, never "SimulationResult") and by the persistent **"MOCK / SCRIPTED — not a +simulation"** HUD tag. + +--- + +## J. Gate recommendation + +``` +Verdict: Fail / Conditional-Pass / Pass-with-notes / Pass +Action: Continue / Defer / Rewrite / Abandon +``` + +**Verdict: Pass-with-notes** +**Action: Continue** + +**Meaning of "Continue" (scoped).** Continue = recommend an **Art PM review and a limited +visual refinement pass** on V0. It does **NOT** authorize M1, backend/simulation integration, +or final art. Those remain the product owners' calls (§9). + +**Why Pass-with-notes and not clean Pass.** Every functional pass criterion is met in the +gray-box, but two honest limits keep it from an unqualified Pass: (1) the **human visual +test (§30) is prepared but not yet run** — readability is builder-assessed only; and (2) +performance is validated on **one machine** with no final art loaded. These are notes for the +refinement pass, not blockers. + +### §31 pass criteria → evidence + +| # | §31 criterion | Result | Evidence | +|--:|---|:--:|---| +| 1 | One working scale standard | **Met** | `50-scale-reference.png`, `51-scale-reference-iso.png` | +| 2 | A default camera that keeps the restaurant readable | **Met** | `03-camera-C-hybrid-overview.png`, `05-whole-restaurant.png` | +| 3 | Distinguishable roles | **Met** (color + 2nd signal; human test pending → **partial** on human confirmation) | `07-dining-closeup.png`, `06-kitchen-closeup.png` | +| 4 | Distinguishable table / customer / employee / kitchen states | **Met** (design-distinct; at-a-glance discrimination pending human test → **partial**) | `21-overlay-minimal.png`, `22-overlay-full.png` | +| 5 | Kitchen bottleneck visually distinct from FOH bottleneck | **Met** | `11-scenario-kitchen-bottleneck.png` vs `12-scenario-foh-bottleneck.png` | +| 6 | Walls/roof don't routinely obstruct | **Met** | `31-walls-autohide.png`, `32-walls-cutaway.png`, `33-walls-transparent.png` | +| 7 | A scripted service understandable without backend | **Met** | `rec-01-kitchen-service.mp4`, `rec-02-foh-service.mp4`, `13-scenario-host-bottleneck.png` | +| 8 | Performance holds on the documented machine | **Met** (M3 Max only; one-machine caveat) | `performance/performance.csv` (§H) | +| 9 | Documented (not assumed) asset suitability | **Met** | `docs/art/ASSET-EVALUATION.md`, `60-asset-assisted-animation-lib.png` | +| 10 | Restricted assets kept out of the public repo | **Met** | `.gitignore` quarantine; `git ls-files` shows no binaries; §G | +| 11 | Visual layer owns no gameplay truth | **Met** | §15 boundary in `mock_service.gd`/`service_view.gd`; "MOCK / SCRIPTED" HUD tag; §I | +| 12 | Enough evidence for the Art PM to make a concrete camera/scale/direction decision | **Met** | This report + 24 screenshots + 3 recordings + CSV (§K decision) | + +Overall: **10 fully met, 2 partial** (criteria 3 and 4 pending the human visual test's +confirmation of at-a-glance role/state discrimination — the very thing §1 exists to test). +No criterion failed. + +--- + +## K. Required Art PM decision + +**The single concrete decision Howard (with Aaron) must make:** + +> **Adopt the V0 defaults — Hybrid-C camera + 1 u = 1 m scale + treatment-A readability +> baseline — and authorize a limited V0 refinement pass. Do NOT begin M1.** + +Recommended defaults, with the evidence each rests on: + +- **Camera:** Hybrid-C as default (overview + focus dive + guaranteed return) — §E, + `03-camera-C-hybrid-overview.png`, `04-camera-C-hybrid-focus-grill.png`. +- **Scale:** 1 Godot unit = 1 metre, per `scripts/v0_config.gd` — §D, `50-scale-reference.png`; + corroborated by the CC0 animation asset measuring ~1:1 (§G). +- **Readability baseline:** **Visual Treatment A ("clean stylized management sim")** — brighter + ambient (0.75), soft light, ACES, light SSAO — most readable at management distance. + Treatment B ("grounded") is moodier and better for marketing stills but noticeably darker + and costs readability. Evidence: `40-treatment-A-clean.png`, `41-treatment-B-grounded.png`. + +Authorizing the refinement pass explicitly does **not** authorize M1, backend integration, +or final art. + +### Honest limits (read before deciding) + +- **Mock data only.** Every state shown is authored, not simulated. V0 proves the *visual + language is legible*, not that the game *plays* well. +- **One machine.** All performance is M3 Max / macOS / Metal / 1920×1080. No low-end or + cross-platform data. +- **Gray-box only.** No final meshes, materials, animation, audio, VFX, or UI (§13). Real art + will consume the measured headroom. +- **Human visual test not yet run.** §30 test is prepared; the readability verdicts here are + the builder's, pending Howard & Aaron's own test. + +--- + +### Evidence index — 24 screenshots + +All under `reports/visual/v0/screenshots/`. + +| # | File | Shows | +|---|---|---| +| 01 | `01-camera-A-isometric.png` | Camera A (orthographic isometric) | +| 02 | `02-camera-B-orbit.png` | Camera B (perspective free orbit) | +| 03 | `03-camera-C-hybrid-overview.png` | Camera C hybrid — overview default | +| 04 | `04-camera-C-hybrid-focus-grill.png` | Camera C hybrid — focus dive on the grill | +| 05 | `05-whole-restaurant.png` | Whole restaurant, management view | +| 06 | `06-kitchen-closeup.png` | Kitchen close-up (Grill/Fry/Range/Garde + pass) | +| 07 | `07-dining-closeup.png` | Dining close-up (tables, roles) | +| 10 | `10-scenario-healthy.png` | Scenario 0 — Healthy (baseline) | +| 11 | `11-scenario-kitchen-bottleneck.png` | Scenario 1 — Kitchen bottleneck | +| 12 | `12-scenario-foh-bottleneck.png` | Scenario 2 — FOH bottleneck | +| 13 | `13-scenario-host-bottleneck.png` | Scenario 3 — Host & seating bottleneck | +| 20 | `20-overlay-off.png` | Overlays OFF | +| 21 | `21-overlay-minimal.png` | Overlays MINIMAL (default) | +| 22 | `22-overlay-full.png` | Overlays FULL (diagnostics) | +| 30 | `30-walls-solid.png` | Walls SOLID | +| 31 | `31-walls-autohide.png` | Walls AUTO_HIDE | +| 32 | `32-walls-cutaway.png` | Walls CUTAWAY (dollhouse, roof off) | +| 33 | `33-walls-transparent.png` | Walls TRANSPARENT (~16% alpha) | +| 34 | `34-roof-on.png` | Roof ON (for comparison) | +| 40 | `40-treatment-A-clean.png` | Treatment A — clean stylized | +| 41 | `41-treatment-B-grounded.png` | Treatment B — grounded/moody | +| 50 | `50-scale-reference.png` | Scale reference (perspective) | +| 51 | `51-scale-reference-iso.png` | Scale reference (isometric) | +| 60 | `60-asset-assisted-animation-lib.png` | Asset-assisted: CC0 Universal Animation Library vs scale ref | + +### Evidence index — 3 recordings + +All under `reports/visual/v0/recordings/`. + +| # | File | Shows | +|---|---|---| +| 1 | `rec-01-kitchen-service.mp4` | Scripted kitchen-bottleneck service over time | +| 2 | `rec-02-foh-service.mp4` | Scripted FOH-bottleneck service over time | +| 3 | `rec-03-camera-orbit.mp4` | Camera orbit / hybrid behaviour | + +--- + +*End of V0 final report. V0 recommends; it does not approve, authorize M1, integrate the +simulation, or merge its own PR (§9).* diff --git a/reports/visual/v0/performance/.gitkeep b/reports/visual/v0/performance/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/reports/visual/v0/performance/performance.csv b/reports/visual/v0/performance/performance.csv new file mode 100644 index 0000000..099c4fc --- /dev/null +++ b/reports/visual/v0/performance/performance.csv @@ -0,0 +1,6 @@ +config,actors,mesh_instances,draw_calls,primitives,avg_fps,worst_fps,avg_frame_ms,worst_frame_ms,video_mem_mb +empty_restaurant,9,268,671,136926,257,116,3.89,8.65,332.6 +healthy_service,32,329,848,399046,358,330,2.79,3.03,338.0 +peak_kitchen_full,31,342,758,450408,322,72,3.11,13.91,337.8 +peak_foh_all_diagnostics,25,320,708,258564,361,330,2.77,3.03,336.5 +max_zoom_out,31,342,791,474844,360,330,2.78,3.03,337.8 diff --git a/reports/visual/v0/recordings/.gitkeep b/reports/visual/v0/recordings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/reports/visual/v0/recordings/rec-01-kitchen-service.mp4 b/reports/visual/v0/recordings/rec-01-kitchen-service.mp4 new file mode 100644 index 0000000..1cd835b Binary files /dev/null and b/reports/visual/v0/recordings/rec-01-kitchen-service.mp4 differ diff --git a/reports/visual/v0/recordings/rec-02-foh-service.mp4 b/reports/visual/v0/recordings/rec-02-foh-service.mp4 new file mode 100644 index 0000000..4465921 Binary files /dev/null and b/reports/visual/v0/recordings/rec-02-foh-service.mp4 differ diff --git a/reports/visual/v0/recordings/rec-03-camera-orbit.mp4 b/reports/visual/v0/recordings/rec-03-camera-orbit.mp4 new file mode 100644 index 0000000..ebdaf0d Binary files /dev/null and b/reports/visual/v0/recordings/rec-03-camera-orbit.mp4 differ diff --git a/reports/visual/v0/screenshots/.gitkeep b/reports/visual/v0/screenshots/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/reports/visual/v0/screenshots/01-camera-A-isometric.png b/reports/visual/v0/screenshots/01-camera-A-isometric.png new file mode 100644 index 0000000..8956aee Binary files /dev/null and b/reports/visual/v0/screenshots/01-camera-A-isometric.png differ diff --git a/reports/visual/v0/screenshots/02-camera-B-orbit.png b/reports/visual/v0/screenshots/02-camera-B-orbit.png new file mode 100644 index 0000000..7bc3e67 Binary files /dev/null and b/reports/visual/v0/screenshots/02-camera-B-orbit.png differ diff --git a/reports/visual/v0/screenshots/03-camera-C-hybrid-overview.png b/reports/visual/v0/screenshots/03-camera-C-hybrid-overview.png new file mode 100644 index 0000000..6db9f92 Binary files /dev/null and b/reports/visual/v0/screenshots/03-camera-C-hybrid-overview.png differ diff --git a/reports/visual/v0/screenshots/04-camera-C-hybrid-focus-grill.png b/reports/visual/v0/screenshots/04-camera-C-hybrid-focus-grill.png new file mode 100644 index 0000000..8d282e4 Binary files /dev/null and b/reports/visual/v0/screenshots/04-camera-C-hybrid-focus-grill.png differ diff --git a/reports/visual/v0/screenshots/05-whole-restaurant.png b/reports/visual/v0/screenshots/05-whole-restaurant.png new file mode 100644 index 0000000..abc4a6c Binary files /dev/null and b/reports/visual/v0/screenshots/05-whole-restaurant.png differ diff --git a/reports/visual/v0/screenshots/06-kitchen-closeup.png b/reports/visual/v0/screenshots/06-kitchen-closeup.png new file mode 100644 index 0000000..43f3128 Binary files /dev/null and b/reports/visual/v0/screenshots/06-kitchen-closeup.png differ diff --git a/reports/visual/v0/screenshots/07-dining-closeup.png b/reports/visual/v0/screenshots/07-dining-closeup.png new file mode 100644 index 0000000..0594e73 Binary files /dev/null and b/reports/visual/v0/screenshots/07-dining-closeup.png differ diff --git a/reports/visual/v0/screenshots/10-scenario-healthy.png b/reports/visual/v0/screenshots/10-scenario-healthy.png new file mode 100644 index 0000000..58e1b22 Binary files /dev/null and b/reports/visual/v0/screenshots/10-scenario-healthy.png differ diff --git a/reports/visual/v0/screenshots/11-scenario-kitchen-bottleneck.png b/reports/visual/v0/screenshots/11-scenario-kitchen-bottleneck.png new file mode 100644 index 0000000..8956aee Binary files /dev/null and b/reports/visual/v0/screenshots/11-scenario-kitchen-bottleneck.png differ diff --git a/reports/visual/v0/screenshots/12-scenario-foh-bottleneck.png b/reports/visual/v0/screenshots/12-scenario-foh-bottleneck.png new file mode 100644 index 0000000..8bcf36b Binary files /dev/null and b/reports/visual/v0/screenshots/12-scenario-foh-bottleneck.png differ diff --git a/reports/visual/v0/screenshots/13-scenario-host-bottleneck.png b/reports/visual/v0/screenshots/13-scenario-host-bottleneck.png new file mode 100644 index 0000000..4d20bbe Binary files /dev/null and b/reports/visual/v0/screenshots/13-scenario-host-bottleneck.png differ diff --git a/reports/visual/v0/screenshots/20-overlay-off.png b/reports/visual/v0/screenshots/20-overlay-off.png new file mode 100644 index 0000000..500e0d0 Binary files /dev/null and b/reports/visual/v0/screenshots/20-overlay-off.png differ diff --git a/reports/visual/v0/screenshots/21-overlay-minimal.png b/reports/visual/v0/screenshots/21-overlay-minimal.png new file mode 100644 index 0000000..86891b9 Binary files /dev/null and b/reports/visual/v0/screenshots/21-overlay-minimal.png differ diff --git a/reports/visual/v0/screenshots/22-overlay-full.png b/reports/visual/v0/screenshots/22-overlay-full.png new file mode 100644 index 0000000..e386713 Binary files /dev/null and b/reports/visual/v0/screenshots/22-overlay-full.png differ diff --git a/reports/visual/v0/screenshots/30-walls-solid.png b/reports/visual/v0/screenshots/30-walls-solid.png new file mode 100644 index 0000000..4d7bf03 Binary files /dev/null and b/reports/visual/v0/screenshots/30-walls-solid.png differ diff --git a/reports/visual/v0/screenshots/31-walls-autohide.png b/reports/visual/v0/screenshots/31-walls-autohide.png new file mode 100644 index 0000000..703e5fd Binary files /dev/null and b/reports/visual/v0/screenshots/31-walls-autohide.png differ diff --git a/reports/visual/v0/screenshots/32-walls-cutaway.png b/reports/visual/v0/screenshots/32-walls-cutaway.png new file mode 100644 index 0000000..703e5fd Binary files /dev/null and b/reports/visual/v0/screenshots/32-walls-cutaway.png differ diff --git a/reports/visual/v0/screenshots/33-walls-transparent.png b/reports/visual/v0/screenshots/33-walls-transparent.png new file mode 100644 index 0000000..425800f Binary files /dev/null and b/reports/visual/v0/screenshots/33-walls-transparent.png differ diff --git a/reports/visual/v0/screenshots/34-roof-on.png b/reports/visual/v0/screenshots/34-roof-on.png new file mode 100644 index 0000000..001ed1f Binary files /dev/null and b/reports/visual/v0/screenshots/34-roof-on.png differ diff --git a/reports/visual/v0/screenshots/40-treatment-A-clean.png b/reports/visual/v0/screenshots/40-treatment-A-clean.png new file mode 100644 index 0000000..e386713 Binary files /dev/null and b/reports/visual/v0/screenshots/40-treatment-A-clean.png differ diff --git a/reports/visual/v0/screenshots/41-treatment-B-grounded.png b/reports/visual/v0/screenshots/41-treatment-B-grounded.png new file mode 100644 index 0000000..836ea5f Binary files /dev/null and b/reports/visual/v0/screenshots/41-treatment-B-grounded.png differ diff --git a/reports/visual/v0/screenshots/50-scale-reference.png b/reports/visual/v0/screenshots/50-scale-reference.png new file mode 100644 index 0000000..42677a9 Binary files /dev/null and b/reports/visual/v0/screenshots/50-scale-reference.png differ diff --git a/reports/visual/v0/screenshots/51-scale-reference-iso.png b/reports/visual/v0/screenshots/51-scale-reference-iso.png new file mode 100644 index 0000000..cfc8b5b Binary files /dev/null and b/reports/visual/v0/screenshots/51-scale-reference-iso.png differ diff --git a/reports/visual/v0/screenshots/60-asset-assisted-animation-lib.png b/reports/visual/v0/screenshots/60-asset-assisted-animation-lib.png new file mode 100644 index 0000000..7a5a794 Binary files /dev/null and b/reports/visual/v0/screenshots/60-asset-assisted-animation-lib.png differ diff --git a/tools/v0/capture.sh b/tools/v0/capture.sh new file mode 100755 index 0000000..2c37d5d --- /dev/null +++ b/tools/v0/capture.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# V0 evidence capture — reproducible. Renders the committed shot-list to +# reports/visual/v0/screenshots/ and runs the performance probe. No manual steps. +# +# GODOT=/path/to/Godot tools/v0/capture.sh # override engine if needed +# +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +PROJ="$REPO/visual/RestaurantVisualSandbox" +GODOT="${GODOT:-$HOME/.local/opt/godot/Godot.app/Contents/MacOS/Godot}" + +SHOTS="$PROJ/data/shots_evidence.json" +SHOTDIR="$REPO/reports/visual/v0/screenshots" +PERFDIR="$REPO/reports/visual/v0/performance" + +if [ ! -x "$GODOT" ]; then echo "Godot not found at $GODOT (set GODOT=...)"; exit 1; fi +echo "engine: $("$GODOT" --version 2>/dev/null | head -1)" + +echo "[1/3] editor scan (register class_names)…" +"$GODOT" --path "$PROJ" --editor --quit --headless >/dev/null 2>&1 || true + +echo "[2/3] screenshots -> $SHOTDIR" +"$GODOT" --path "$PROJ" res://scenes/diagnostics/Capture.tscn -- \ + --shots="$SHOTS" --outdir="$SHOTDIR" + +echo "[3/3] performance probe -> $PERFDIR" +mkdir -p "$PERFDIR" +"$GODOT" --path "$PROJ" res://scenes/diagnostics/Perf.tscn -- \ + --out="$PERFDIR/performance.csv" || echo "(perf probe skipped/failed)" + +echo "done." diff --git a/visual/RestaurantVisualSandbox/README.md b/visual/RestaurantVisualSandbox/README.md new file mode 100644 index 0000000..a9cebb8 --- /dev/null +++ b/visual/RestaurantVisualSandbox/README.md @@ -0,0 +1,48 @@ +# Restaurant Visual Sandbox — V0 + +Presentation-only Godot 4 sandbox for the **V0 — 3D Restaurant Readability** +experiment. It answers one question: *can a first-time viewer look at one small +3D restaurant during service and quickly understand where customers are, what +staff are doing, where congestion is forming, and which area needs attention?* + +It is **not** a game, not the M0 simulation, and owns **no** gameplay truth. All +activity is a hand-authored `Mock/Scripted/VisualOnly` timeline (see +`../../docs/art/V0-PROTOTYPE-CONTRACT.md` and `scripts/` files named `*_mock` / +`*_scripted`). + +## Engine + +- **Godot 4.7.1-stable** · Renderer **Forward+** (Metal on Apple Silicon) +- 1 Godot unit = 1 metre (scale lock in `scripts/v0_config.gd`) + +## Open / run interactively + +```bash +GODOT=~/.local/opt/godot/Godot.app/Contents/MacOS/Godot +"$GODOT" --path visual/RestaurantVisualSandbox # run main sandbox +"$GODOT" -e --path visual/RestaurantVisualSandbox # open in editor +``` + +### Interactive controls (when built out in Phase B) + +| Key | Action | +|---|---| +| `1` `2` `3` | Camera candidate A (isometric) / B (orbit) / C (hybrid) | +| Drag / `Q` `E` | Orbit · `WASD` pan · wheel zoom (management camera) | +| `Space` | Pause / resume the scripted service | +| `,` `.` | Slower / faster scripted time | +| `R` | Reset the scripted scenario | +| `F1..F4` | Select scenario: healthy / kitchen bottleneck / FOH bottleneck / host bottleneck | +| `Tab` | Cycle diagnostic overlays | +| `H` | Toggle wall hide / cutaway | + +## Regenerate evidence (reproducible, no manual screenshots) + +```bash +# from repo root: +tools/v0/capture.sh # renders all screenshots + performance to reports/visual/v0/ +``` + +The capture harness (`scripts/capture_runner.gd`, added in Phase C) drives the +same scenes non-interactively and writes PNGs / CSVs. Nothing about the evidence +requires a human to click. diff --git a/visual/RestaurantVisualSandbox/data/shots_evidence.json b/visual/RestaurantVisualSandbox/data/shots_evidence.json new file mode 100644 index 0000000..145ae06 --- /dev/null +++ b/visual/RestaurantVisualSandbox/data/shots_evidence.json @@ -0,0 +1,34 @@ +{ + "_comment": "V0 evidence shot-list. MOCK/SCRIPTED/VisualOnly. Rendered by tools/v0/capture.sh into reports/visual/v0/screenshots/. Out paths are relative to --outdir.", + "shots": [ + {"out":"01-camera-A-isometric.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":true}}, + {"out":"02-camera-B-orbit.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-2.0,0.6,-0.6","yaw":26,"pitch":30,"size":24,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":true}}, + {"out":"03-camera-C-hybrid-overview.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"hybrid","pivot":"-2.0,0.6,-0.6","yaw":24,"pitch":34,"size":26,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"minimal","hud":true}}, + {"out":"04-camera-C-hybrid-focus-grill.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"hybrid","pivot":"-2.0,0.6,-0.6","yaw":18,"pitch":26,"size":26,"wait":18,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":true,"focus":"-7.0,1.2,-5.4","focus_dist":9.0}}, + + {"out":"05-whole-restaurant.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-1.5,0.6,-0.4","yaw":24,"pitch":28,"size":26,"wait":16,"config":{"scenario":0,"seek":150,"overlay":"minimal","hud":true}}, + {"out":"06-kitchen-closeup.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-5.0,0.8,-4.6","yaw":10,"pitch":24,"size":13,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":false}}, + {"out":"07-dining-closeup.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-4.5,0.7,2.0","yaw":28,"pitch":26,"size":13,"wait":16,"config":{"scenario":2,"seek":"peak","overlay":"full","hud":false}}, + + {"out":"10-scenario-healthy.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":0,"seek":"peak","overlay":"full","hud":true}}, + {"out":"11-scenario-kitchen-bottleneck.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":true}}, + {"out":"12-scenario-foh-bottleneck.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":2,"seek":"peak","overlay":"full","hud":true}}, + {"out":"13-scenario-host-bottleneck.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":3,"seek":"peak","overlay":"full","hud":true}}, + + {"out":"20-overlay-off.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"off","hud":false}}, + {"out":"21-overlay-minimal.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"minimal","hud":false}}, + {"out":"22-overlay-full.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":false}}, + + {"out":"30-walls-solid.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-1.5,0.6,-0.4","yaw":24,"pitch":28,"size":27,"wait":14,"config":{"scenario":0,"seek":150,"overlay":"minimal","hud":false,"wall_mode":0}}, + {"out":"31-walls-autohide.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-1.5,0.6,-0.4","yaw":24,"pitch":28,"size":27,"wait":14,"config":{"scenario":0,"seek":150,"overlay":"minimal","hud":false,"wall_mode":1}}, + {"out":"32-walls-cutaway.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-1.5,0.6,-0.4","yaw":24,"pitch":28,"size":27,"wait":14,"config":{"scenario":0,"seek":150,"overlay":"minimal","hud":false,"wall_mode":2}}, + {"out":"33-walls-transparent.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-1.5,0.6,-0.4","yaw":24,"pitch":28,"size":27,"wait":14,"config":{"scenario":0,"seek":150,"overlay":"minimal","hud":false,"wall_mode":3}}, + {"out":"34-roof-on.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"orbit","pivot":"-1.5,0.6,-0.4","yaw":24,"pitch":20,"size":30,"wait":14,"config":{"scenario":0,"seek":150,"overlay":"off","hud":false,"wall_mode":0,"roof":true}}, + + {"out":"40-treatment-A-clean.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":false,"treatment":"A"}}, + {"out":"41-treatment-B-grounded.png","scene":"res://scenes/sandbox/Sandbox.tscn","cam":"iso","pivot":"-2.5,0.7,-1.0","size":18,"wait":16,"config":{"scenario":1,"seek":"peak","overlay":"full","hud":false,"treatment":"B"}}, + + {"out":"50-scale-reference.png","scene":"res://scenes/sandbox/ScaleReference.tscn","cam":"orbit","pivot":"-0.5,1.0,1.0","yaw":20,"pitch":20,"size":18,"wait":12}, + {"out":"51-scale-reference-iso.png","scene":"res://scenes/sandbox/ScaleReference.tscn","cam":"iso","pivot":"-0.5,1.0,0.5","size":13,"wait":12} + ] +} diff --git a/visual/RestaurantVisualSandbox/materials/.gitkeep b/visual/RestaurantVisualSandbox/materials/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/visual/RestaurantVisualSandbox/placeholders/.gitkeep b/visual/RestaurantVisualSandbox/placeholders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/visual/RestaurantVisualSandbox/project.godot b/visual/RestaurantVisualSandbox/project.godot new file mode 100644 index 0000000..1af8755 --- /dev/null +++ b/visual/RestaurantVisualSandbox/project.godot @@ -0,0 +1,32 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Restaurant Visual Sandbox — V0" +config/description="V0 — 3D Restaurant Readability Sandbox. Presentation-only gray-box. All activity is Mock/Scripted/VisualOnly and owns no gameplay truth (see docs/art/V0-PROTOTYPE-CONTRACT.md). Not integrated with the M0 RestaurantSim.Core." +run/main_scene="res://scenes/sandbox/Sandbox.tscn" +config/features=PackedStringArray("4.7", "Forward Plus") + +[autoload] + +V0="*res://scripts/v0_config.gd" + +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1080 +window/stretch/mode="canvas_items" + +[rendering] + +anti_aliasing/quality/msaa_3d=2 +environment/defaults/default_clear_color=Color(0.09, 0.1, 0.12, 1) +anti_aliasing/quality/screen_space_aa=1 diff --git a/visual/RestaurantVisualSandbox/scenes/cameras/.gitkeep b/visual/RestaurantVisualSandbox/scenes/cameras/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/visual/RestaurantVisualSandbox/scenes/characters/.gitkeep b/visual/RestaurantVisualSandbox/scenes/characters/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/visual/RestaurantVisualSandbox/scenes/diagnostics/.gitkeep b/visual/RestaurantVisualSandbox/scenes/diagnostics/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/visual/RestaurantVisualSandbox/scenes/diagnostics/AssetCompare.tscn b/visual/RestaurantVisualSandbox/scenes/diagnostics/AssetCompare.tscn new file mode 100644 index 0000000..a1253f0 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scenes/diagnostics/AssetCompare.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bv0assetcmp01"] + +[ext_resource type="Script" path="res://scripts/asset_compare.gd" id="1_ac"] + +[node name="AssetCompare" type="Node3D"] +script = ExtResource("1_ac") diff --git a/visual/RestaurantVisualSandbox/scenes/diagnostics/Capture.tscn b/visual/RestaurantVisualSandbox/scenes/diagnostics/Capture.tscn new file mode 100644 index 0000000..0cc25cc --- /dev/null +++ b/visual/RestaurantVisualSandbox/scenes/diagnostics/Capture.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bv0capture01"] + +[ext_resource type="Script" path="res://scripts/capture_runner.gd" id="1_cap"] + +[node name="Capture" type="Node"] +script = ExtResource("1_cap") diff --git a/visual/RestaurantVisualSandbox/scenes/diagnostics/Perf.tscn b/visual/RestaurantVisualSandbox/scenes/diagnostics/Perf.tscn new file mode 100644 index 0000000..b9636da --- /dev/null +++ b/visual/RestaurantVisualSandbox/scenes/diagnostics/Perf.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bv0perf01"] + +[ext_resource type="Script" path="res://scripts/perf_probe.gd" id="1_perf"] + +[node name="Perf" type="Node"] +script = ExtResource("1_perf") diff --git a/visual/RestaurantVisualSandbox/scenes/restaurant/.gitkeep b/visual/RestaurantVisualSandbox/scenes/restaurant/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/visual/RestaurantVisualSandbox/scenes/sandbox/Sandbox.tscn b/visual/RestaurantVisualSandbox/scenes/sandbox/Sandbox.tscn new file mode 100644 index 0000000..a2b4c4b --- /dev/null +++ b/visual/RestaurantVisualSandbox/scenes/sandbox/Sandbox.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bv0sandbox01"] + +[ext_resource type="Script" path="res://scripts/sandbox_root.gd" id="1_root"] + +[node name="Sandbox" type="Node3D"] +script = ExtResource("1_root") diff --git a/visual/RestaurantVisualSandbox/scenes/sandbox/ScaleReference.tscn b/visual/RestaurantVisualSandbox/scenes/sandbox/ScaleReference.tscn new file mode 100644 index 0000000..9be48d8 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scenes/sandbox/ScaleReference.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bv0scaleref01"] + +[ext_resource type="Script" path="res://scripts/scale_reference.gd" id="1_sref"] + +[node name="ScaleReference" type="Node3D"] +script = ExtResource("1_sref") diff --git a/visual/RestaurantVisualSandbox/scripts/asset_compare.gd b/visual/RestaurantVisualSandbox/scripts/asset_compare.gd new file mode 100644 index 0000000..0fbbb9e --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/asset_compare.gd @@ -0,0 +1,96 @@ +extends Node3D +## Asset-assisted comparison (§29 / §11 step 7). Loads a CC0 pack asset (Universal +## Animation Library, Godot-ready GLB) next to our 1 m cube and placeholder capsule +## to test IMPORT + SCALE + ANIMATION compatibility. The source binary lives in the +## git-ignored placeholders/_assetlab/; only the rendered comparison PNG is committed. +## If the asset is absent, the scene renders a note (so it stays reproducible). + +const GLB_PATH := "res://placeholders/_assetlab/UAL1_Standard.glb" + +func _ready() -> void: + Stage.setup(self, "A") + add_child(Placeholder.floor_tile(Vector2(14, 8), Color("22262e"))) + + # our known-scale references + var cube := Placeholder.unit_cube() + cube.position = Vector3(-3, 0, 0) + add_child(cube) + _label("1 m cube", Vector3(-3, 2.4, 0)) + + var capsule := Placeholder.person(V0.Role.CUSTOMER) + capsule.position = Vector3(-1, 0, 0) + add_child(capsule) + _label("V0 placeholder\n%.2f m" % V0.HUMAN_HEIGHT_M, Vector3(-1, 2.4, 0)) + + # the CC0 asset + var note := "asset not imported" + var packed = load(GLB_PATH) + if packed: + var inst = packed.instantiate() + add_child(inst) + inst.position = Vector3(1.6, 0, 0) + await get_tree().process_frame + var h := _scene_height(inst) + var anims := _anim_names(inst) + # pose it with the first animation to prove animations imported + var ap := _find_anim_player(inst) + if ap and anims.size() > 0: + ap.play(anims[0]) + ap.seek(0.4, true) + note = "UAL1_Standard.glb\nmeasured %.2f m tall\n%d animations" % [h, anims.size()] + print("[asset] imported %s: height=%.3f m, animations=%d %s" % [GLB_PATH, h, anims.size(), str(anims).substr(0, 120)]) + if h > 0.01: + print("[asset] scale factor to reach %.2f m target = %.3f" % [V0.HUMAN_HEIGHT_M, V0.HUMAN_HEIGHT_M / h]) + else: + print("[asset] GLB not importable/absent at ", GLB_PATH) + _label(note, Vector3(1.6, 2.4, 0), Color("52b788")) + + var rig := CameraRig.new() + rig.interactive = true + add_child(rig) + rig.set_mode(CameraRig.Mode.ORBIT_B) + rig.frame(Vector3(-0.6, 1.0, 0), 20.0, 12.0, 9.0) + +func _label(text: String, pos: Vector3, color := Color.WHITE) -> void: + var l := Placeholder.label(text, 0.24, color) + l.position = pos + add_child(l) + +func _scene_height(n: Node) -> float: + var merged := AABB() + var first := true + for mi in _all_meshes(n): + var a: AABB = mi.get_aabb() + a = mi.global_transform * a + if first: + merged = a + first = false + else: + merged = merged.merge(a) + return merged.size.y if not first else 0.0 + +func _all_meshes(n: Node, acc: Array = []) -> Array: + if n is MeshInstance3D: + acc.append(n) + for c in n.get_children(): + _all_meshes(c, acc) + return acc + +func _find_anim_player(n: Node) -> AnimationPlayer: + if n is AnimationPlayer: + return n + for c in n.get_children(): + var r := _find_anim_player(c) + if r: + return r + return null + +func _anim_names(n: Node) -> Array: + var ap := _find_anim_player(n) + if ap == null: + return [] + var out := [] + for a in ap.get_animation_list(): + if a != "RESET": + out.append(a) + return out diff --git a/visual/RestaurantVisualSandbox/scripts/asset_compare.gd.uid b/visual/RestaurantVisualSandbox/scripts/asset_compare.gd.uid new file mode 100644 index 0000000..b94a7c7 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/asset_compare.gd.uid @@ -0,0 +1 @@ +uid://bx06cj3jccoyd diff --git a/visual/RestaurantVisualSandbox/scripts/camera_rig.gd b/visual/RestaurantVisualSandbox/scripts/camera_rig.gd new file mode 100644 index 0000000..d5aad27 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/camera_rig.gd @@ -0,0 +1,190 @@ +class_name CameraRig +extends Node3D +## Three camera candidates in one rig (§19): +## ISO_A — classic isometric: orthographic, fixed high 3/4 angle, 90° step rotation. +## ORBIT_B — rotatable management camera: free orbit / pan / zoom within bounds. +## HYBRID_C — overview by default; focus on a target then return to overview. +## +## Driven interactively (mouse/keys) AND programmatically (capture harness). +## VisualOnly. + +enum Mode { ISO_A, ORBIT_B, HYBRID_C } + +@export var mode: int = Mode.ORBIT_B +@export var interactive: bool = true + +var pivot: Vector3 = Vector3.ZERO +var yaw: float = 45.0 +var pitch: float = 34.0 +var distance: float = 22.0 +var ortho_size: float = 22.0 +var cam: Camera3D + +const PITCH_MIN := 14.0 +const PITCH_MAX := 80.0 +const DIST_MIN := 6.0 +const DIST_MAX := 48.0 +const ORTHO_MIN := 7.0 +const ORTHO_MAX := 44.0 +const ISO_YAW := 45.0 +const ISO_PITCH := 35.264 # true isometric elevation + +var _focus_targets: Array = [] # Array[Vector3] +var _focus_index: int = -1 +var _overview := {} +var _tween: Tween +var _pan_speed := 8.0 + +func _ready() -> void: + cam = Camera3D.new() + cam.name = "Camera3D" + add_child(cam) + cam.current = true + cam.near = 0.05 + cam.far = 500.0 + _save_overview() + apply_mode() + +# ---- public API (also used by the capture harness) ----------------------- + +func set_mode(m: int) -> void: + mode = m + apply_mode() + +func apply_mode() -> void: + match mode: + Mode.ISO_A: + yaw = ISO_YAW + pitch = ISO_PITCH + distance = 80.0 # ortho: distance only avoids clipping + cam.projection = Camera3D.PROJECTION_ORTHOGONAL + cam.size = ortho_size + _: + cam.projection = Camera3D.PROJECTION_PERSPECTIVE + cam.fov = 50.0 + if distance < DIST_MIN: + distance = 22.0 + _update() + +func frame(p_pivot: Vector3, p_yaw: float, p_pitch: float, p_dist_or_size: float) -> void: + pivot = p_pivot + yaw = p_yaw + pitch = p_pitch + if cam and cam.projection == Camera3D.PROJECTION_ORTHOGONAL: + ortho_size = p_dist_or_size + else: + distance = p_dist_or_size + _save_overview() + _update() + +func orbit(dyaw: float, dpitch: float) -> void: + if mode == Mode.ISO_A: + return + yaw = fmod(yaw - dyaw, 360.0) + pitch = clampf(pitch + dpitch, PITCH_MIN, PITCH_MAX) + _update() + +func rotate_iso(step_deg: float) -> void: + yaw = fmod(yaw + step_deg, 360.0) + _update() + +func zoom(factor: float) -> void: + if cam.projection == Camera3D.PROJECTION_ORTHOGONAL: + ortho_size = clampf(ortho_size * factor, ORTHO_MIN, ORTHO_MAX) + else: + distance = clampf(distance * factor, DIST_MIN, DIST_MAX) + _update() + +func pan(dx: float, dz: float) -> void: + var yr := deg_to_rad(yaw) + var fwd := Vector3(sin(yr), 0, cos(yr)) + var right := Vector3(cos(yr), 0, -sin(yr)) + var scale := (ortho_size if cam.projection == Camera3D.PROJECTION_ORTHOGONAL else distance) * 0.06 + pivot += (right * dx + fwd * dz) * scale + _update() + +# ---- hybrid focus (§19 Candidate C) -------------------------------------- + +func register_focus_targets(targets: Array) -> void: + _focus_targets = targets.duplicate() + +func focus_on(target: Vector3, dist := 9.0, tween := true) -> void: + if _overview.is_empty(): + _save_overview() + var to_pivot := target + var to_pitch := 28.0 + var to_dist := dist + if tween and is_inside_tree(): + _do_tween(to_pivot, yaw, to_pitch, to_dist) + else: + pivot = to_pivot; pitch = to_pitch; distance = to_dist; _update() + +func focus_next() -> void: + if _focus_targets.is_empty(): + return + _focus_index = (_focus_index + 1) % _focus_targets.size() + focus_on(_focus_targets[_focus_index]) + +func return_to_overview(tween := true) -> void: + if _overview.is_empty(): + return + if tween and is_inside_tree(): + _do_tween(_overview.pivot, _overview.yaw, _overview.pitch, _overview.distance) + else: + pivot = _overview.pivot; yaw = _overview.yaw + pitch = _overview.pitch; distance = _overview.distance + _update() + _focus_index = -1 + +func _save_overview() -> void: + _overview = {"pivot": pivot, "yaw": yaw, "pitch": pitch, "distance": distance} + +func _do_tween(p: Vector3, y: float, pt: float, d: float) -> void: + if _tween and _tween.is_running(): + _tween.kill() + _tween = create_tween().set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) + _tween.tween_method(func(t): + pivot = pivot.lerp(p, t) + yaw = lerpf(yaw, y, t) + pitch = lerpf(pitch, pt, t) + distance = lerpf(distance, d, t) + _update(), 0.0, 1.0, 0.6) + +# ---- internal ------------------------------------------------------------- + +func _update() -> void: + if not cam: + return + var pr := deg_to_rad(pitch) + var yr := deg_to_rad(yaw) + var offset := Vector3(distance * cos(pr) * sin(yr), distance * sin(pr), distance * cos(pr) * cos(yr)) + cam.global_position = pivot + offset + cam.look_at(pivot, Vector3.UP) + if cam.projection == Camera3D.PROJECTION_ORTHOGONAL: + cam.size = ortho_size + +# ---- interaction ---------------------------------------------------------- + +func _unhandled_input(event: InputEvent) -> void: + if not interactive: + return + if event is InputEventMouseMotion and (event.button_mask & MOUSE_BUTTON_MASK_LEFT): + orbit(event.relative.x * 0.3, event.relative.y * 0.3) + elif event is InputEventMouseButton and event.pressed: + match event.button_index: + MOUSE_BUTTON_WHEEL_UP: + zoom(0.92) + MOUSE_BUTTON_WHEEL_DOWN: + zoom(1.08) + +func _process(delta: float) -> void: + if not interactive: + return + var mx := 0.0 + var mz := 0.0 + if Input.is_physical_key_pressed(KEY_A): mx -= 1.0 + if Input.is_physical_key_pressed(KEY_D): mx += 1.0 + if Input.is_physical_key_pressed(KEY_W): mz += 1.0 + if Input.is_physical_key_pressed(KEY_S): mz -= 1.0 + if mx != 0.0 or mz != 0.0: + pan(mx * _pan_speed * delta, mz * _pan_speed * delta) diff --git a/visual/RestaurantVisualSandbox/scripts/camera_rig.gd.uid b/visual/RestaurantVisualSandbox/scripts/camera_rig.gd.uid new file mode 100644 index 0000000..1ce7901 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/camera_rig.gd.uid @@ -0,0 +1 @@ +uid://cm2ilca6kx75d diff --git a/visual/RestaurantVisualSandbox/scripts/capture_runner.gd b/visual/RestaurantVisualSandbox/scripts/capture_runner.gd new file mode 100644 index 0000000..ecd1df8 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/capture_runner.gd @@ -0,0 +1,139 @@ +extends Node +## Reproducible screenshot harness (§29 evidence). Renders capturable scenes to +## PNG files at absolute OS paths, no manual screenshotting. VisualOnly / Prototype. +## +## Usage (from a shell): +## Godot --path PROJ res://scenes/diagnostics/Capture.tscn -- \ +## --scene=res://scenes/sandbox/ScaleReference.tscn --out=/abs/out.png \ +## --cam=orbit --pivot=0,1,0 --yaw=20 --pitch=22 --size=17 --wait=8 +## or a batch: +## Godot --path PROJ res://scenes/diagnostics/Capture.tscn -- --shots=res://data/shots_all.json +## +## A shot object: {scene,out,cam,pivot,yaw,pitch,size,wait, config:{...}} +## `config` is passed to the loaded scene's optional capture_configure() so the +## restaurant sandbox can be set to a scenario / seek time / overlay / treatment. + +var _args := {} + +func _ready() -> void: + _args = _parse_args(OS.get_cmdline_user_args()) + get_window().size = Vector2i(int(_args.get("w", 1920)), int(_args.get("h", 1080))) + var shots := _build_shots() + print("[capture] %d shot(s)" % shots.size()) + var ok := 0 + for shot in shots: + if await _run_shot(shot): + ok += 1 + print("[capture] done: %d/%d ok" % [ok, shots.size()]) + get_tree().quit(0 if ok == shots.size() else 1) + +func _build_shots() -> Array: + if _args.has("shots"): + var txt := FileAccess.get_file_as_string(_args["shots"]) + var data: Variant = JSON.parse_string(txt) + if typeof(data) == TYPE_DICTIONARY and data.has("shots"): + return data["shots"] + if typeof(data) == TYPE_ARRAY: + return data + push_error("[capture] bad shots file: " + _args["shots"]) + return [] + # single inline shot + return [{ + "scene": _args.get("scene", "res://scenes/sandbox/Sandbox.tscn"), + "out": _args.get("out", "/tmp/v0_capture.png"), + "cam": _args.get("cam", ""), + "pivot": _args.get("pivot", ""), + "yaw": _args.get("yaw", ""), + "pitch": _args.get("pitch", ""), + "size": _args.get("size", ""), + "wait": int(_args.get("wait", 10)), + }] + +func _run_shot(shot: Dictionary) -> bool: + var scene_path: String = shot.get("scene", "") + var packed: PackedScene = load(scene_path) + if packed == null: + push_error("[capture] cannot load " + scene_path) + return false + var inst := packed.instantiate() + add_child(inst) + # let the scene build itself + await get_tree().process_frame + await get_tree().process_frame + # scene-specific configuration (scenario/seek/overlay/treatment) + if shot.has("config") and inst.has_method("capture_configure"): + inst.capture_configure(shot["config"]) + await get_tree().process_frame + # camera override + var rig := _find_rig(inst) + if rig and shot.get("cam", "") != "": + rig.interactive = false + match str(shot["cam"]).to_lower(): + "iso": rig.set_mode(CameraRig.Mode.ISO_A) + "orbit": rig.set_mode(CameraRig.Mode.ORBIT_B) + "hybrid": rig.set_mode(CameraRig.Mode.HYBRID_C) + var pivot: Vector3 = _to_vec3(shot.get("pivot", ""), rig.pivot) + var yaw: float = _to_f(shot.get("yaw", ""), rig.yaw) + var pitch: float = _to_f(shot.get("pitch", ""), rig.pitch) + var siz: float = _to_f(shot.get("size", ""), (rig.ortho_size if rig.cam.projection == Camera3D.PROJECTION_ORTHOGONAL else rig.distance)) + rig.frame(pivot, yaw, pitch, siz) + # hybrid focus: after framing the overview, focus on a target (§19 Candidate C) + if shot.has("config") and shot["config"].has("focus"): + rig.focus_on(_to_vec3(shot["config"]["focus"], rig.pivot), _to_f(shot["config"].get("focus_dist", 9.0), 9.0), false) + # settle then capture + var wait: int = int(shot.get("wait", 10)) + for i in wait: + await get_tree().process_frame + await RenderingServer.frame_post_draw + var img := get_viewport().get_texture().get_image() + var out: String = shot.get("out", "capture.png") + if not out.begins_with("/") and _args.has("outdir"): + out = _args["outdir"].rstrip("/") + "/" + out + DirAccess.make_dir_recursive_absolute(out.get_base_dir()) + var err := img.save_png(out) + print("[capture] %s (%dx%d, err=%d) <- %s" % [out, img.get_width(), img.get_height(), err, scene_path]) + inst.queue_free() + await get_tree().process_frame + return err == OK + +# ---- helpers -------------------------------------------------------------- + +func _find_rig(n: Node) -> CameraRig: + if n is CameraRig: + return n + for c in n.get_children(): + var r := _find_rig(c) + if r: + return r + return null + +func _parse_args(argv: PackedStringArray) -> Dictionary: + var d := {} + for a in argv: + if a.begins_with("--"): + var kv := a.substr(2).split("=", true, 1) + d[kv[0]] = kv[1] if kv.size() > 1 else "1" + return d + +func _to_vec3(s: Variant, fallback: Vector3) -> Vector3: + if s == null: + return fallback + if s is Array and s.size() == 3: + return Vector3(s[0], s[1], s[2]) + var text := str(s) + if text == "": + return fallback + var parts := text.split(",") + if parts.size() == 3: + return Vector3(parts[0].to_float(), parts[1].to_float(), parts[2].to_float()) + return fallback + +func _to_f(s: Variant, fallback: float) -> float: + if s == null: + return fallback + if s is float or s is int: + return float(s) + var text := str(s) + if text == "": + return fallback + return text.to_float() diff --git a/visual/RestaurantVisualSandbox/scripts/capture_runner.gd.uid b/visual/RestaurantVisualSandbox/scripts/capture_runner.gd.uid new file mode 100644 index 0000000..dfb91e9 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/capture_runner.gd.uid @@ -0,0 +1 @@ +uid://bm6mreyw30jon diff --git a/visual/RestaurantVisualSandbox/scripts/mat_lib.gd b/visual/RestaurantVisualSandbox/scripts/mat_lib.gd new file mode 100644 index 0000000..071c1e9 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/mat_lib.gd @@ -0,0 +1,36 @@ +class_name MatLib +## Cached flat materials so the whole sandbox shares a small material set +## (keeps draw calls / state changes down — §28). VisualOnly. +extends RefCounted + +static var _cache: Dictionary = {} + +static func flat(color: Color, unshaded := false, emit := 0.0) -> StandardMaterial3D: + var key := "%s|%s|%.2f" % [color.to_html(), unshaded, emit] + if _cache.has(key): + return _cache[key] + var m := StandardMaterial3D.new() + m.albedo_color = color + m.roughness = 0.85 + m.metallic = 0.0 + if unshaded: + m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + if emit > 0.0: + m.emission_enabled = true + m.emission = color + m.emission_energy_multiplier = emit + _cache[key] = m + return m + +static func transparent(color: Color, alpha: float) -> StandardMaterial3D: + var key := "T|%s|%.2f" % [color.to_html(), alpha] + if _cache.has(key): + return _cache[key] + var m := StandardMaterial3D.new() + var c := color + c.a = alpha + m.albedo_color = c + m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + m.roughness = 0.9 + _cache[key] = m + return m diff --git a/visual/RestaurantVisualSandbox/scripts/mat_lib.gd.uid b/visual/RestaurantVisualSandbox/scripts/mat_lib.gd.uid new file mode 100644 index 0000000..a29c2bd --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/mat_lib.gd.uid @@ -0,0 +1 @@ +uid://c0tstgnh7kfbl diff --git a/visual/RestaurantVisualSandbox/scripts/mock_service.gd b/visual/RestaurantVisualSandbox/scripts/mock_service.gd new file mode 100644 index 0000000..86b4941 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/mock_service.gd @@ -0,0 +1,155 @@ +class_name MockService +extends RefCounted +## MOCK / SCRIPTED / VISUALONLY service timeline (§14). This is NOT a simulation +## and owns NO gameplay truth (§15). `snapshot(scenario, t)` is a PURE function of +## scripted time — it returns an authored per-beat snapshot of facts that the +## renderer merely displays. Being pure makes it seekable (scrub / reproducible +## capture). It must never be treated as authoritative; it is a stand-in for the +## future immutable simulation snapshot. + +const N_STATIONS := 4 +const CLOCK_END := 360.0 # a 6:00 scripted service + +static func scenario_names() -> Array: + return [ + "Healthy service", + "Kitchen bottleneck", + "Front-of-house bottleneck", + "Host & seating bottleneck", + ] + +# ---- pure snapshot -------------------------------------------------------- + +static func snapshot(scenario: int, t: float) -> Dictionary: + var bs := _beats(scenario) + var cur: Dictionary = bs[0] + for b in bs: + if t + 0.0001 >= float(b["t"]): + cur = b + else: + break + var snap := _defaults() + for k in cur: + snap[k] = cur[k] + snap["scenario"] = scenario + snap["scenario_name"] = scenario_names()[scenario] + snap["t"] = t + snap["clock"] = _clock(t) + return snap + +static func peak_time(scenario: int) -> float: + match scenario: + 1: return 165.0 + 2: return 180.0 + 3: return 90.0 + _: return 150.0 + +static func _defaults() -> Dictionary: + var stations := [] + for i in N_STATIONS: + stations.append([V0.StationState.NORMAL, 0]) + return { + "tables": {}, # id -> TableState (omitted == AVAILABLE) + "parties": {}, # id -> seat count present (for occupied tables) + "stations": stations, # [ [state, queue] x4 ] + "waiting": 0, # parties queued at host + "pass": 0, # ready plates sitting at the pass + "servers": ["idle", "idle", "idle"],# each server target: table id / "pass" / "idle" + "busser": "idle", # target table id / "idle" + "host": "idle", # "idle" / "greet" / "overloaded" + "overload_server": -1, # index of an overloaded server, or -1 + "leaving": 0, # customers walking out (abandon) + "headline": "", + } + +# ---- authored scenarios (peak beat is the money shot) --------------------- + +static func _beats(scenario: int) -> Array: + var S := V0.TableState + var K := V0.StationState + match scenario: + 1: # KITCHEN BOTTLENECK — grill overwhelmed, food never reaches the pass + return [ + {"t": 0, "headline": "Doors open. Kitchen calm."}, + {"t": 60, "headline": "Filling up, kitchen keeping pace.", + "tables": {"T1": S.EATING, "T5": S.OCCUPIED, "T2": S.WAITING_ORDER}, + "stations": [[K.QUEUE, 1], [K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T2", "pass", "idle"], "host": "greet", "waiting": 1}, + {"t": 120, "headline": "A grill queue is starting to build.", + "tables": {"T1": S.WAITING_FOOD, "T2": S.WAITING_FOOD, "T4": S.OCCUPIED, "T5": S.EATING, "T8": S.WAITING_ORDER}, + "stations": [[K.QUEUE, 3], [K.QUEUE, 1], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T8", "pass", "T4"], "host": "greet", "waiting": 2, "pass": 1}, + {"t": 165, "headline": "The grill is overwhelmed — food isn't reaching the pass; servers wait empty-handed.", + "tables": {"T1": S.WAITING_FOOD, "T2": S.WAITING_FOOD, "T4": S.WAITING_FOOD, "T5": S.WAITING_FOOD, "T8": S.WAITING_FOOD, "T7": S.EATING, "T3": S.OCCUPIED}, + "stations": [[K.CONGESTED, 5], [K.QUEUE, 3], [K.NORMAL, 1], [K.NORMAL, 0]], + "servers": ["pass", "pass", "pass"], "host": "greet", "waiting": 2, "pass": 0}, + {"t": 255, "headline": "Grill recovering; food moving again.", + "tables": {"T1": S.EATING, "T2": S.EATING, "T4": S.WAITING_FOOD, "T5": S.NEEDS_PAYMENT, "T7": S.DIRTY}, + "stations": [[K.QUEUE, 2], [K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T4", "T5", "pass"], "busser": "T7", "waiting": 1, "pass": 2}, + ] + 2: # FRONT-OF-HOUSE BOTTLENECK — kitchen fine, food/tables not being served + return [ + {"t": 0, "headline": "Doors open."}, + {"t": 90, "headline": "Kitchen humming; food starting to stack at the pass.", + "tables": {"T1": S.EATING, "T2": S.WAITING_ORDER, "T5": S.OCCUPIED, "T9": S.EATING}, + "stations": [[K.NORMAL, 0], [K.NORMAL, 0], [K.FOOD_READY, 0], [K.NORMAL, 0]], + "servers": ["T2", "idle", "idle"], "host": "greet", "waiting": 1, "pass": 3}, + {"t": 180, "headline": "Food's ready and tables need service — the kitchen is NOT the problem.", + "tables": {"T3": S.WAITING_ORDER, "T6": S.WAITING_ORDER, "T2": S.WAITING_ORDER, + "T1": S.NEEDS_PAYMENT, "T9": S.NEEDS_PAYMENT, "T7": S.DIRTY, "T8": S.DIRTY, "T4": S.DIRTY, + "T5": S.EATING}, + "stations": [[K.IDLE, 0], [K.NORMAL, 0], [K.IDLE, 0], [K.FOOD_READY, 0]], + "servers": ["T3", "overloaded", "overloaded"], "overload_server": 1, + "busser": "idle", "host": "greet", "waiting": 1, "pass": 6}, + {"t": 270, "headline": "Extra hands on the floor; backlog clearing.", + "tables": {"T1": S.EATING, "T3": S.WAITING_FOOD, "T7": S.CLEANING, "T8": S.DIRTY}, + "stations": [[K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T3", "T1", "pass"], "busser": "T7", "waiting": 0, "pass": 2}, + ] + 3: # HOST & SEATING BOTTLENECK — entrance queue while clean tables sit dirty + return [ + {"t": 0, "headline": "Doors open."}, + {"t": 45, "headline": "A queue is forming at the host stand.", + "tables": {"T1": S.EATING, "T2": S.DIRTY, "T5": S.DIRTY, "T9": S.EATING}, + "stations": [[K.NORMAL, 0], [K.NORMAL, 0], [K.IDLE, 0], [K.IDLE, 0]], + "servers": ["T1", "idle", "idle"], "host": "overloaded", "waiting": 4}, + {"t": 90, "headline": "The door is backed up while clean-able tables sit dirty — seating is the bottleneck. Kitchen has spare capacity.", + "tables": {"T1": S.EATING, "T9": S.EATING, "T2": S.DIRTY, "T5": S.DIRTY, "T8": S.DIRTY, "T4": S.DIRTY}, + "stations": [[K.IDLE, 0], [K.IDLE, 0], [K.IDLE, 0], [K.NORMAL, 0]], + "servers": ["idle", "idle", "idle"], "busser": "idle", + "host": "overloaded", "waiting": 5, "leaving": 1}, + {"t": 180, "headline": "Busser reset the tables; the queue is seating.", + "tables": {"T1": S.EATING, "T2": S.CLEANING, "T5": S.OCCUPIED, "T8": S.CLEANING, "T4": S.WAITING_ORDER}, + "stations": [[K.QUEUE, 1], [K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T4", "T5", "idle"], "busser": "T2", "host": "greet", "waiting": 2}, + ] + _: # HEALTHY — the reference for what "normal" looks like + return [ + {"t": 0, "headline": "Doors open."}, + {"t": 60, "headline": "First guests seated; smooth start.", + "tables": {"T1": S.EATING, "T2": S.WAITING_ORDER, "T5": S.OCCUPIED}, + "stations": [[K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T2", "pass", "idle"], "host": "greet", "waiting": 1, "pass": 1}, + {"t": 150, "headline": "Busy but under control — every zone keeping pace.", + "tables": {"T1": S.EATING, "T2": S.EATING, "T4": S.WAITING_FOOD, "T5": S.EATING, + "T7": S.WAITING_ORDER, "T8": S.OCCUPIED, "T3": S.NEEDS_PAYMENT, "T9": S.EATING}, + "stations": [[K.NORMAL, 1], [K.NORMAL, 0], [K.QUEUE, 1], [K.NORMAL, 0]], + "servers": ["T7", "T4", "T3"], "busser": "idle", "host": "greet", "waiting": 1, "pass": 1}, + {"t": 240, "headline": "Tables turning cleanly.", + "tables": {"T1": S.NEEDS_PAYMENT, "T2": S.DIRTY, "T4": S.EATING, "T5": S.CLEANING, "T8": S.EATING}, + "stations": [[K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0], [K.NORMAL, 0]], + "servers": ["T1", "pass", "T4"], "busser": "T5", "host": "idle", "waiting": 0, "pass": 1}, + ] + +# ---- helpers -------------------------------------------------------------- + +static func _clock(t: float) -> String: + var m := int(t) / 60 + var s := int(t) % 60 + return "%02d:%02d" % [m, s] + +## Deterministic pseudo-random in [0,1) from an int key (no Math.random; visual jitter only). +static func hash01(key: int) -> float: + var x := (key * 2654435761) & 0x7fffffff + return float(x % 10000) / 10000.0 diff --git a/visual/RestaurantVisualSandbox/scripts/mock_service.gd.uid b/visual/RestaurantVisualSandbox/scripts/mock_service.gd.uid new file mode 100644 index 0000000..533085e --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/mock_service.gd.uid @@ -0,0 +1 @@ +uid://cfexi0w2bt4s7 diff --git a/visual/RestaurantVisualSandbox/scripts/perf_probe.gd b/visual/RestaurantVisualSandbox/scripts/perf_probe.gd new file mode 100644 index 0000000..c3e5ea7 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/perf_probe.gd @@ -0,0 +1,93 @@ +extends Node +## Performance probe (§28). Runs the sandbox under a set of load configs, samples +## frame time / draw calls / primitives / video memory with vsync OFF (to expose +## real headroom), and writes a CSV + prints a summary. VisualOnly / Prototype. + +const SETTLE := 24 +const SAMPLE := 120 + +func _ready() -> void: + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) + Engine.max_fps = 0 + get_window().size = Vector2i(1920, 1080) + var out := "/tmp/v0_performance.csv" + for a in OS.get_cmdline_user_args(): + if a.begins_with("--out="): + out = a.substr(6) + + var configs := [ + {"name": "empty_restaurant", "cam": "orbit", "pivot": Vector3(-1.5, 0.6, -0.4), "yaw": 24, "pitch": 28, "size": 26, "cfg": {"scenario": 0, "seek": 0, "overlay": "off", "hud": false}}, + {"name": "healthy_service", "cam": "orbit", "pivot": Vector3(-1.5, 0.6, -0.4), "yaw": 24, "pitch": 28, "size": 26, "cfg": {"scenario": 0, "seek": 150, "overlay": "minimal", "hud": true}}, + {"name": "peak_kitchen_full", "cam": "iso", "pivot": Vector3(-2.5, 0.7, -1.0), "yaw": 45, "pitch": 35, "size": 18, "cfg": {"scenario": 1, "seek": "peak", "overlay": "full", "hud": true}}, + {"name": "peak_foh_all_diagnostics", "cam": "iso", "pivot": Vector3(-2.5, 0.7, -1.0), "yaw": 45, "pitch": 35, "size": 18, "cfg": {"scenario": 2, "seek": "peak", "overlay": "full", "hud": true}}, + {"name": "max_zoom_out", "cam": "iso", "pivot": Vector3(-1.5, 0.6, -0.4), "yaw": 45, "pitch": 35, "size": 40, "cfg": {"scenario": 1, "seek": "peak", "overlay": "full", "hud": true}}, + ] + + var rows := ["config,actors,mesh_instances,draw_calls,primitives,avg_fps,worst_fps,avg_frame_ms,worst_frame_ms,video_mem_mb"] + var packed: PackedScene = load("res://scenes/sandbox/Sandbox.tscn") + for c in configs: + var inst := packed.instantiate() + add_child(inst) + await get_tree().process_frame + await get_tree().process_frame + inst.capture_configure(c["cfg"]) + var rig := _find_rig(inst) + if rig: + rig.interactive = false + match c["cam"]: + "iso": rig.set_mode(CameraRig.Mode.ISO_A) + _: rig.set_mode(CameraRig.Mode.ORBIT_B) + rig.frame(c["pivot"], float(c["yaw"]), float(c["pitch"]), float(c["size"])) + for i in SETTLE: + await get_tree().process_frame + var acc := 0.0 + var worst := 0.0 + for i in SAMPLE: + await get_tree().process_frame + var dt := get_process_delta_time() + acc += dt + worst = maxf(worst, dt) + var avg := acc / SAMPLE + var draw := RenderingServer.get_rendering_info(RenderingServer.RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME) + var prims := RenderingServer.get_rendering_info(RenderingServer.RENDERING_INFO_TOTAL_PRIMITIVES_IN_FRAME) + var vmem := RenderingServer.get_rendering_info(RenderingServer.RENDERING_INFO_VIDEO_MEM_USED) + var meshes := _count_meshes(inst) + var actors := _count_actors(inst) + var row := "%s,%d,%d,%d,%d,%.0f,%.0f,%.2f,%.2f,%.1f" % [ + c["name"], actors, meshes, draw, prims, + 1.0 / avg, 1.0 / worst, avg * 1000.0, worst * 1000.0, vmem / 1048576.0] + rows.append(row) + print("[perf] ", row) + inst.queue_free() + await get_tree().process_frame + + var f := FileAccess.open(out, FileAccess.WRITE) + if f: + f.store_string("\n".join(rows) + "\n") + f.close() + print("[perf] wrote ", out) + get_tree().quit() + +func _find_rig(n: Node) -> CameraRig: + if n is CameraRig: + return n + for c in n.get_children(): + var r := _find_rig(c) + if r: + return r + return null + +func _count_meshes(n: Node) -> int: + var c := 1 if (n is MeshInstance3D and (n as MeshInstance3D).is_visible_in_tree()) else 0 + for ch in n.get_children(): + c += _count_meshes(ch) + return c + +func _count_actors(n: Node) -> int: + # each actor has exactly one visible capsule body — count those + var c := 0 + if n is MeshInstance3D and n.mesh is CapsuleMesh and (n as MeshInstance3D).is_visible_in_tree(): + c = 1 + for ch in n.get_children(): + c += _count_actors(ch) + return c diff --git a/visual/RestaurantVisualSandbox/scripts/perf_probe.gd.uid b/visual/RestaurantVisualSandbox/scripts/perf_probe.gd.uid new file mode 100644 index 0000000..e86537d --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/perf_probe.gd.uid @@ -0,0 +1 @@ +uid://qan6x8iwjun6 diff --git a/visual/RestaurantVisualSandbox/scripts/placeholder_factory.gd b/visual/RestaurantVisualSandbox/scripts/placeholder_factory.gd new file mode 100644 index 0000000..7ed710d --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/placeholder_factory.gd @@ -0,0 +1,212 @@ +class_name Placeholder +## Primitive placeholder builders (§11 placeholder-first). Every dimension reads +## from the V0 scale lock so nothing is scaled per-scene "to look right" (§17). +## All nodes are returned with their BASE at y=0 for trivial floor placement. +## VisualOnly — geometry carries no gameplay meaning. +extends RefCounted + +# ---- primitives ----------------------------------------------------------- + +static func box(size: Vector3, color: Color, base_at_zero := true) -> MeshInstance3D: + var mi := MeshInstance3D.new() + var m := BoxMesh.new() + m.size = size + mi.mesh = m + mi.material_override = MatLib.flat(color) + if base_at_zero: + mi.position.y = size.y * 0.5 + return mi + +static func cylinder(height: float, radius: float, color: Color) -> MeshInstance3D: + var mi := MeshInstance3D.new() + var m := CylinderMesh.new() + m.height = height + m.top_radius = radius + m.bottom_radius = radius + m.radial_segments = 16 + mi.mesh = m + mi.material_override = MatLib.flat(color) + mi.position.y = height * 0.5 + return mi + +static func floor_tile(size: Vector2, color: Color) -> MeshInstance3D: + var mi := MeshInstance3D.new() + var m := PlaneMesh.new() + m.size = size + mi.mesh = m + mi.material_override = MatLib.flat(color) + return mi + +static func wall(length: float, height: float, thick: float, color: Color) -> MeshInstance3D: + return box(Vector3(length, height, thick), color) + +static func label(text: String, size := 0.28, color := Color.WHITE) -> Label3D: + var l := Label3D.new() + l.text = text + l.font_size = 64 + l.pixel_size = size / 64.0 + l.modulate = color + l.billboard = BaseMaterial3D.BILLBOARD_ENABLED + l.no_depth_test = false + l.outline_size = 12 + l.outline_modulate = Color(0, 0, 0, 0.8) + return l + +# ---- furniture ------------------------------------------------------------ + +static func chair(color := Color("6b7280")) -> Node3D: + var n := Node3D.new() + var seat := box(Vector3(0.42, 0.06, 0.42), color) + seat.position.y = V0.CHAIR_SEAT_H_M + n.add_child(seat) + var back := box(Vector3(0.42, 0.45, 0.05), color) + back.position = Vector3(0, V0.CHAIR_SEAT_H_M + 0.22, -0.18) + n.add_child(back) + # four thin legs + for sx in [-0.17, 0.17]: + for sz in [-0.17, 0.17]: + var leg := box(Vector3(0.04, V0.CHAIR_SEAT_H_M, 0.04), color) + leg.position = Vector3(sx, V0.CHAIR_SEAT_H_M * 0.5, sz) + n.add_child(leg) + return n + +## Dining table with N seats and a floating STATE DISC (colored by scenario, §22). +static func table(seats: int, color := Color("8a8f98")) -> Node3D: + var n := Node3D.new() + n.name = "Table" + var top_size: Vector2 = V0.TABLE_2TOP if seats <= 2 else V0.TABLE_4TOP + var top := box(Vector3(top_size.x, 0.05, top_size.y), color) + top.position.y = V0.TABLE_HEIGHT_M + top.name = "Top" + n.add_child(top) + var post := box(Vector3(0.10, V0.TABLE_HEIGHT_M, 0.10), color) + n.add_child(post) + # seats around the long axis + var seat_positions := _seat_ring(seats, top_size) + for i in seat_positions.size(): + var ch := chair() + ch.position = seat_positions[i].position + ch.rotation.y = seat_positions[i].yaw + n.add_child(ch) + # floating state disc above the table (readability, §22/§25) + var disc := _state_disc() + disc.name = "StateDisc" + disc.position = Vector3(0, V0.TABLE_HEIGHT_M + 0.55, 0) + n.add_child(disc) + return n + +static func _seat_ring(seats: int, top: Vector2) -> Array: + var out := [] + var dx := top.x * 0.5 + 0.30 + var dz := top.y * 0.5 + 0.30 + if seats <= 2: + out.append({"position": Vector3(-dx, 0, 0), "yaw": deg_to_rad(90)}) + out.append({"position": Vector3(dx, 0, 0), "yaw": deg_to_rad(-90)}) + else: + out.append({"position": Vector3(-dx, 0, -0.2), "yaw": deg_to_rad(90)}) + out.append({"position": Vector3(-dx, 0, 0.2), "yaw": deg_to_rad(90)}) + out.append({"position": Vector3(dx, 0, -0.2), "yaw": deg_to_rad(-90)}) + out.append({"position": Vector3(dx, 0, 0.2), "yaw": deg_to_rad(-90)}) + return out + +## A thin, always-visible disc used as a table/station status pip. +static func _state_disc(color := V0.C_CALM) -> MeshInstance3D: + var mi := MeshInstance3D.new() + var m := CylinderMesh.new() + m.height = 0.07 + m.top_radius = 0.34 + m.bottom_radius = 0.34 + m.radial_segments = 22 + mi.mesh = m + mi.material_override = MatLib.flat(color, false, 0.9) + return mi + +static func counter(length: float, color := Color("7a7f88")) -> Node3D: + var n := Node3D.new() + var c := box(Vector3(length, V0.COUNTER_HEIGHT_M, 0.7), color) + n.add_child(c) + return n + +static func appliance(color := Color("5b6068")) -> Node3D: + var n := Node3D.new() + n.add_child(box(Vector3(V0.APPLIANCE_W_M, V0.APPLIANCE_H_M, 0.7), color)) + return n + +static func door_frame(color := Color("caa06a")) -> Node3D: + var n := Node3D.new() + var post_w := 0.12 + for sx in [-1, 1]: + var post := box(Vector3(post_w, V0.DOOR_HEIGHT_M, 0.16), color) + post.position.x = sx * (V0.DOOR_WIDTH_M * 0.5 + post_w * 0.5) + n.add_child(post) + var lintel := box(Vector3(V0.DOOR_WIDTH_M + post_w * 2, 0.16, 0.16), color) + lintel.position.y = V0.DOOR_HEIGHT_M + n.add_child(lintel) + return n + +## 1 m reference cube (wire-look via emissive edges + translucent body). +static func unit_cube(color := Color("ffd166")) -> Node3D: + var n := Node3D.new() + var body := box(Vector3(1, 1, 1), color) + body.material_override = MatLib.transparent(color, 0.25) + n.add_child(body) + return n + +# ---- actors (§21 role readability: colour + a second, non-colour signal) -- + +static func person(role: int) -> Node3D: + var info: Dictionary = V0.ROLE[role] + var h: float = info["height"] + var col: Color = info["color"] + var n := Node3D.new() + n.name = "Actor_" + str(info["name"]) + # body capsule, feet at y=0 + var body := MeshInstance3D.new() + var cap := CapsuleMesh.new() + cap.height = h + cap.radius = V0.HUMAN_RADIUS_M + cap.radial_segments = 12 + cap.rings = 6 + body.mesh = cap + body.material_override = MatLib.flat(col) + body.position.y = h * 0.5 + body.name = "Body" + n.add_child(body) + # a small "head" cap so silhouettes read as people, tinted by role + var head := MeshInstance3D.new() + var hs := SphereMesh.new() + hs.radius = 0.16 + hs.height = 0.32 + head.mesh = hs + head.material_override = MatLib.flat(col.lightened(0.15)) + head.position.y = h - 0.12 + n.add_child(head) + _attach_role_signal(n, role, h) + return n + +## The mandatory SECOND signal per role — never colour alone (§21). +static func _attach_role_signal(n: Node3D, role: int, h: float) -> void: + match role: + V0.Role.COOK: # white toque + var toque := cylinder(0.22, 0.14, Color.WHITE) + toque.position = Vector3(0, h - 0.02, 0) + n.add_child(toque) + V0.Role.SERVER: # tray held forward + var tray := box(Vector3(0.34, 0.03, 0.28), Color("cfd8e3")) + tray.position = Vector3(0, h * 0.62, 0.32) + n.add_child(tray) + V0.Role.HOST: # clipboard + var cb := box(Vector3(0.20, 0.26, 0.02), Color("2b3a44")) + cb.position = Vector3(0.20, h * 0.60, 0.20) + cb.rotation.x = deg_to_rad(-20) + n.add_child(cb) + V0.Role.BUSSER: # bus bin + var bin := box(Vector3(0.34, 0.22, 0.26), Color("2f6b4a")) + bin.position = Vector3(0, h * 0.55, 0.30) + n.add_child(bin) + V0.Role.MANAGER: # floating star marker + var star := label("★", 0.5, Color("ffd166")) + star.position = Vector3(0, h + 0.45, 0) + n.add_child(star) + V0.Role.CUSTOMER: + pass # guests carry no staff prop; identity is "the ones being served" diff --git a/visual/RestaurantVisualSandbox/scripts/placeholder_factory.gd.uid b/visual/RestaurantVisualSandbox/scripts/placeholder_factory.gd.uid new file mode 100644 index 0000000..bb27ecf --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/placeholder_factory.gd.uid @@ -0,0 +1 @@ +uid://bvm15er8fplo1 diff --git a/visual/RestaurantVisualSandbox/scripts/restaurant.gd b/visual/RestaurantVisualSandbox/scripts/restaurant.gd new file mode 100644 index 0000000..4309dd6 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/restaurant.gd @@ -0,0 +1,292 @@ +class_name Restaurant +extends Node3D +## Gray-box restaurant shell (§18). One compact floor that reads from one +## management view but exposes distinct operational zones: entrance/host, +## waiting, dining (10 tables / 32 seats), a compact 4-station kitchen with a +## pass, dishwashing, restrooms reached by a deliberately NARROW corridor +## (the circulation-risk pinch), and a staff door. Exterior walls support +## solid / auto-hide / cutaway / transparent modes (§20); interior dividers are +## half-height so the management camera sees over them. VisualOnly geometry — +## owns no gameplay truth. + +# Interior extents (metres). X = width, Z = depth (+Z = front/entrance). +const X_MIN := -9.0 +const X_MAX := 9.0 +const Z_MIN := -7.0 # back (kitchen / restrooms) +const Z_MAX := 7.0 # front (entrance) +const DIVIDER_H := 1.30 + +# Public data model consumed by the scripted service (§14/§15 snapshot source). +var tables: Array = [] # {id, node, seats, center, seats_world, disc} +var stations: Array = [] # {id, name, node, pos, queue_anchor, pass_slot, bar} +var host_pos: Vector3 +var entrance_pos: Vector3 +var waiting_slots: Array = [] # Vector3 +var pass_center: Vector3 +var dish_pos: Vector3 +var staff_door_pos: Vector3 +var focus_targets: Array = [] # Vector3, for hybrid camera + +var walls := Node3D.new() +var roof := Node3D.new() +var partitions := Node3D.new() +var zone_labels := Node3D.new() +var _ext_walls: Array = [] # {node, inward: Vector3, center: Vector3} + +func build() -> void: + walls.name = "ExteriorWalls" + roof.name = "Roof" + partitions.name = "Partitions" + zone_labels.name = "ZoneLabels" + add_child(walls) + add_child(roof) + add_child(partitions) + add_child(zone_labels) + _floor() + _exterior_walls_and_roof() + _kitchen() + _pass_and_dish() + _restrooms_and_corridor() + _host_and_waiting() + _dining_tables() + _zone_markers() + roof.visible = false # management view: roof off by default (§20) + +# ---------------------------------------------------------------- floor ----- + +func _floor() -> void: + var base := Placeholder.floor_tile(Vector2(X_MAX - X_MIN, Z_MAX - Z_MIN), Color("39424e")) + base.name = "Floor" + base.position = Vector3((X_MIN + X_MAX) * 0.5, 0, (Z_MIN + Z_MAX) * 0.5) + add_child(base) + # faint zone tints aid zone recognition without labels (§18) + _zone_patch(Vector3(-5.25, 0.01, -4.75), Vector2(7.5, 4.5), Color("323a44")) # kitchen + _zone_patch(Vector3(5.5, 0.01, -5.5), Vector2(6.0, 3.0), Color("35303a")) # restrooms + _zone_patch(Vector3(5.5, 0.01, 5.0), Vector2(6.0, 4.0), Color("2f3a3a")) # waiting/host + +func _zone_patch(pos: Vector3, size: Vector2, color: Color) -> void: + var p := Placeholder.floor_tile(size, color) + p.position = pos + add_child(p) + +# ------------------------------------------------- exterior walls / roof ---- + +func _exterior_walls_and_roof() -> void: + var h := V0.WALL_HEIGHT_M + var t := V0.WALL_THICK_M + var col := Color("3b414b") + # Back (north, Z_MIN) — solid + _ext_wall(Vector3(0, h * 0.5, Z_MIN), Vector3(X_MAX - X_MIN + t, h, t), Vector3(0, 0, 1), col) + # Left (west, X_MIN) + _ext_wall(Vector3(X_MIN, h * 0.5, 0), Vector3(t, h, Z_MAX - Z_MIN + t), Vector3(1, 0, 0), col) + # Right (east, X_MAX) + _ext_wall(Vector3(X_MAX, h * 0.5, 0), Vector3(t, h, Z_MAX - Z_MIN + t), Vector3(-1, 0, 0), col) + # Front (south, Z_MAX) with an entrance opening near X = +5.5 + var door_x := 5.5 + var gap := 1.4 + var left_len := (door_x - gap * 0.5) - X_MIN + _ext_wall(Vector3(X_MIN + left_len * 0.5, h * 0.5, Z_MAX), Vector3(left_len, h, t), Vector3(0, 0, -1), col) + var right_len := X_MAX - (door_x + gap * 0.5) + _ext_wall(Vector3(X_MAX - right_len * 0.5, h * 0.5, Z_MAX), Vector3(right_len, h, t), Vector3(0, 0, -1), col) + entrance_pos = Vector3(door_x, 0, Z_MAX - 0.4) + var frame := Placeholder.door_frame() + frame.position = Vector3(door_x, 0, Z_MAX) + add_child(frame) + # roof (hidden by default) + var r := Placeholder.box(Vector3(X_MAX - X_MIN + t, 0.15, Z_MAX - Z_MIN + t), Color("2e333c"), false) + r.position = Vector3(0, h + 0.075, 0) + roof.add_child(r) + +func _ext_wall(center: Vector3, size: Vector3, inward: Vector3, color: Color) -> void: + var w := Placeholder.box(size, color, false) + w.position = center + walls.add_child(w) + _ext_walls.append({"node": w, "inward": inward, "center": center}) + +# --------------------------------------------------------------- kitchen ---- + +func _kitchen() -> void: + # Kitchen occupies back-left: X -9..-1.5, Z -7..-2.6. Half-height divider + # separates it from dining, with the pass as the opening. + var div_col := Color("454b55") + # vertical divider between kitchen and restroom/dining side at X = -1.5, Z -7..-2.6 + _divider(Vector3(-1.5, DIVIDER_H * 0.5, -4.8), Vector3(V0.WALL_THICK_M, DIVIDER_H, 4.4), div_col) + # Four distinct stations along the back wall (§12): Grill, Fry, Range, Garde/Plating + var names := ["Grill", "Fry", "Range", "Garde"] + var appliance_cols := [Color("6b4a3a"), Color("6b5a3a"), Color("4a5a6b"), Color("4a6b5a")] + var xs := [-8.0, -6.0, -4.0, -2.4] + for i in 4: + var st := Node3D.new() + st.name = "Station_" + names[i] + st.position = Vector3(xs[i], 0, -6.2) + add_child(st) + var cabinet := Placeholder.counter(1.6, Color("4d535d")) + st.add_child(cabinet) + var appl := Placeholder.appliance(appliance_cols[i]) + appl.position = Vector3(0, 0, 0.15) + st.add_child(appl) + # station status bar (queue pressure) — vertical gauge above the station (§22/§25) + var bar := _status_bar() + bar.position = Vector3(0, 2.15, 0) + st.add_child(bar) + var lbl := Placeholder.label(names[i], 0.24, Color("cfd8e3")) + lbl.position = Vector3(0, 2.25, 0) + st.add_child(lbl) + stations.append({ + "id": i, "name": names[i], "node": st, + "pos": st.position, + "queue_anchor": st.position + Vector3(0, 0, 1.0), + "pass_slot": Vector3(xs[i], V0.COUNTER_HEIGHT_M + 0.15, -2.7), + "bar": bar, + }) + focus_targets.append(st.position + Vector3(0, 1.2, 0.6)) + +func _pass_and_dish() -> void: + # The pass: a counter line at Z = -2.6 where finished food appears (§12). + var pass_counter := Placeholder.counter(6.4, Color("6a6f78")) + pass_counter.position = Vector3(-5.0, 0, -2.6) + add_child(pass_counter) + pass_center = Vector3(-5.0, V0.COUNTER_HEIGHT_M, -2.4) + var plabel := Placeholder.label("PASS", 0.26, Color("ffd166")) + plabel.position = Vector3(-5.0, 1.7, -2.4) + zone_labels.add_child(plabel) + # Dishwashing zone in the kitchen's near-left corner. + var dish := Placeholder.counter(1.6, Color("41535e")) + dish.rotation.y = deg_to_rad(90) + dish.position = Vector3(-8.6, 0, -3.4) + add_child(dish) + dish_pos = Vector3(-8.2, 0, -3.4) + var dlabel := Placeholder.label("DISH", 0.20, Color("9fb3c8")) + dlabel.position = Vector3(-8.4, 1.5, -3.4) + zone_labels.add_child(dlabel) + # staff-only / storage door on the back wall + staff_door_pos = Vector3(-0.5, 0, Z_MIN + 0.3) + var sd := Placeholder.box(Vector3(1.0, 2.05, 0.1), Color("55402f"), false) + sd.position = Vector3(-0.5, 1.02, Z_MIN + 0.12) + add_child(sd) + +# ------------------------------------------------ restrooms + the pinch ----- + +func _restrooms_and_corridor() -> void: + var div_col := Color("454b55") + # Restroom block back-right: X +2.5..+9, Z -7..-4. Corridor from dining runs + # north between X=+3.2 walls -> a ~0.9 m PINCH (the circulation risk, §18). + # corridor walls + _divider(Vector3(3.2, DIVIDER_H * 0.5, -3.0), Vector3(V0.WALL_THICK_M, DIVIDER_H, 4.0), div_col) # left wall of corridor + _divider(Vector3(4.1, DIVIDER_H * 0.5, -3.0), Vector3(V0.WALL_THICK_M, DIVIDER_H, 3.0), div_col) # right wall of corridor (leaves mouth) + # restroom back partition + _divider(Vector3(5.75, DIVIDER_H * 0.5, -4.2), Vector3(6.5, DIVIDER_H, V0.WALL_THICK_M), div_col) + # two stalls + for sx in [5.0, 7.0]: + var stall := Placeholder.box(Vector3(1.2, 1.0, 1.2), Color("50566180"), false) + stall.material_override = MatLib.transparent(Color("6b7280"), 0.5) + stall.position = Vector3(sx, 0.5, -5.6) + add_child(stall) + var wc := Placeholder.label("WC", 0.24, Color("9fb3c8")) + wc.position = Vector3(5.75, 1.6, -4.6) + zone_labels.add_child(wc) + +func _divider(center: Vector3, size: Vector3, color: Color) -> void: + var d := Placeholder.box(size, color, false) + d.position = center + partitions.add_child(d) + +## Vertical floating gauge for kitchen-station pressure — colour = state, height = queue (§22/§25). +func _status_bar() -> MeshInstance3D: + var mi := Placeholder.box(Vector3(0.26, 1.5, 0.26), V0.C_CALM, false) + mi.material_override = MatLib.flat(V0.C_CALM, false, 0.85) + return mi + +# ----------------------------------------------------- host + waiting ------- + +func _host_and_waiting() -> void: + host_pos = Vector3(5.0, 0, 4.6) + var podium := Placeholder.box(Vector3(1.0, 1.1, 0.5), Color("5a6472"), false) + podium.position = Vector3(5.0, 0.55, 4.6) + add_child(podium) + var hlabel := Placeholder.label("HOST", 0.22, Color("2ec4b6")) + hlabel.position = Vector3(5.0, 1.6, 4.6) + zone_labels.add_child(hlabel) + # waiting bench + standing slots + var bench := Placeholder.box(Vector3(2.4, 0.45, 0.5), Color("54606e"), false) + bench.position = Vector3(6.6, 0.225, 6.0) + add_child(bench) + for i in 4: + waiting_slots.append(Vector3(4.4 + i * 0.55, 0, 5.9)) + var wl := Placeholder.label("WAITING", 0.2, Color("9fb3c8")) + wl.position = Vector3(6.2, 1.4, 6.2) + zone_labels.add_child(wl) + focus_targets.append(Vector3(5.0, 1.2, 5.2)) + +# ------------------------------------------------------ dining tables ------- + +func _dining_tables() -> void: + # 10 tables / 32 seats. Hand-placed for a main 1.4 m aisle and one tight gap. + var layout := [ + {"id": "T1", "seats": 4, "pos": Vector3(-7.0, 0, 4.8)}, + {"id": "T2", "seats": 4, "pos": Vector3(-3.8, 0, 4.8)}, + {"id": "T3", "seats": 2, "pos": Vector3(-0.8, 0, 5.2)}, + {"id": "T4", "seats": 4, "pos": Vector3(-7.0, 0, 1.8)}, + {"id": "T5", "seats": 4, "pos": Vector3(-3.8, 0, 1.8)}, + {"id": "T6", "seats": 2, "pos": Vector3(-0.8, 0, 2.2)}, + {"id": "T7", "seats": 4, "pos": Vector3(-7.0, 0, -1.0)}, + {"id": "T8", "seats": 4, "pos": Vector3(-3.8, 0, -1.0)}, + {"id": "T9", "seats": 2, "pos": Vector3(1.7, 0, 5.2)}, + {"id": "T10", "seats": 2, "pos": Vector3(1.7, 0, 2.6)}, + ] + for t in layout: + var node := Placeholder.table(t["seats"]) + node.name = t["id"] + node.position = t["pos"] + add_child(node) + var disc: MeshInstance3D = node.get_node("StateDisc") + tables.append({ + "id": t["id"], "node": node, "seats": t["seats"], + "center": t["pos"], "disc": disc, + }) + var idlbl := Placeholder.label(t["id"], 0.18, Color("aeb7c2")) + idlbl.position = t["pos"] + Vector3(0, 0.05, 0) + idlbl.billboard = BaseMaterial3D.BILLBOARD_DISABLED + idlbl.rotation.x = deg_to_rad(-90) + add_child(idlbl) + focus_targets.append(t["pos"] + Vector3(0, 1.0, 0)) + +func _zone_markers() -> void: + var dl := Placeholder.label("KITCHEN", 0.30, Color("f4a261")) + dl.position = Vector3(-5.0, 2.7, -6.9) + zone_labels.add_child(dl) + var din := Placeholder.label("DINING", 0.30, Color("8ecae6")) + din.position = Vector3(-4.5, 2.7, 3.0) + zone_labels.add_child(din) + +# ------------------------------------------------- wall/roof modes (§20) ---- + +enum WallMode { SOLID, AUTO_HIDE, CUTAWAY, TRANSPARENT } + +func set_wall_mode(mode: int, cam_pos: Vector3) -> void: + for w in _ext_walls: + var node: MeshInstance3D = w["node"] + var center: Vector3 = w["center"] + var inward: Vector3 = w["inward"] + var faces_camera := inward.dot(cam_pos - center) < 0.0 # wall sits between camera and interior + node.visible = true + node.material_override = MatLib.flat(Color("3b414b")) + match mode: + WallMode.SOLID: + pass + WallMode.AUTO_HIDE: + if faces_camera: + node.visible = false + WallMode.CUTAWAY: + if faces_camera: + node.visible = false + roof.visible = false + WallMode.TRANSPARENT: + if faces_camera: + node.material_override = MatLib.transparent(Color("6b7280"), 0.16) + +func set_roof(v: bool) -> void: + roof.visible = v + +func set_zone_labels(v: bool) -> void: + zone_labels.visible = v diff --git a/visual/RestaurantVisualSandbox/scripts/restaurant.gd.uid b/visual/RestaurantVisualSandbox/scripts/restaurant.gd.uid new file mode 100644 index 0000000..c4f084c --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/restaurant.gd.uid @@ -0,0 +1 @@ +uid://dxlekl3pmvsf7 diff --git a/visual/RestaurantVisualSandbox/scripts/sandbox_root.gd b/visual/RestaurantVisualSandbox/scripts/sandbox_root.gd new file mode 100644 index 0000000..88a1fd9 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/sandbox_root.gd @@ -0,0 +1,166 @@ +extends Node3D +## V0 sandbox root: stage + gray-box restaurant + camera rig + the scripted +## MockService and its ServiceView renderer, plus interactive controls and the +## capture-harness hook. Presentation-only; owns no gameplay truth (§15). + +const RATE := 6.0 # scripted seconds per real second at speed 1.0 + +var restaurant: Restaurant +var view: ServiceView +var rig: CameraRig +var _wall_mode: int = Restaurant.WallMode.AUTO_HIDE +var _treatment := "A" + +var scenario := 0 +var clock := 0.0 +var playing := true +var speed := 1.0 + +func _ready() -> void: + Stage.setup(self, _treatment) + restaurant = Restaurant.new() + restaurant.name = "Restaurant" + add_child(restaurant) + restaurant.build() + + view = ServiceView.new() + view.name = "ServiceView" + add_child(view) + view.setup(restaurant) + + rig = CameraRig.new() + rig.name = "CameraRig" + add_child(rig) + rig.set_mode(CameraRig.Mode.ORBIT_B) + rig.register_focus_targets(restaurant.focus_targets) + _frame_overview() + _apply_walls() + _apply_snapshot() + _apply_launch_args() + print("[V0] sandbox ready: scenario=%s" % MockService.scenario_names()[scenario]) + +# Recording driver (Godot Movie Maker). --rec_mode=service plays the scenario; +# --rec_mode=orbit freezes at the peak and slowly orbits the camera. +var _rec_mode := "" +const REC_ORBIT_DPS := 30.0 + +func _apply_launch_args() -> void: + for a in OS.get_cmdline_user_args(): + if a.begins_with("--rec_mode="): _rec_mode = a.substr(11) + elif a.begins_with("--rec_scenario="): scenario = int(a.substr(15)) + elif a.begins_with("--rec_speed="): speed = float(a.substr(12)) + elif a.begins_with("--rec_overlay="): + match a.substr(14): + "off": view.set_overlay_mode(ServiceView.Overlay.OFF) + "minimal": view.set_overlay_mode(ServiceView.Overlay.MINIMAL) + "full": view.set_overlay_mode(ServiceView.Overlay.FULL) + elif a.begins_with("--rec_cam="): + match a.substr(10): + "iso": rig.set_mode(CameraRig.Mode.ISO_A); _frame_overview() + "orbit": rig.set_mode(CameraRig.Mode.ORBIT_B); _frame_overview() + if _rec_mode == "service": + clock = 0.0 + playing = true + elif _rec_mode == "orbit": + clock = MockService.peak_time(scenario) + playing = false + _apply_snapshot() + +func _process(dt: float) -> void: + if playing: + clock += dt * RATE * speed + if clock > MockService.CLOCK_END: + clock = 0.0 + _apply_snapshot() + if _rec_mode == "orbit": + rig.orbit(REC_ORBIT_DPS * dt, 0.0) + if _wall_mode != Restaurant.WallMode.SOLID: + _apply_walls() + +func _apply_snapshot() -> void: + view.apply(MockService.snapshot(scenario, clock)) + +func _frame_overview() -> void: + var pivot := Vector3(-1.5, 0.6, -0.4) + match rig.mode: + CameraRig.Mode.ISO_A: + rig.frame(pivot, 45.0, rig.ISO_PITCH, 22.0) + _: + rig.frame(pivot, 24.0, 27.0, 26.0) + +func _apply_walls() -> void: + if is_instance_valid(rig) and rig.cam: + restaurant.set_wall_mode(_wall_mode, rig.cam.global_position) + +# ---- interactive controls ------------------------------------------------- + +func _unhandled_input(e: InputEvent) -> void: + if not (e is InputEventKey and e.pressed and not e.echo): + return + match e.physical_keycode: + KEY_1: rig.set_mode(CameraRig.Mode.ISO_A); _frame_overview() + KEY_2: rig.set_mode(CameraRig.Mode.ORBIT_B); _frame_overview() + KEY_3: rig.set_mode(CameraRig.Mode.HYBRID_C); _frame_overview() + KEY_H: _cycle_wall_mode() + KEY_J: restaurant.set_roof(not restaurant.roof.visible) + KEY_L: restaurant.set_zone_labels(not restaurant.zone_labels.visible) + KEY_K: view.set_hud_visible(not view._hud.visible) + KEY_TAB: view.cycle_overlay() + KEY_F: rig.focus_next() + KEY_G: rig.return_to_overview() + KEY_SPACE: playing = not playing + KEY_R: clock = 0.0; _apply_snapshot() + KEY_P: clock = MockService.peak_time(scenario); playing = false; _apply_snapshot() + KEY_COMMA: speed = maxf(0.25, speed * 0.5) + KEY_PERIOD: speed = minf(8.0, speed * 2.0) + KEY_F1: _set_scenario(0) + KEY_F2: _set_scenario(1) + KEY_F3: _set_scenario(2) + KEY_F4: _set_scenario(3) + +func _set_scenario(s: int) -> void: + scenario = s + clock = 0.0 + playing = true + _apply_snapshot() + print("[V0] scenario = %s" % MockService.scenario_names()[scenario]) + +func _cycle_wall_mode() -> void: + _wall_mode = (_wall_mode + 1) % 4 + _apply_walls() + +# ---- capture-harness hook ------------------------------------------------- + +func capture_configure(cfg: Dictionary) -> void: + if cfg.has("treatment") and cfg["treatment"] != _treatment: + _treatment = cfg["treatment"] + for c in get_children(): + if c is DirectionalLight3D or c is WorldEnvironment: + c.queue_free() + Stage.setup(self, _treatment) + if cfg.has("scenario"): + scenario = int(cfg["scenario"]) + if cfg.has("seek"): + if str(cfg["seek"]).to_lower() == "peak": + clock = MockService.peak_time(scenario) + else: + clock = float(cfg["seek"]) + if cfg.has("overlay"): + match str(cfg["overlay"]).to_lower(): + "off": view.set_overlay_mode(ServiceView.Overlay.OFF) + "minimal": view.set_overlay_mode(ServiceView.Overlay.MINIMAL) + "full": view.set_overlay_mode(ServiceView.Overlay.FULL) + "selection": view.set_overlay_mode(ServiceView.Overlay.SELECTION) + if cfg.has("select"): + view.select_table(str(cfg["select"])) + if cfg.has("hud"): + view.set_hud_visible(bool(cfg["hud"])) + if cfg.has("wall_mode"): + _wall_mode = int(cfg["wall_mode"]) + if cfg.has("roof"): + restaurant.set_roof(bool(cfg["roof"])) + if cfg.has("zone_labels"): + restaurant.set_zone_labels(bool(cfg["zone_labels"])) + playing = false + _apply_snapshot() + _apply_walls() diff --git a/visual/RestaurantVisualSandbox/scripts/sandbox_root.gd.uid b/visual/RestaurantVisualSandbox/scripts/sandbox_root.gd.uid new file mode 100644 index 0000000..042a6b5 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/sandbox_root.gd.uid @@ -0,0 +1 @@ +uid://cyuqi124voa1p diff --git a/visual/RestaurantVisualSandbox/scripts/scale_reference.gd b/visual/RestaurantVisualSandbox/scripts/scale_reference.gd new file mode 100644 index 0000000..4d4aa5f --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/scale_reference.gd @@ -0,0 +1,71 @@ +extends Node3D +## Scale-reference scene (§17). A labelled lineup so every imported asset can be +## compared against known human-scale primitives — the scene is NOT re-scaled to +## look right; assets are scaled to match this. VisualOnly. + +func _ready() -> void: + Stage.setup(self, "A") + var floor := Placeholder.floor_tile(Vector2(26, 12), Color("22262e")) + add_child(floor) + _grid() + + var x := -9.0 + var step := 2.6 + x = _place(Placeholder.unit_cube(), x, "1 m cube", "1.0 × 1.0 × 1.0 m") + x += step + x = _place(Placeholder.person(V0.Role.CUSTOMER), x, "Adult", "%.2f m tall" % V0.HUMAN_HEIGHT_M) + x += step + x = _place(Placeholder.door_frame(), x, "Door", "%.2f × %.2f m" % [V0.DOOR_HEIGHT_M, V0.DOOR_WIDTH_M]) + x += step + x = _place(Placeholder.table(2), x, "Table 2-top", "H %.2f m" % V0.TABLE_HEIGHT_M) + x += step + 0.6 + x = _place(Placeholder.table(4), x, "Table 4-top", "%.2f × %.2f m" % [V0.TABLE_4TOP.x, V0.TABLE_4TOP.y]) + x += step + 0.6 + x = _place(Placeholder.chair(), x, "Chair", "seat %.2f m" % V0.CHAIR_SEAT_H_M) + x += step + x = _place(Placeholder.counter(1.2), x, "Counter", "H %.2f m" % V0.COUNTER_HEIGHT_M) + x += step + x = _place(Placeholder.appliance(), x, "Appliance", "%.2f × %.2f m" % [V0.APPLIANCE_W_M, V0.APPLIANCE_H_M]) + + # aisle-width reference strip (main / kitchen / pinch) laid on the floor + _aisle_swatch(Vector3(-9, 0.02, 3.5), V0.AISLE_MAIN_M, V0.C_CALM, "main aisle %.2f m" % V0.AISLE_MAIN_M) + _aisle_swatch(Vector3(-4, 0.02, 3.5), V0.AISLE_KITCHEN_M, V0.C_WAIT_SOFT, "kitchen %.2f m" % V0.AISLE_KITCHEN_M) + _aisle_swatch(Vector3(0.5, 0.02, 3.5), V0.AISLE_PINCH_M, V0.C_ALERT, "pinch %.2f m" % V0.AISLE_PINCH_M) + + var rig := CameraRig.new() + rig.interactive = true + add_child(rig) + rig.set_mode(CameraRig.Mode.ORBIT_B) + rig.frame(Vector3(0, 1.0, 0), 18.0, 20.0, 17.0) + +func _place(node: Node3D, x: float, title: String, dim: String) -> float: + node.position = Vector3(x, 0, 0) + add_child(node) + var l1 := Placeholder.label(title, 0.30, Color.WHITE) + l1.position = Vector3(x, 2.55, 0) + add_child(l1) + var l2 := Placeholder.label(dim, 0.22, Color("9fb3c8")) + l2.position = Vector3(x, 2.25, 0) + add_child(l2) + return x + +func _grid() -> void: + # 1 m grid lines so scale is legible without reading every label + var mat := MatLib.flat(Color("2f3540"), true) + for i in range(-13, 14): + var line := MeshInstance3D.new() + var m := BoxMesh.new() + m.size = Vector3(0.02, 0.001, 12) + line.mesh = m + line.material_override = mat + line.position = Vector3(i, 0.01, 0) + add_child(line) + +func _aisle_swatch(pos: Vector3, width: float, color: Color, text: String) -> void: + var strip := Placeholder.floor_tile(Vector2(width, 2.0), color) + strip.material_override = MatLib.transparent(color, 0.45) + strip.position = pos + add_child(strip) + var l := Placeholder.label(text, 0.20, color.lightened(0.3)) + l.position = pos + Vector3(0, 0.6, 0) + add_child(l) diff --git a/visual/RestaurantVisualSandbox/scripts/scale_reference.gd.uid b/visual/RestaurantVisualSandbox/scripts/scale_reference.gd.uid new file mode 100644 index 0000000..f10f3e2 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/scale_reference.gd.uid @@ -0,0 +1 @@ +uid://b061ogpqm2aep diff --git a/visual/RestaurantVisualSandbox/scripts/service_view.gd b/visual/RestaurantVisualSandbox/scripts/service_view.gd new file mode 100644 index 0000000..feb7f8b --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/service_view.gd @@ -0,0 +1,413 @@ +class_name ServiceView +extends Node3D +## Renderer for a MockService snapshot (§15 presentation layer). It reads a per-beat +## snapshot of facts and only chooses HOW to show them — actor placement, disc/bar +## colours, overlays, HUD. It decides no gameplay truth. VisualOnly. + +enum Overlay { OFF, MINIMAL, FULL, SELECTION } + +var restaurant: Restaurant +var overlay_mode: int = Overlay.MINIMAL +var _selected := "" + +# actor pools +var _cust: Array = [] # customer capsules +var _servers: Array = [] # 3 +var _cooks: Array = [] # 4 +var _host: Node3D +var _busser: Node3D + +# overlay element pools / groups +var _pips: Array = [] # patience pips (parallel to _cust) +var _rings: Array = [] # workload rings (staff) +var _plates: Array = [] # pass plates +var _qlabels: Array = [] # station queue-count labels +var _paths := Node3D.new() # server->target hint lines +var _overlay_root := Node3D.new() + +# HUD +var _hud: CanvasLayer +var _hud_title: Label +var _hud_clock: Label +var _hud_head: Label +var _hud_mode: Label + +func setup(r: Restaurant) -> void: + restaurant = r + add_child(_overlay_root) + _overlay_root.name = "Overlays" + _overlay_root.add_child(_paths) + _build_staff() + _build_station_overlays() + _build_hud() + +# ---- construction --------------------------------------------------------- + +func _build_staff() -> void: + _host = Placeholder.person(V0.Role.HOST) + _host.position = restaurant.host_pos + _host.rotation.y = PI + add_child(_host) + _busser = Placeholder.person(V0.Role.BUSSER) + _busser.position = restaurant.dish_pos + add_child(_busser) + _rings.append(_ring()) # host ring + _rings.append(_ring()) # busser ring + for i in 3: + var s := Placeholder.person(V0.Role.SERVER) + s.position = restaurant.pass_center + Vector3(0.6 * i - 0.6, 0, 0.9) + add_child(s) + _servers.append(s) + _rings.append(_ring()) + for st in restaurant.stations: + var c := Placeholder.person(V0.Role.COOK) + c.position = st["pos"] + Vector3(0, 0, 0.9) + c.rotation.y = 0 + add_child(c) + _cooks.append(c) + +func _build_station_overlays() -> void: + for st in restaurant.stations: + var lbl := Placeholder.label("", 0.26, Color.WHITE) + lbl.position = st["pos"] + Vector3(0, 2.55, 0) + _overlay_root.add_child(lbl) + _qlabels.append(lbl) + +func _ring(color := V0.C_IDLE) -> MeshInstance3D: + var mi := MeshInstance3D.new() + var tm := TorusMesh.new() + tm.inner_radius = 0.42 + tm.outer_radius = 0.55 + mi.mesh = tm + mi.material_override = MatLib.flat(color, false, 0.7) + mi.position.y = 0.03 + mi.visible = false + _overlay_root.add_child(mi) + return mi + +func _get_customer(i: int) -> Node3D: + while _cust.size() <= i: + var c := Placeholder.person(V0.Role.CUSTOMER) + add_child(c) + _cust.append(c) + var pip := _pip() + _pips.append(pip) + return _cust[i] + +func _pip() -> MeshInstance3D: + var mi := MeshInstance3D.new() + var sm := SphereMesh.new() + sm.radius = 0.13 + sm.height = 0.26 + mi.mesh = sm + mi.material_override = MatLib.flat(V0.C_CALM, false, 0.7) + mi.visible = false + _overlay_root.add_child(mi) + return mi + +func _plate(i: int) -> MeshInstance3D: + while _plates.size() <= i: + var p := Placeholder.box(Vector3(0.28, 0.06, 0.28), V0.C_READY, false) + p.material_override = MatLib.flat(V0.C_READY, false, 0.5) + p.visible = false + _overlay_root.add_child(p) + _plates.append(p) + return _plates[i] + +# ---- apply a snapshot ----------------------------------------------------- + +func apply(snap: Dictionary) -> void: + _apply_tables(snap) + _apply_stations(snap) + _apply_customers(snap) + _apply_staff(snap) + _apply_pass(snap) + _hud_update(snap) + _apply_overlay_visibility() + +func _apply_tables(snap: Dictionary) -> void: + var states: Dictionary = snap["tables"] + for t in restaurant.tables: + var state: int = states.get(t["id"], V0.TableState.AVAILABLE) + var disc: MeshInstance3D = t["disc"] + var col: Color = V0.TABLE_STATE_COLOR[state] + if t["id"] == _selected and overlay_mode == Overlay.SELECTION: + col = col.lightened(0.35) + disc.material_override = MatLib.flat(col, false, 0.9) + # tint the tabletop itself so state reads from the management view (§22/§23) + var top: MeshInstance3D = t["node"].get_node_or_null("Top") + if top: + if state == V0.TableState.AVAILABLE: + top.material_override = MatLib.flat(Color("6b7078")) + else: + top.material_override = MatLib.flat(col.darkened(0.1), false, 0.15) + t["_state"] = state + +func _apply_stations(snap: Dictionary) -> void: + var arr: Array = snap["stations"] + for i in restaurant.stations.size(): + var st: Dictionary = restaurant.stations[i] + var sstate: int = arr[i][0] + var q: int = arr[i][1] + var bar: MeshInstance3D = st["bar"] + var col: Color = V0.STATION_STATE_COLOR[sstate] + bar.material_override = MatLib.flat(col, false, 0.7) + var fill: float = clampf(0.18 + q * 0.16, 0.18, 1.0) + bar.scale.y = fill + _qlabels[i].text = ("queue %d" % q) if q > 0 else "" + _qlabels[i].modulate = col + +func _apply_customers(snap: Dictionary) -> void: + var idx := 0 + # seated customers at occupied tables + var states: Dictionary = snap["tables"] + for t in restaurant.tables: + var s: int = states.get(t["id"], V0.TableState.AVAILABLE) + if s in [V0.TableState.OCCUPIED, V0.TableState.WAITING_ORDER, V0.TableState.WAITING_FOOD, V0.TableState.EATING, V0.TableState.NEEDS_PAYMENT]: + var seats := _seat_world(t) + var n: int = min(int(t["seats"]), seats.size()) + for k in n: + var c := _get_customer(idx) + c.visible = true + c.position = seats[k] + # an unhappy table (waiting too long) shows a red pip in FULL + var p := 0.9 + if s == V0.TableState.WAITING_FOOD: + p = 0.35 + elif s == V0.TableState.NEEDS_PAYMENT: + p = 0.6 + _set_pip(idx, c.position, p, s == V0.TableState.WAITING_FOOD) + idx += 1 + # waiting parties at the host queue + var waiting: int = snap["waiting"] + for w in waiting: + var c := _get_customer(idx) + c.visible = true + var slot: Vector3 = restaurant.waiting_slots[min(w, restaurant.waiting_slots.size() - 1)] + c.position = slot + Vector3(0, 0, w * 0.5) + var p: float = clampf(0.85 - 0.14 * w - (0.28 if snap["scenario"] == 3 else 0.0), 0.08, 1.0) + _set_pip(idx, c.position, p, true) + idx += 1 + # customers walking out (abandon) + var leaving: int = snap.get("leaving", 0) + for l in leaving: + var c := _get_customer(idx) + c.visible = true + c.position = restaurant.entrance_pos + Vector3(-0.8 - l, 0, 0.6) + _set_pip(idx, c.position, 0.05, true) + idx += 1 + # hide the rest + for j in range(idx, _cust.size()): + _cust[j].visible = false + _pips[j].visible = false + +func _apply_staff(snap: Dictionary) -> void: + # host + _host.position = restaurant.host_pos if snap["host"] != "greet" else restaurant.host_pos + Vector3(-0.8, 0, 0.4) + _set_ring(0, restaurant.host_pos, snap["host"] == "overloaded", snap["host"] == "idle") + # servers + for i in _servers.size(): + var target = snap["servers"][i] if i < snap["servers"].size() else "idle" + var pos := _target_pos(target, i) + _servers[i].position = pos + var overloaded: bool = (int(snap.get("overload_server", -1)) == i) or (target == "overloaded") + _set_ring(2 + i, pos, overloaded, target == "idle") + _path_line(i, pos, target) + # busser + var bt = snap.get("busser", "idle") + _busser.position = _target_pos(bt, 0) if bt != "idle" else restaurant.dish_pos + _set_ring(1, _busser.position, false, bt == "idle") + +func _apply_pass(snap: Dictionary) -> void: + var n: int = snap["pass"] + for i in range(0, int(max(n, _plates.size()))): + var p := _plate(i) + if i < n: + p.visible = true + var col := V0.C_READY + if n >= 5 and i >= 3: + col = V0.C_ALERT # the pile is old/backed up + elif n >= 3 and i >= 2: + col = V0.C_WAIT_HARD + p.material_override = MatLib.flat(col, false, 0.6) + p.position = restaurant.pass_center + Vector3(-2.6 + i * 0.85, 0.12, 0.0) + else: + p.visible = false + +# ---- overlay + selection -------------------------------------------------- + +func set_overlay_mode(m: int) -> void: + overlay_mode = m + _apply_overlay_visibility() + +func cycle_overlay() -> void: + set_overlay_mode((overlay_mode + 1) % 4) + +func select_table(id: String) -> void: + _selected = id + +func _apply_overlay_visibility() -> void: + var full := overlay_mode == Overlay.FULL + var minimal := overlay_mode in [Overlay.MINIMAL, Overlay.FULL, Overlay.SELECTION] + # table discs + station bars are the baseline signal (MINIMAL+) + for t in restaurant.tables: + t["disc"].visible = minimal + for st in restaurant.stations: + st["bar"].visible = minimal + for l in _qlabels: + l.visible = minimal + # patience pips, workload rings, pass plates, paths only in FULL + for p in _pips: + p.visible = p.visible and full + for r in _rings: + if not full: + r.visible = false + _paths.visible = full + if _hud_mode: + _hud_mode.text = "overlays: %s" % Overlay.keys()[overlay_mode] + +# ---- helpers -------------------------------------------------------------- + +func _seat_world(t: Dictionary) -> Array: + var seats: int = t["seats"] + var c: Vector3 = t["center"] + var top: Vector2 = V0.TABLE_2TOP if seats <= 2 else V0.TABLE_4TOP + var dx := top.x * 0.5 + 0.34 + if seats <= 2: + return [c + Vector3(-dx, 0, 0), c + Vector3(dx, 0, 0)] + return [c + Vector3(-dx, 0, -0.22), c + Vector3(-dx, 0, 0.22), c + Vector3(dx, 0, -0.22)] + +func _target_pos(target, server_i: int) -> Vector3: + if target == "pass" or target == "overloaded": + return restaurant.pass_center + Vector3(0.7 * server_i - 0.7, 0, 0.9) + if target == "idle": + return restaurant.pass_center + Vector3(0.7 * server_i - 0.7, 0, 1.6) + # a table id + for t in restaurant.tables: + if t["id"] == target: + return t["center"] + Vector3(0, 0, 1.0) + return restaurant.pass_center + +func _set_pip(i: int, at: Vector3, patience: float, show: bool) -> void: + if i >= _pips.size(): + return + var pip: MeshInstance3D = _pips[i] + pip.position = at + Vector3(0, 2.05, 0) + pip.material_override = MatLib.flat(V0.patience_color(patience), false, 0.8) + pip.visible = show + +func _set_ring(ring_i: int, at: Vector3, overloaded: bool, idle: bool) -> void: + if ring_i >= _rings.size(): + return + var r: MeshInstance3D = _rings[ring_i] + r.position = Vector3(at.x, 0.03, at.z) + if overloaded: + r.material_override = MatLib.flat(V0.C_ALERT, false, 0.9) + elif idle: + r.material_override = MatLib.flat(V0.C_IDLE, false, 0.5) + else: + r.material_override = MatLib.flat(V0.C_ACTIVE, false, 0.6) + +func _path_line(i: int, from_pos: Vector3, target) -> void: + # thin hint line from a server to the table it is serving (FULL overlay only) + var line_name := "path_%d" % i + var old := _paths.get_node_or_null(line_name) + if old: + old.free() + if overlay_mode != Overlay.FULL: + return + if not (target is String) or target in ["idle", "pass", "overloaded"]: + return + var to_pos := _target_pos(target, i) + var dir := to_pos - from_pos + var length := dir.length() + if length < 0.2: + return + var mid := (from_pos + to_pos) * 0.5 + Vector3(0, 0.06, 0) + var line := Placeholder.box(Vector3(0.06, 0.03, length), V0.C_ACTIVE, false) + line.material_override = MatLib.transparent(V0.C_ACTIVE, 0.55) + line.position = mid + line.rotation.y = atan2(dir.x, dir.z) # align local +Z with the direction + line.name = line_name + _paths.add_child(line) + +# ---- HUD (2D, CanvasLayer) ------------------------------------------------ + +func _build_hud() -> void: + _hud = CanvasLayer.new() + add_child(_hud) + var panel := ColorRect.new() + panel.color = Color(0, 0, 0, 0.46) + panel.position = Vector2(24, 20) + panel.size = Vector2(640, 156) + _hud.add_child(panel) + _hud_title = _mk_label(Vector2(40, 28), 27, Color.WHITE) + _hud_clock = _mk_label(Vector2(42, 64), 20, Color("8ecae6")) + _hud_head = _mk_label(Vector2(42, 92), 18, Color("ffd166")) + _hud_head.custom_minimum_size = Vector2(590, 44) + _hud_head.size = Vector2(590, 44) + _hud_head.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _hud_mode = _mk_label(Vector2(42, 150), 15, Color("9fb3c8")) + var tag := _mk_label(Vector2(330, 150), 15, Color("e07a5f")) + tag.text = "MOCK / SCRIPTED — not a simulation" + _build_legend() + +func _mk_label(pos: Vector2, sz: int, col: Color) -> Label: + var l := Label.new() + l.position = pos + l.add_theme_font_size_override("font_size", sz) + l.add_theme_color_override("font_color", col) + _hud.add_child(l) + return l + +func _build_legend() -> void: + var legend := VBoxContainer.new() + legend.position = Vector2(24, 900) + _hud.add_child(legend) + var roles := [ + ["Customer", V0.ROLE[V0.Role.CUSTOMER]["color"]], + ["Host (clipboard)", V0.ROLE[V0.Role.HOST]["color"]], + ["Server (tray)", V0.ROLE[V0.Role.SERVER]["color"]], + ["Cook (toque)", V0.ROLE[V0.Role.COOK]["color"]], + ["Busser (bin)", V0.ROLE[V0.Role.BUSSER]["color"]], + ] + var states := [ + ["available", V0.C_CALM], ["waiting food", V0.C_WAIT_HARD], + ["needs payment / ready", V0.C_READY], ["dirty", V0.C_DIRTY], + ["congested / overloaded", V0.C_ALERT], ["idle", V0.C_IDLE], + ] + _legend_row(legend, "ROLES", roles) + _legend_row(legend, "STATES", states) + +func _legend_row(parent: VBoxContainer, title: String, items: Array) -> void: + var t := Label.new() + t.text = title + t.add_theme_font_size_override("font_size", 15) + t.add_theme_color_override("font_color", Color("cfd8e3")) + parent.add_child(t) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 12) + parent.add_child(row) + for it in items: + var chip := HBoxContainer.new() + var sw := ColorRect.new() + sw.color = it[1] + sw.custom_minimum_size = Vector2(16, 16) + chip.add_child(sw) + var lb := Label.new() + lb.text = " " + it[0] + lb.add_theme_font_size_override("font_size", 14) + lb.add_theme_color_override("font_color", Color("aeb7c2")) + chip.add_child(lb) + row.add_child(chip) + +func _hud_update(snap: Dictionary) -> void: + if not _hud_title: + return + _hud_title.text = "%d — %s" % [snap["scenario"] + 1, snap["scenario_name"]] + _hud_clock.text = "service %s" % snap["clock"] + _hud_head.text = snap["headline"] + +func set_hud_visible(v: bool) -> void: + if _hud: + _hud.visible = v diff --git a/visual/RestaurantVisualSandbox/scripts/service_view.gd.uid b/visual/RestaurantVisualSandbox/scripts/service_view.gd.uid new file mode 100644 index 0000000..932dc94 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/service_view.gd.uid @@ -0,0 +1 @@ +uid://g274g6qxtbel diff --git a/visual/RestaurantVisualSandbox/scripts/stage.gd b/visual/RestaurantVisualSandbox/scripts/stage.gd new file mode 100644 index 0000000..3708fd8 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/stage.gd @@ -0,0 +1,53 @@ +class_name Stage +extends RefCounted +## Shared environment + lighting. Two visual treatments (§27): +## "A" — clean stylized management sim: higher ambient, soft light, flat-ish, readable. +## "B" — grounded but readable: lower ambient, stronger sun + shadows + SSAO, muted. +## The SAME gray-box geometry is rendered under both to compare (§27). VisualOnly. + +static func setup(root: Node, treatment := "A") -> void: + var we := WorldEnvironment.new() + we.name = "WorldEnvironment" + var env := Environment.new() + env.background_mode = Environment.BG_COLOR + env.tonemap_mode = Environment.TONE_MAPPER_ACES + env.tonemap_white = 6.0 + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + + var sun := DirectionalLight3D.new() + sun.name = "Sun" + sun.rotation_degrees = Vector3(-52.0, -46.0, 0.0) + sun.shadow_enabled = true + sun.shadow_blur = 1.5 + + if treatment == "B": + env.background_color = Color("0b0d10") + env.ambient_light_color = Color("5b636e") + env.ambient_light_energy = 0.22 + env.ssao_enabled = true + env.ssao_intensity = 2.2 + env.ssao_radius = 0.6 + env.sdfgi_enabled = false + sun.light_energy = 1.35 + sun.light_color = Color("fff2d9") + else: # "A" + env.background_color = Color("161b22") + env.ambient_light_color = Color("aab4c2") + env.ambient_light_energy = 0.75 + env.ssao_enabled = true + env.ssao_intensity = 1.0 + env.ssao_radius = 0.5 + sun.light_energy = 1.25 + sun.light_color = Color("fff6ea") + + we.environment = env + root.add_child(we) + root.add_child(sun) + + # a soft fill from the opposite side keeps silhouettes readable in both + var fill := DirectionalLight3D.new() + fill.name = "Fill" + fill.rotation_degrees = Vector3(-28.0, 140.0, 0.0) + fill.light_energy = 0.25 + fill.shadow_enabled = false + root.add_child(fill) diff --git a/visual/RestaurantVisualSandbox/scripts/stage.gd.uid b/visual/RestaurantVisualSandbox/scripts/stage.gd.uid new file mode 100644 index 0000000..11b07f7 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/stage.gd.uid @@ -0,0 +1 @@ +uid://b1blnonrbs7uo diff --git a/visual/RestaurantVisualSandbox/scripts/v0_config.gd b/visual/RestaurantVisualSandbox/scripts/v0_config.gd new file mode 100644 index 0000000..8541336 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/v0_config.gd @@ -0,0 +1,113 @@ +extends Node +## V0 — Shared configuration singleton (autoload name: `V0`). +## +## Single source of truth for the LOCKED scale standard (§17), the role/state +## visual language (§21, §22), and naming. Everything in the sandbox reads its +## numbers and colours from here so nothing is scaled "to look right" per-scene. +## +## VISUAL TRUTH BOUNDARY (§15): none of this owns gameplay truth. All state here +## is presentation vocabulary driven by a Mock/Scripted timeline. See +## docs/art/READABILITY-CONTRACT.md and V0-PROTOTYPE-CONTRACT.md. + +# --------------------------------------------------------------------------- +# Scale standard — metric. 1 Godot unit = 1 metre. (§17) +# These are visual-testing starting points, not final architectural rules. +# --------------------------------------------------------------------------- +const SCALE_UNIT_M := 1.0 + +const HUMAN_HEIGHT_M := 1.80 # adult, range 1.75–1.85 +const HUMAN_RADIUS_M := 0.25 # shoulder half-width for a capsule proxy + +const DOOR_HEIGHT_M := 2.05 # interior door, range 2.0–2.1 +const DOOR_WIDTH_M := 0.95 # range 0.85–1.0 + +const TABLE_HEIGHT_M := 0.75 # dining table, range 0.74–0.78 +const TABLE_2TOP := Vector2(0.70, 0.70) +const TABLE_4TOP := Vector2(1.10, 0.80) + +const CHAIR_SEAT_H_M := 0.45 # range 0.43–0.48 +const CHAIR_FOOTPRINT_M := 0.48 + +const COUNTER_HEIGHT_M := 0.92 # kitchen counter, range 0.88–0.95 +const APPLIANCE_H_M := 0.90 # major appliance block height +const APPLIANCE_W_M := 0.80 + +const WALL_HEIGHT_M := 3.00 # interior ceiling height +const WALL_THICK_M := 0.15 + +# Circulation widths — one comfortable path and one deliberate pinch (§18) +const AISLE_MAIN_M := 1.40 # main dining circulation +const AISLE_KITCHEN_M := 1.10 # kitchen working aisle +const AISLE_PINCH_M := 0.85 # the intentional narrow-risk choke point + +# --------------------------------------------------------------------------- +# Role palette (§21). Colour is ONE signal; every role also carries a second, +# non-colour signal (silhouette height, prop, headwear, or marker) so roles +# stay distinguishable without relying on colour alone. +# --------------------------------------------------------------------------- +enum Role { CUSTOMER, HOST, SERVER, COOK, BUSSER, MANAGER } + +const ROLE := { + Role.CUSTOMER: {"name": "Customer", "color": Color("d9c7a3"), "prop": "none", "marker": "none", "height": 1.78}, + Role.HOST: {"name": "Host", "color": Color("2ec4b6"), "prop": "podium", "marker": "none", "height": 1.82}, + Role.SERVER: {"name": "Server", "color": Color("3a86ff"), "prop": "tray", "marker": "none", "height": 1.80}, + Role.COOK: {"name": "Cook", "color": Color("f4f4f4"), "prop": "toque", "marker": "none", "height": 1.80}, + Role.BUSSER: {"name": "Busser", "color": Color("52b788"), "prop": "bin", "marker": "none", "height": 1.79}, + Role.MANAGER: {"name": "Manager", "color": Color("9b5de5"), "prop": "clipboard", "marker": "star", "height": 1.83}, +} + +# --------------------------------------------------------------------------- +# State colours (§22). Used by floating status pips / table discs / station bars. +# Chosen so "calm" reads cool/green and "pressure" reads warm/red at a glance. +# --------------------------------------------------------------------------- +const C_CALM := Color("52b788") # green — available / normal / healthy +const C_ACTIVE := Color("3a86ff") # blue — occupied / working / in-progress +const C_WAIT_SOFT := Color("ffd166") # amber — waiting, still patient +const C_WAIT_HARD := Color("f77f00") # orange — waiting too long / growing queue +const C_ALERT := Color("e63946") # red — unhappy / congested / overloaded +const C_DIRTY := Color("8d6b4b") # brown — needs cleaning +const C_READY := Color("00b4d8") # cyan — food ready / needs payment +const C_IDLE := Color("9aa0a6") # grey — idle / unavailable + +# Table lifecycle states (§22) +enum TableState { AVAILABLE, PREPARING, OCCUPIED, WAITING_ORDER, WAITING_FOOD, EATING, NEEDS_PAYMENT, DIRTY, CLEANING } +const TABLE_STATE_COLOR := { + TableState.AVAILABLE: C_CALM, + TableState.PREPARING: C_IDLE, + TableState.OCCUPIED: C_ACTIVE, + TableState.WAITING_ORDER: C_WAIT_SOFT, + TableState.WAITING_FOOD: C_WAIT_HARD, + TableState.EATING: C_ACTIVE, + TableState.NEEDS_PAYMENT: C_READY, + TableState.DIRTY: C_DIRTY, + TableState.CLEANING: C_CALM, +} +const TABLE_STATE_NAME := { + TableState.AVAILABLE: "Available", TableState.PREPARING: "Preparing", + TableState.OCCUPIED: "Seated", TableState.WAITING_ORDER: "Waiting to order", + TableState.WAITING_FOOD: "Waiting for food", TableState.EATING: "Eating", + TableState.NEEDS_PAYMENT: "Needs payment", TableState.DIRTY: "Dirty", + TableState.CLEANING: "Being cleaned", +} + +# Kitchen station pressure (§22) +enum StationState { NORMAL, QUEUE, CONGESTED, FOOD_READY, IDLE, UNAVAILABLE } +const STATION_STATE_COLOR := { + StationState.NORMAL: C_CALM, StationState.QUEUE: C_WAIT_SOFT, + StationState.CONGESTED: C_ALERT, StationState.FOOD_READY: C_READY, + StationState.IDLE: C_IDLE, StationState.UNAVAILABLE: Color("4a4e54"), +} + +# Customer patience → colour ramp (green → amber → red) +static func patience_color(p: float) -> Color: + # p in [0,1]; 1 = fresh/patient, 0 = out of patience + p = clampf(p, 0.0, 1.0) + if p > 0.5: + return C_CALM.lerp(C_WAIT_SOFT, (1.0 - p) * 2.0) + return C_WAIT_SOFT.lerp(C_ALERT, (0.5 - p) * 2.0) + +# --------------------------------------------------------------------------- +# Naming (§14) — labels that must never imply authoritative simulation truth. +# --------------------------------------------------------------------------- +const TRACK_NAME := "V0 — 3D Restaurant Readability Sandbox" +const DATA_KIND := "Mock / Scripted / VisualOnly" # never "SimulationResult" diff --git a/visual/RestaurantVisualSandbox/scripts/v0_config.gd.uid b/visual/RestaurantVisualSandbox/scripts/v0_config.gd.uid new file mode 100644 index 0000000..793bc95 --- /dev/null +++ b/visual/RestaurantVisualSandbox/scripts/v0_config.gd.uid @@ -0,0 +1 @@ +uid://darfmb1bh5phn diff --git a/visual/RestaurantVisualSandbox/tests/.gitkeep b/visual/RestaurantVisualSandbox/tests/.gitkeep new file mode 100644 index 0000000..e69de29