diff --git a/docs/agents/PLAYTEST-2026-09-02-menus.md b/docs/agents/PLAYTEST-2026-09-02-menus.md new file mode 100644 index 00000000..021f847c --- /dev/null +++ b/docs/agents/PLAYTEST-2026-09-02-menus.md @@ -0,0 +1,127 @@ +# Play-test, 2026-09-02 (second) β€” **P5 IS MET**, and four findings + +## βœ… P5's gate is MET β€” the human clicked through it + +> *"Menu walk and navigation is fine. Video skips too. Extras open. New Game +> shows new game intro video."* + +**P5 is done.** Its gate was *"a human clicks through it"*, the retro said it had +been waiting on that and not on code for the whole milestone, and it has now +happened. `PORT-MISSION.md` is updated. + +The human also confirmed the NEW GAME gap is understood and acceptable: +*"Deliberate AFAIK, in actual game the difficulty select comes first."* The port +announces the two screens it skips; that stays as it is. + +--- + +## F1 β€” πŸ”΄ The menu DOES repeat on a held direction. Ours does not. + +> *"Moving stick up/down and holding only moves one item. In game it actually +> continues to move when holding up/down, just at a medium pace so player does +> not need to move pad middle↔up/down, but also slow enough to see which item is +> selected and move to target."* + +**This settles the existence half of H1, and it settles it against us.** One step +per deflection was authored as the conservative choice precisely because nobody +knew. Now somebody has watched the real game: **it repeats.** + +⚠️ **The RATE is still not measured, and it must not be guessed.** The human's +description bounds it usefully and does not supply a number: fast enough that a +player need not return the stick to centre, slow enough to read the selection as +it passes. That is a range, not a value. + +* **Decoder β€” measure it.** Hold a direction in Canary and count. Two numbers: + the **initial delay** before the first repeat, and the **repeat interval** + after it. Frames between cursor moves, at a stated present rate β€” a count, not + a stopwatch reading ([`TEMPORAL-VERIFICATION.md`](TEMPORAL-VERIFICATION.md)). + Also: does the d-pad differ from the stick? Does the rate accelerate while + held, or stay flat? +* **Port β€” implement the mechanism, take the number from the Decoder.** Do not + ship a placeholder rate: an invented interval here is indistinguishable from a + measured one a month from now, and this is the exact field where that has + already cost us once. + +## F2 β€” πŸ”΄ The sound effects are too loud. There is no mix at all. + +> *"Largely OK. Biggest notice is the volume, many effects are too loud."* + +**Measured, and the human is right.** Every clip plays at unity gain, because +**no volume or gain value exists anywhere** β€” not in `export/`, not in +`authored/`, not in the manifest: + +| | mean | max | +|---|---|---| +| **`se/confirm`** | **βˆ’17.7 dB** | **βˆ’0.0 dB** β€” at full scale | +| `se/move` | βˆ’24.1 | βˆ’1.4 | +| `se/back` | βˆ’21.0 | βˆ’5.7 | +| `bgm/main_menu` | βˆ’20.7 | βˆ’4.2 | + +`confirm` is the loudest thing in the export: **3 dB hotter in mean than the +music** and 6.4 dB above `move`. A game mixes SE against BGM on separate buses; +this port has one bus and no gains. + +* **Decoder β€” is the mix ON THE DISC?** The obvious place is the cue table: a + cue record commonly carries volume alongside the wave index, and + `sub_821C5580` is already known to play cue 1103. If per-cue or per-bus gain is + there, it is **decoded** and nobody has to choose. If it is provably not, say + so with reach and it becomes an authored mix. +* **Port β€” do not normalise in the exporter to fix this.** Re-levelling the file + destroys the relationship between clips and cannot be undone by a modder. + Gains belong at playback, as data, where a measured value can replace a chosen + one without re-exporting. + +## F3 β€” ❔ Something is missing on the title screen + +> *"I also think that there is a sound track or effect missing at the title +> screen."* + +The export carries exactly one music track, `bgm/main_menu.ogg`, and the port +plays nothing on the title. Whether the game does is unestablished. + +* **Decoder:** which cue, if any, does the **title** play? The menu's is decoded + (cue 1103 = `BGM_103` via `sub_821C5580`); the same route should answer the + title. And is there a one-shot **sting** when the plate appears, or when β’Ά is + accepted? Either would read as "something missing" to a player. +* ⚠️ A negative here needs a positive control (R4): show the method finding the + menu's cue before concluding the title has none. + +## F4 β€” πŸ”΄ β’Ά SKIPS FORWARD through the boot. We only implement one of the three. + +> *"In the game one can get the plate to immediately show by pressing β’Ά, +> essentially skip to it. So after the logos one can immediately skip to the main +> menu by pressing β’Ά three times: 1. skip intro video, 2. show plate, 3. the +> plate itself."* + +A measured behaviour of the real game, and a good one β€” it is how a returning +player gets past the boot. + +| press | in the game | in the port | +|---|---|---| +| β’Ά #1 | skips the intro video | βœ… implemented | +| **β’Ά #2** | **completes the title build-in and shows the plate immediately** | ❌ **missing** | +| β’Ά #3 | activates the plate β†’ main menu | βœ… implemented | + +* **Port:** β’Ά during the title build-in should jump the sequence to the plate's + arrival rather than being swallowed. ⚠️ **Careful what "jump" means** β€” the + plate has a declared arrival at `t=238` and the title a build-in ending at + `tβ‰ˆ118`. Whether the game *snaps* both clocks forward, or only reveals the + plate and leaves the title where it is, is a question, not a detail. Ask if + the capture does not say. +* **Decoder:** what does β’Ά do to the clocks? This is also a **second, cheap route + to the plate-arrival question** β€” a press that skips to the plate tells you + where the game thinks the plate belongs. +* πŸ“Œ And it bears on `REFUTED.md`: *"any title after the first one refuses + input"* is already narrowed to the attract-returned title. This is a third + input the boot title accepts. + +--- + +## H3 β€” the plate delay is ACCEPTED + +> *"Delay feels the same. Cannot verify it is exact same, but is sufficient."* + +Good enough to stop working on, **not** established as correct. Leave the row as +unattributed rather than closing it green; if the duration question is ever +settled by the pipeline work, check it against this rather than re-opening it +from scratch. diff --git a/docs/agents/decoder-loop.md b/docs/agents/decoder-loop.md index 4219330b..c02c84d2 100644 --- a/docs/agents/decoder-loop.md +++ b/docs/agents/decoder-loop.md @@ -37,9 +37,39 @@ The three load-bearing questions stand, and the first is now the most valuable: 3. **What Canary does to it** before a capture records it β€” present cadence, resolve, scale, gamma. -Also open and yours: **H1** β€” does a held direction repeat in the menus, and at -what rate? The 61 % arm threshold you decoded is adopted; one step per deflection -is still authored. +### πŸ”΄ Four asks from the 2026-09-02 menu play-test β€” [`PLAYTEST-2026-09-02-menus.md`](PLAYTEST-2026-09-02-menus.md) + +P5's gate is **met** (a human walked the menus). These came out of the same +session, and three of the four are yours. They are ahead of the pipeline work +because the port is blocked on two of them. + +1. **F1 β€” MEASURE THE MENU REPEAT RATE.** The human watched the real game: a held + direction **repeats**, *"at a medium pace… slow enough to see which item is + selected"*. That settles the existence half of H1 against our authored + one-step-per-deflection. Two numbers, and the port will not move without + them: the **initial delay** before the first repeat, and the **repeat + interval** after it. Frames between cursor moves at a stated present rate β€” a + count, not a stopwatch. Also: does the d-pad differ from the stick? Does it + accelerate while held, or stay flat? +2. **F2 β€” IS THE AUDIO MIX ON THE DISC?** The SFX are too loud and there is **no + gain value anywhere** in the export; `confirm` peaks at βˆ’0.0 dBFS and sits + 3 dB above the music in mean. A cue record commonly carries a volume beside + its wave index, and you already decoded `sub_821C5580` playing cue 1103. If + per-cue or per-bus gain is there it is **decoded** and nobody has to choose. + If it provably is not, say so with reach. +3. **F3 β€” WHAT DOES THE TITLE PLAY?** A human says something is missing there. + Which cue, if any, does the title screen play, and is there a **sting** when + the plate appears or when β’Ά is accepted? ⚠️ A negative needs a positive + control (R4): show the method finding the *menu's* cue before concluding the + title has none. +4. **F4 β€” WHAT DOES β’Ά DO TO THE CLOCKS?** In the real game, β’Ά during the title + build-in **reveals the plate immediately** β€” so the boot takes three presses: + skip video, reveal plate, accept plate. Does the game snap both clocks + forward, or only reveal the plate? πŸ“Œ This is also a **cheap second route to + the plate-arrival question**: a press that skips to the plate tells you where + the game thinks the plate belongs. And it is a third input the boot title + accepts, which narrows `REFUTED.md`'s *"any title after the first refuses + input"* further. ⚠️ Deliver a **series, not a settled value** β€” see [`TEMPORAL-VERIFICATION.md`](TEMPORAL-VERIFICATION.md), and note that the port's diff --git a/docs/agents/port-loop.md b/docs/agents/port-loop.md index c8644c2d..f6a4814e 100644 --- a/docs/agents/port-loop.md +++ b/docs/agents/port-loop.md @@ -17,17 +17,25 @@ drawn rather than frames different. Ask of any new check: **what would this stil report if the feature were entirely absent?** `tools/motion-census` exists for exactly that question; keep it in `check-all`. -⚠️ **P5's gate is "a human clicks through it" and is NOT claimed.** They signed -off the logos, and separately confirmed β’Ά, the stick and the submenus work. -Nobody has said the milestone is met β€” **do not record it as met on their -behalf.** If you believe it is ready, say so and ask. +## βœ… P5's GATE IS MET β€” the human walked it, 2026-09-02 -Still open, none of it blocking: **H1** (does a held direction repeat, and how -fast β€” one step per deflection stays authored) and **H3** (the plate; every named -cause is dead and the observation is unattributed β€” ⚠️ worth re-asking, since the -animation fix changed what the whole boot looks like and the original observation -may no longer reproduce). Both are in -[`../agents/PLAYTEST-2026-09-02.md`](../agents/PLAYTEST-2026-09-02.md). +> *"Menu walk and navigation is fine. Video skips too. Extras open. New Game +> shows new game intro video."* + +`PORT-MISSION.md` is updated. The NEW GAME gap is accepted as-is β€” they know the +difficulty select comes first in the real game and that the port announces it. + +### πŸ”΄ Four findings from the same session β€” read [`../agents/PLAYTEST-2026-09-02-menus.md`](../agents/PLAYTEST-2026-09-02-menus.md) + +| | | yours to do | +|---|---|---| +| **F1** | **The menu REPEATS on a held direction. Ours does not.** One step per deflection was authored as the safe choice; the human has now watched the real game and it repeats. | **Implement the mechanism. Take the RATE from the Decoder β€” do NOT ship a placeholder interval.** An invented rate here is indistinguishable from a measured one later, and this is the exact field where that already cost us. | +| **F2** | **SFX too loud, and there is no mix at all.** Measured: `confirm` βˆ’17.7 dB mean / **βˆ’0.0 dB peak**, 3 dB hotter than the music; no gain value exists anywhere in `export/` or `authored/`. | Add gains **at playback, as data** β€” a bus per kind. ⚠️ **Do NOT normalise in the exporter**: re-levelling destroys the relationship between clips and a modder cannot undo it. The Decoder is checking whether the mix is on the disc. | +| **F3** | **Something is missing on the title screen** β€” a track or a sting. The export has one music file and the port plays nothing on the title. | Wait for the Decoder; nothing to author yet. | +| **F4** | **β’Ά skips FORWARD through the boot, and we implement two of three presses.** β’Ά#1 skips the video βœ…, **β’Ά#2 reveals the plate immediately ❌ missing**, β’Ά#3 activates it βœ…. | Make β’Ά during the title build-in jump to the plate. ⚠️ Whether the game snaps **both** clocks forward or only reveals the plate is a question β€” ask rather than choose. | + +**H3, the plate delay, is ACCEPTED** β€” *"feels the same… sufficient"*. Stop +working on it. Leave the row unattributed rather than closing it green. ## Previous sole focus, 2026-09-02 β€” RESOLVED, kept for the method diff --git a/docs/port/PORT-MISSION.md b/docs/port/PORT-MISSION.md index b8c75028..de6f8c73 100644 --- a/docs/port/PORT-MISSION.md +++ b/docs/port/PORT-MISSION.md @@ -100,8 +100,8 @@ A milestone is done when its **artifact** exists, not when the code compiles. | **P2** | Keyframe animation | Buttons slide in. ~~Blocked on HANDOFF Q1 (the time unit). Do not invent it~~ β€” **Q1 is answered**: ramp linear, 2 units per rendered frame, 1 unit = 1/60 s. Gate met | | **P3** | Splash β†’ title, with the transition | Both screens back to back, unattended | | **P4** | Intro video | `ADV.wmv` plays with audio (Β§6) | -| **P5** | Main menu: navigation, focus states, β’Ά into a submenu, B back | A human clicks through it | -| **P6** | Audio β€” menu BGM and move/confirm SFX | Sound on the P5 gate. ~~Looping is blocked on HANDOFF Q10~~ β€” **Q10 is answered**: two stems of one performance, played together. Gate met | +| **P5** | Main menu: navigation, focus states, β’Ά into a submenu, B back | ~~A human clicks through it~~ β€” βœ… **GATE MET 2026-09-02.** A human walked it: *"Menu walk and navigation is fine. Video skips too. Extras open."* [`../agents/PLAYTEST-2026-09-02-menus.md`](../agents/PLAYTEST-2026-09-02-menus.md) | +| **P6** | Audio β€” menu BGM and move/confirm SFX | Sound on the P5 gate. ~~Looping is blocked on HANDOFF Q10~~ β€” **Q10 is answered**: two stems of one performance, played together. πŸ”΄ **Gate NOT claimed**: the same play-test found the SFX mix wrong (F2), and "sound on the P5 gate" means the right sound. | | **P7** | New-game intro video after NEW GAME | Plays, then returns to a defined state | Work the lowest unfinished milestone. When one is blocked on an RE answer, say so diff --git a/port/scripts/gamepad.gd b/port/scripts/gamepad.gd index eade6bca..1bb3884a 100644 --- a/port/scripts/gamepad.gd +++ b/port/scripts/gamepad.gd @@ -48,12 +48,17 @@ extends RefCounted ## not again until it comes back. That makes it behave exactly like the d-pad, ## which needs no latch because a button already is an edge. ## -## ⚠️ **AUTHORED, NOT MEASURED β€” and deliberately the conservative half.** -## Whether the real game *repeats* while a direction is held, and how fast, is -## unknown; it is an oracle question. One deflection = one step cannot run away -## and cannot invent a rate. If the game does repeat, this is a difference a -## human will notice as "I have to flick it again", and the fix is a measured -## repeat interval β€” not a guessed one. Logged for the Decoder as `pad-repeat`. +## ⚠️ ~~AUTHORED, NOT MEASURED β€” and deliberately the conservative half.~~ +## πŸ”΄ **THE GAME DOES REPEAT, and this paragraph predicted its own refutation.** +## It said: *"If the game does repeat, this is a difference a human will notice +## as 'I have to flick it again'."* On 2026-09-02 a human who has played both +## reported exactly that β€” *"holding only moves one item. In game it actually +## continues to move when holding up/down, just at a medium pace"*. +## +## So one-step-per-deflection is no longer the conservative reading; it is a +## known defect, and keeping it would be choosing a wrong behaviour over an +## approximate one. The repeat is implemented below. **Its RATE is authored and +## its FACT is not** β€” see `REPEAT_DELAY`. ## βœ… DECODED 2026-09-01, and it replaces an authored value. ## @@ -88,11 +93,37 @@ const ENTER := 0.61 ## smaller number. const RELEASE := 0.4 +## ## 3. A held direction repeats +## +## πŸ”΄ **THE FACT IS REPORTED, THE RATE IS AUTHORED. Do not read the second as +## carried by the first.** A human who has played both said the game repeats at +## *"a medium pace … slow enough to see which item is selected"* β€” that settles +## THAT it repeats and gives an order of magnitude, nothing more. Nobody has +## measured an interval off the running game, and `pad-repeat` stays open for the +## Decoder. +## +## πŸ“Œ **A constant interval is the right SHAPE, and that part is measured.** The +## game digitises the left stick to four direction bits at 61 % deflection +## (`ENTER` above), so it cannot see a deflection magnitude at all β€” a repeat it +## drives cannot be rate-by-how-far-you-push. That excludes the one alternative +## model, so only the constants are open. +## +## The delay exists so a deliberate single step never repeats by accident: a +## flick to move one item is held for well under 0.4 s. +## +## ⚠️ **These two numbers change how the menu feels and only a human can judge +## them** β€” the same standing as `ENTER`'s 0.61. Too fast reads as a cursor that +## runs away; too slow reads as the defect this replaces. +const REPEAT_DELAY := 0.40 +const REPEAT_INTERVAL := 0.20 + ## Only the left stick. The triggers are axes too, and latching them here would ## silently swallow input the port does not read yet but might. const STICK := [JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y] var _latched: Dictionary = {} +var _repeat_direction := 0 +var _repeat_clock := 0.0 ## Add the joypad buttons the built-in map omits. Returns a human-readable line, @@ -156,6 +187,53 @@ func accepts(event: InputEvent) -> bool: return true +## Which way a direction is being HELD right now, as -1 (up), 0 or +1 (down). +## +## πŸ”΄ **Polled at the DEVICE, never through `Input.is_action_pressed`.** `ui_up` +## and `ui_down` are bound to the stick axis at Godot's 0.50 action deadzone, +## while this port steps at the game's measured 0.61. Polling the action would +## repeat throughout the 0.50–0.61 band β€” the exact band `ENTER` exists to +## exclude β€” so the repeat would contradict the threshold on the same stick. +## That is the input-map lesson again: assert the device, not the layer above it. +func held_direction() -> int: + # The stick, from the latch `accepts()` already maintains, so the repeat and + # the first step read one state and cannot disagree about hysteresis. + var stick := int(_latched.get(JOY_AXIS_LEFT_Y, 0)) + if stick != 0: + return stick + for device in Input.get_connected_joypads(): + if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_UP): + return -1 + if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_DOWN): + return 1 + if Input.is_key_pressed(KEY_UP): + return -1 + if Input.is_key_pressed(KEY_DOWN): + return 1 + return 0 + + +## One repeat step, or 0. Call once per frame with the frame's delta. +## +## The FIRST step is not this function's: it comes from the event edge in +## `_unhandled_input`, and the clock below starts from that same frame, so a held +## direction gives one step now and the next only after `REPEAT_DELAY`. A change +## of direction restarts the delay rather than inheriting the old cadence. +func repeat_due(delta: float) -> int: + var direction := held_direction() + if direction == 0 or direction != _repeat_direction: + _repeat_direction = direction + _repeat_clock = 0.0 + return 0 + _repeat_clock += delta + if _repeat_clock < REPEAT_DELAY: + return 0 + # Subtract rather than reset, so the cadence cannot drift with the frame rate + # -- at 140 fps and at 30 fps the same number of steps happen per second. + _repeat_clock -= REPEAT_INTERVAL + return direction + + ## The pads Godot can see, for the startup line. A run where the human believes ## a controller is connected and Godot disagrees should say so on its own, ## rather than presenting as unresponsive buttons.