diff --git a/authored/flow.json b/authored/flow.json index feedd4ee..3deb807c 100644 --- a/authored/flow.json +++ b/authored/flow.json @@ -29,7 +29,7 @@ }, { "screen": "title", - "why": "HANDOFF Q2/Q6: the boot reaches the title after the intro movie. The port holds here -- nothing takes the title's place until P5 gives it somewhere to go." + "why": "HANDOFF Q2/Q6: the boot reaches the title after the intro movie. This is the LAST step, and a last step is where the sequence stops rather than fading out -- a boot that ends by fading to black looks like a boot that crashed. P5 gave the title somewhere to go, but that is a HANDOVER and not another boot step: `--boot` still stops here, and `--boot --play` hands the same held title to the menu flow, where (A) opens TITLE_MENU. Kept as a stop rather than folded into `screens` because what the boot does is authored from a measured sequence, and what (A) does is a separate measurement." } ], "dwell": { diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 1476ab74..3d661501 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -66,6 +66,7 @@ git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconc | P6 audio | which BGM the menu plays | Q10 | ❔ **not on the disc.** All 32 banks are named `BGM_001`…`BGM_109` with no semantic name anywhere. The port is choosing a track, and that choice is authored. | | P6 looping | where a menu loop restarts | Q10 | ❔ `BGM_001` fades out at 167.663 s into 6.15 s of silence, and no loop-point field has been identified. A menu loop is authored. | | P5 focus marker | **the focus ring's spin PERIOD, and whether it loops** | Q1 + the 2026-08-28 *"groups hold"* answer | ❔ open, and the port is drawing a pose it knows is wrong. Derived at HANDOFF `9ca1eb5` **plus** `7eeae30`, which HANDOFF does not yet carry. `ptbtneff01` declares `t=120, rot 0` then an **untimed** `rot 360`. The port measured the two oracle captures at **~76°** and **~210°** — 134° apart, peak corr 0.968, null control 0.369 (`DECISIONS.md`) — so **0° is not a pose the game shows**, and the port draws 0° because a spin rate would be invented. Two unknowns, both the Decoder's: (a) under Q1's *replicated* reading `t=120` is when the **next** pose is reached, giving one revolution in 2.0 s, but this port's `pose_at` implements the other reading and switching it changes every screen's animation timing; (b) *"groups hold"* predicts a stop at 360 = 0, which contradicts both captures. **What settles it: two frames of one focused button a known time apart.** | +| P3/P5 — the title screen | **does the idle post-boot title show the `PRESS Ⓐ` plate?** | Q2 | 🔴 the port's boot ends on `title` (build 4), which has **no plate**, and P5 has just made Ⓐ the only way off it. Both states are captured — `live-title-build4-no-plate.png` and `live-title-press-a.png` — so the art is not the question; the **sequence** is: build 4 alone, build 4 with build 2 over it, or build 4 *then* the plate after a delay. Behavioural, so the port has no oracle for it. `press_start` (build 2) is already exported and unused. ⚠️ Fixing it also means drawing **two builds at once**, which this port has never done — a change to `ScreenView`, not a line in `flow.json`. Not blocking P5. | | P5 — Ⓑ on the main menu | **is Ⓑ what returns to the title, or the idle timer?** | Q5 | 🟡 stated in HANDOFF, no capture behind it. The title self-returns after ~8–10 s idle, so one unrecorded observation cannot separate them. `authored/flow.json` implements it and marks it *authored — likely but UNPROVEN*. **Not blocking** — P5 shipped with it — but it is the only navigation rule on that screen with nothing under it. Settled by one run that presses Ⓑ well inside the idle window, timestamped. | ## Answered since this file was last written — no longer blocking diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 14afa2da..d33f5ce5 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -1138,3 +1138,84 @@ and both belong to the Decoder: Filed in `BLOCKED.md` and asked over the message channel. What settles it is two frames of one focused button a known time apart. + +--- + +## P5 end to end — and the title does not say `PRESS Ⓐ`, 2026-08-29 + +The gate walk above starts on a screen. This is the whole thing, unattended, in +one run — the sequence PORT-MISSION names as the objective: + +```sh +xvfb-run -a godot --path port -- --boot --play \ + --script=accept,down,down,down,down,accept,cancel,cancel --shots=/tmp/e2e +``` + +``` +screen publisher_logo … settles at t=235 (3.917 s) + -> developer_logos at 4.70 s + -> video ADV at 8.60 s + video ended at 151.91 s + -> title at 151.91 s +boot sequence complete after 156.30 s, holding on title + menu on title +script[1] accept (A) -> main_menu +… +script[6] accept (EXTRAS) -> extras +script[7] cancel (B) -> main_menu focus restored to ptbtn05 +script[8] cancel (B) -> title +script complete after 166.76 s on title +``` + +Publisher wordmark → developer logos → `ADV` → title → Ⓐ → main menu → +navigate → Ⓐ → `EXTRAS` → Ⓑ (focus restored) → Ⓑ → title. Contact sheet shared. + +Two smaller things this run found, both fixed here: + +* the boot step's `why` in `authored/flow.json` still said *"nothing takes the + title's place until P5 gives it somewhere to go"*. P5 has. Rewritten to say + what is actually true — `--boot` still **stops** on the title, and `--play` + **hands the held title over**; the stop is not a bug and the handover is not + another boot step. +* an empty focus printed as a line that trailed off, which reads like a value + went missing rather than like there is none. The title is a screen with no + `buttons` that still takes Ⓐ, so it prints + `(none -- this screen has no focusable item)`. + +Also confirmed on the way: entering a submenu **directly** (`--menu=extras`) and +pressing Ⓑ enters the parent at its authored initial focus, not at a restored +one — there is no history to restore, and `MenuFlow.cancel` only claims a +restored focus when the stack agrees about where it is going. + +### 🔴 The port's title does not tell the player to press Ⓐ + +Found by running the objective end to end, which is the only thing that would +have found it: the boot's last step is `title` (build 4), and **build 4 has no +`PRESS Ⓐ BUTTON` plate**. P5 has now made Ⓐ the only way off that screen. + +This is not a guess about the art. Both states are captured off the running +game and they differ by exactly that plate: + +| | capture | +|---|---| +| title **without** the plate | `title-builds/live-title-build4-no-plate.png` | +| title **with** the plate | `title-builds/live-title-press-a.png` | + +And the plate is already exported — `press_start`, `GP_TITLE` build 2 (HANDOFF +Q2), sitting in `export/screens/title/` unused by anything. + +**This is P3's gate, not P5's, and P5 is what exposed it.** Recording rather +than fixing, for two reasons: + +1. Which state an idle post-boot title shows — build 4 alone, build 4 with the + plate over it, or build 4 *then* the plate after a delay — is **behavioural**, + and the port has no oracle for a sequence. The game demonstrably has both + states; nothing here says which one follows the intro movie. That is the + Decoder's. +2. Showing it would mean **drawing two builds at once**, which this port has + never done — every mode loads exactly one screen. That is a real change to + `ScreenView`, not a line in `flow.json`, and it should not be smuggled in + under a navigation milestone on the strength of "it looks more right". + +Filed in `BLOCKED.md`. Not blocking: P5's gate is Ⓐ into a submenu and Ⓑ back, +and both work. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index c3919f7d..fd0ca247 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -342,7 +342,7 @@ func _menu_enter(name: String, fresh: bool) -> void: _menu.enter(name, view.screen.get("buttons", [])) view.focused_id = _menu.focus() view.queue_redraw() - print(" menu on %s, focus %s" % [name, view.focused_id]) + print(" menu on %s, focus %s" % [name, _focus_label(view.focused_id)]) if not _script.is_empty() and not _script_started: _script_started = true _run_script() @@ -366,7 +366,7 @@ func _menu_arrive() -> void: _menu.stack[_menu.stack.size() - 1]["focus"] = String(action["restore_focus"]) view.focused_id = _menu.focus() view.queue_redraw() - print(" menu on %s, focus restored to %s" % [name, view.focused_id]) + print(" menu on %s, focus restored to %s" % [name, _focus_label(view.focused_id)]) else: _menu_enter(name, true) @@ -393,6 +393,14 @@ func _has_display(flag: String) -> bool: return false +## How a focus reads in the log. The title has no focusable item at all -- it is +## a screen with no `buttons` that still takes (A) -- and an empty string there +## printed as a line that trailed off, which reads like the value went missing +## rather than like there is none. +static func _focus_label(id: String) -> String: + return id if id != "" else "(none -- this screen has no focusable item)" + + func _capture(path: String) -> void: # Two frames: the first is the one this callback is still inside of. await RenderingServer.frame_post_draw @@ -483,7 +491,7 @@ func _run_script() -> void: return await _shoot("%02d_%s" % [i + 1, token]) print("script complete after %.2f s on %s, focus %s" - % [_elapsed, _menu.current(), view.focused_id]) + % [_elapsed, _menu.current(), _focus_label(view.focused_id)]) get_tree().quit(0) @@ -530,4 +538,4 @@ func _shoot(label: String) -> void: push_error("cannot write %s" % tmp) return DirAccess.rename_absolute(tmp, path) - print(" shot %s (%s, focus %s)" % [path, _menu.current(), view.focused_id]) + print(" shot %s (%s, focus %s)" % [path, _menu.current(), _focus_label(view.focused_id)])